1MooseX::Extended::ManuaUls:e:rTuCtoonrtirailb(u3t)ed PerMlooDsoecXu:m:eEnxttaetnidoend::Manual::Tutorial(3)
2
3
4

NAME

6       MooseX::Extended::Manual::Tutorial - Building a Better Moose
7

VERSION

9       version 0.35
10

GENESIS

12       MooseX::Extended is built on years of experience hacking on Moose and
13       being the lead designer of the Corinna <https://github.com/Ovid/Cor>
14       project to bring modern OO to the Perl language. We love Moose, but
15       over the years, it's become clear that there are some problematic
16       design choices. Further, Corinna is not yet in core as we write this
17       (though the Perl Steering Committee has accepted it), so for now, let's
18       see how far we can push the envelope. Interestingly, in some respects,
19       MooseX::Extended offers more than the initial versions of Corinna
20       (though this won't last).
21

BEST PRACTICES

23       MooseX::Extended has the philosophy of providing best practices, but
24       not enforcing them. We try to make many best practices the default, but
25       you can opt out of them. For more background, see the article Common
26       Problems in Object-Oriented Code
27       <https://ovid.github.io/articles/common-problems-in-object-oriented-
28       code.html>.  That's what lead to the creation of MooseX::Extended.
29
30       In particular, it's designed to make large-scale OOP systems written in
31       Moose easier to maintain by removing many common failure modes, while
32       still allowing you full control over what features you do and do not
33       want.
34
35       What follows is a fairly decent overview of MooseX::Extended. See the
36       documentation of individual modules for more information.
37
38   What's the Point.pm?
39       Let's take a look at a simple "Point" class in Moose. We want it to
40       have x/y coordinates, and the creation time as "seconds from epoch".
41       We'd also like to be able to "invert" points.
42
43           package My::Point {
44               use Moose;
45               has 'x'       => ( is => 'rw', isa => 'Num', writer  => 'set_x' );
46               has 'y'       => ( is => 'rw', isa => 'Num', writer  => 'set_y' );
47               has 'created' => ( is => 'ro', isa => 'Int', default => sub {time} );
48
49               sub invert {
50                   my $self = shift;
51                   my ( $x, $y ) = ( $self->x, $self->y );
52                   $self->set_x($y);
53                   $self->set_y($x);
54               }
55           }
56
57           1;
58
59       To the casual eye, that looks fine, but there are already many issues
60       with the above.
61
62       •   The class is not immutable
63
64           You almost always want to end your Moose classes with
65           "__PACKAGE__->meta->make_immutable". Doing this causes Moose to
66           close the class definition for modifications (if that doesn't make
67           sense, don't worry about it), and speeds up the code considerably.
68
69       •   Dirty namespace
70
71           Currently, "My::Point->can('has')" returns true, even though "has"
72           should not be a method. This, along with a bunch of other functions
73           exported into your class by Moose, can mislead your code and
74           confuse your method resolution order. For this reason, it's
75           generally recommended that you use "namespace::autoclean" or
76           "namespace::clean". To remove those functions from your class.
77
78       •   Unknown constructor arguments
79
80               my $point = My::Point->new( X => 3, y => 4 );
81
82           In the above, the first named argument should be "x", not "X".
83           Moose simply throws away unknown constructor arguments. One way to
84           handle this might be to set your fields as "required":
85
86               has 'x' => ( is => 'rw', isa => 'Num', writer  => 'set_x', required => 1 );
87               has 'x' => ( is => 'rw', isa => 'Num', writer  => 'set_y', required => 1 );
88
89           That causes "My::Point->new( X => 3, y => 4 )" to throw an
90           exception, but not this: "My::Point->new( x => 3, y => 4, z => 5
91           )". For this trivial example, it's probably not a big deal, but for
92           a large codebase, where many Moose classes might have a huge
93           variety of confusing arguments, it's easy to make mistakes.
94
95           For this, we recommend MooseX::StrictConstructor. Unknown arguments
96           are fatal.
97
98       •   Innappropriate constructor arguments
99
100               my $point = My::Point->new( x => 3, y => 4, created => 42 );
101
102           The above works, but the author of the class almost certainly
103           didn't intend for you to be passing "created" to the constructor,
104           but to the programmer reading the code, that's not always clear:
105
106               has 'created' => ( is => 'ro', isa => 'Int', default => sub {time} );
107
108           The fix for this is to add "init_arg => undef" to the attribute
109           definition and hope the maintenance programmer notices this:
110
111               has 'created' => ( is => 'ro', isa => 'Int', init_arg => undef, default => sub {time} );
112
113       •   Misspelled types
114
115           What if "created" was defined like this?
116
117               has 'created' => ( is => 'ro', isa => 'int', default => sub {time} );
118
119           The type constraint is named "Int", not "int". You won't find out
120           about that little issue until runtime. There are a number of ways
121           of dealing with this, but we recommend the Type::Tiny family of
122           type constraints. Misspelling a type name becomes a compile-time
123           failure:
124
125               use Types::Standard 'Int';
126               has 'created' => ( is => 'ro', isa => Int, default => sub {time} );
127
128       •   No signatures
129
130           Let's look at our method:
131
132                   sub invert {
133                       my $self = shift;
134                       my ( $x, $y ) = ( $self->x, $self->y );
135                       $self->set_x($y);
136                       $self->set_y($x);
137                   }
138
139           What if someone were to write "$point->invert( 4, 7 )"? That
140           wouldn't make any sense, but it also wouldn't throw an exception or
141           even a warning, despite it obviously not being what the programmer
142           wanted. The simplest solution is to just use signatures:
143
144               use feature 'signatures';
145               no warnings 'experimental::signatures'; # 5.34 and below
146
147               sub invert ($self) { ... }
148
149   Fixing our Moose class
150       Taking all of the above into consideration, we might rewrite our Moose
151       class as follows:
152
153           package My::Point {
154               use Moose;
155               use MooseX::StrictConstructor;
156               use Types::Standard qw(Num Int);
157               use feature 'signatures';
158               no warnings 'experimental::signatures';
159               use namespace::autoclean;
160
161               has 'x'       => ( is => 'rw', isa => Num, writer  => 'set_x' );
162               has 'y'       => ( is => 'rw', isa => Num, writer  => 'set_y' );
163               has 'created' => ( is => 'ro', isa => Int, init_arg => undef, default => sub {time} );
164
165               sub invert ($self) {
166                   my ( $x, $y ) = ( $self->x, $self->y );
167                   $self->set_x($y);
168                   $self->set_y($x);
169               }
170
171               __PACKAGE__->meta->make_immutable;
172           }
173
174           1;
175
176       That's a lot of boilerplate for a simple x/y point class! Out of the
177       box (but almost completely customisable), MooseX::Extended provides the
178       above for you.
179
180           package My::Point {
181               use MooseX::Extended types => [qw/Num Int/];
182
183               param [ 'x', 'y' ] => ( is => 'rw', isa => Num, writer => 1 );
184               field 'created'    => ( isa => Int, lazy => 0, default => sub {time} );
185
186               sub invert ($self) {
187                   my ( $x, $y ) = ( $self->x, $self->y );
188                   $self->set_x($y);
189                   $self->set_y($x);
190               }
191           }
192
193       No need use those various modules. No need to declare the class
194       immutable or end it with a true value (MooseX::Extended does these for
195       you). Instead of remembering a bunch of boilerplate, you can focus on
196       writing your code.
197

INSTANCE ATTRIBUTES

199       In the Moose world, we use the "has" function to declare an "attribute"
200       to hold instance data for your class. This function is still available,
201       unchanged in "MooseX::Extended", but two new functions are now
202       introduced, "param" and "field", which operate similarly to "has". Both
203       of these functions default to "is => 'ro'", so that may be omitted if
204       the attribute is read-only.
205
206       A "param" is a required parameter (defaults may be used). A "field" is
207       not intended to be passed to the constructor (but see the extended
208       explanation below). This makes it much easier for a developer, either
209       writing or reading the code, to be clear about the intended class
210       interface.
211
212       So instead of this (and having the poor maintenance programmer
213       wondering what is and is not allowed in the constructor):
214
215           has name     => (...);
216           has uuid     => (...);
217           has id       => (...);
218           has backlog  => (...);
219           has auth     => (...);
220           has username => (...);
221           has password => (...);
222           has cache    => (...);
223           has this     => (...);
224           has that     => (...);
225
226       You have this:
227
228           param name     => (...);
229           param backlog  => (...);
230           param auth     => (...);
231           param username => (...);
232           param password => (...);
233
234           field cache    => (...);
235           field this     => (...);
236           field that     => (...);
237           field uuid     => (...);
238           field id       => (...);
239
240       Now the interface is much clearer.
241
242   "param"
243           param name => ( isa => NonEmptyStr );
244
245       A similar function to Moose's "has". A "param" is required. You may
246       pass it to the constructor, or use a "default" or "builder" to supply
247       this value.
248
249       The above "param" definition is equivalent to:
250
251           has name => (
252               is       => 'ro',
253               isa      => NonEmptyStr,
254               required => 1,
255           );
256
257       If you want a parameter that has no "default" or "builder" and can
258       optionally be passed to the constructor, just use "required => 0".
259
260           param title => ( isa => Str, required => 0 );
261
262       Note that "param", like "field", defaults to read-only, "is => 'ro'".
263       You can override this:
264
265           param name => ( is => 'rw',  isa => NonEmptyStr );
266           # or
267           param name => ( is => 'rwp', isa => NonEmptyStr ); # adds _set_name
268
269       Otherwise, it behaves like "has". You can pass in any arguments that
270       "has" accepts.
271
272           # we'll make it private, but allow it to be passed to the constructor
273           # as `name`
274           param _name   => ( isa => NonEmptyStr, init_arg => 'name' );
275
276       The "param"'s "is" option accepts "rwp", like Moo. It will create a
277       writer in the name "_set_${attribute_name|".
278
279   "field"
280           field cache => (
281               isa     => InstanceOf ['Hash::Ordered'],
282               default => sub { Hash::Ordered->new },
283           );
284
285       A similar function to Moose's "has". A "field" is not intended to be
286       passed to the constructor, but you can still use "default" or
287       "builder", as normal.
288
289       The above "field" definition is equivalent to:
290
291           has cache => (
292               is       => 'ro',
293               isa      => InstanceOf['Hash::Ordered'],
294               init_arg => undef,        # not allowed in the constructor
295               default  => sub { Hash::Ordered->new },
296               lazy     => 1,
297           );
298
299       Note that "field", like "param", defaults to read-only, "is => 'ro'".
300       You can override this:
301
302           field some_data => ( is => 'rw',  isa => NonEmptyStr );
303           #
304           field some_data => ( is => 'rwp', isa => NonEmptyStr ); # adds _set_some_data
305
306       Otherwise, it behaves like "has". You can pass in any arguments that
307       "has" accepts.
308
309       The "field"'s "is" option accepts "rwp", like Moo. It will create a
310       writer in the name "_set_${attribute_name|".
311
312       If you pass "field" an "init_arg" with a defined value, the code will
313       usually throw a Moose::Exception::InvalidAttributeDefinition exception.
314       However, if the init_arg begins with an underscore, it's allowed. This
315       is designed to allow developers writing tests to supply their own
316       values more easily.
317
318           field cache => (
319               isa      => InstanceOf ['Hash::Ordered'],
320               default  => sub { Hash::Ordered->new },
321               init_arg => '_cache',
322           );
323
324       With the above, you can pass "_cache => $my_testing_cache" in the
325       constructor.
326
327       A "field" is automatically lazy if it has a "builder" or "default".
328       This is because there's no guarantee the code will call them, but this
329       makes it very easy for a "field" to rely on a "param" value being
330       present. It's a common problem in Moose that attribute initialization
331       order is alphabetical order and if you define an attribute whose
332       "default" or "builder" relies on another attribute, you have to
333       remember to name them correctly or declare the field as lazy.
334
335       Note that is does mean if you need a "field" to be initialized at
336       construction time, you have to take care to declare that it's not lazy:
337
338           field created => ( isa => PositiveInt, lazy => 0, default => sub {time} );
339
340       In our opinion, this tiny little nit is a fair trade-off for this
341       issue:
342
343           package Person {
344               use Moose;
345
346               has name  => ( is => 'ro', required => 1 );
347               has title => ( is => 'ro', required => 0 );
348               has full_name => (
349                   is      => 'ro',
350                   default => sub {
351                       my $self  = shift;
352                       my $title = $self->title;
353                       my $name  = $self->name;
354                       return defined $title ? "$title $name" : $name;
355                   },
356               );
357           }
358
359           my $person = Person->new( title => 'Doctor', name => 'Who' );
360           say $person->title;
361           say $person->full_name;
362
363       The code looks fine, but it doesn't work.  In the above,
364       "$person->full_name" is always undefined because attributes are
365       processed in alphabetical order, so the "full_name" default code is run
366       before "name" or "title" is set. Oops!  Adding "lazy => 1" to the
367       "full_name" attribute definition is required to make it work.
368
369       Here's the same code for "MooseX::Extended". It works correctly:
370
371           package Person {
372               use MooseX::Extended;
373
374               param 'name';
375               param 'title' => ( required => 0 );
376
377               field full_name => (
378                   default => sub {
379                       my $self  = shift;
380                       my $title = $self->title;
381                       my $name  = $self->name;
382                       return defined $title ? "$title $name" : $name;
383                   },
384               );
385           }
386
387       Note that "param" is not lazy by default, but you can add "lazy => 1"
388       if you need to.
389
390       NOTE: We were sorely tempted to change attribute field definition order
391       from alphabetical to declaration order, as that would also solve the
392       above issue (and might allow for deterministic destruction), but we
393       decided to play it safe.
394
395   Attribute shortcuts
396       When using "field" or "param" (but not "has"), we have some attribute
397       shortcuts:
398
399           param name => (
400               isa       => NonEmptyStr,
401               writer    => 1,   # set_name
402               reader    => 1,   # get_name
403               predicate => 1,   # has_name
404               clearer   => 1,   # clear_name
405               builder   => 1,   # _build_name
406           );
407
408           sub _build_name ($self) {
409               ...
410           }
411
412       These should be self-explanatory, but see
413       MooseX::Extended::Manual::Shortcuts for a full explanation.
414

EXCLUDING FEATURES

416       You may find some features to be annoying, or even cause potential bugs
417       (e.g., if you have a "croak" method, our importing of "Carp::croak"
418       will be a problem.
419
420       For example, if you wish to eliminate MooseX::StrictConstructor and the
421       "carp" and "croak" behavior:
422
423           use MooseX::Extended excludes => [qw/StrictConstructor carp/];
424
425       You can exclude the following:
426
427       •   "StrictConstructor"
428
429               use MooseX::Extended::Role excludes => ['StrictConstructor'];
430
431           Excluding this will no longer import "MooseX::StrictConstructor".
432
433       •   "autoclean"
434
435               use MooseX::Extended::Role excludes => ['autoclean'];
436
437           Excluding this will no longer import "namespace::autoclean".
438
439       •   "c3"
440
441               use MooseX::Extended::Role excludes => ['c3'];
442
443           Excluding this will no longer apply the C3 mro.
444
445       •   "carp"
446
447               use MooseX::Extended::Role excludes => ['carp'];
448
449           Excluding this will no longer import "Carp::croak" and
450           "Carp::carp".
451
452       •   "immutable"
453
454               use MooseX::Extended::Role excludes => ['immutable'];
455
456           Excluding this will no longer make your class immutable.
457
458       •   "true"
459
460               use MooseX::Extended::Role excludes => ['true'];
461
462           Excluding this will require your module to end in a true value.
463
464       •   "param"
465
466               use MooseX::Extended::Role excludes => ['param'];
467
468           Excluding this will make the "param" function unavailable.
469
470       •   "field"
471
472               use MooseX::Extended::Role excludes => ['field'];
473
474           Excluding this will make the "field" function unavailable.
475

TYPES

477       We bundle MooseX::Extended::Types to make it easier to have compile-
478       time type checks, along with type checks in functions. Here's a silly
479       example:
480
481           package Not::Corinna {
482               use MooseX::Extended types => [qw(compile Num NonEmptyStr ArrayRef)];
483               use List::Util ();
484
485               # these default to 'ro' (but you can override that) and are required
486               param _name => ( isa => NonEmptyStr, init_arg => 'name' );
487               param title => ( isa => NonEmptyStr, required => 0 );
488
489               # fields must never be passed to the constructor
490               # note that ->title and ->name are guaranteed to be set before
491               # this because fields are lazy by default
492               field name => (
493                   isa     => NonEmptyStr,
494                   default => sub ($self) {
495                       my $title = $self->title;
496                       my $name  = $self->_name;
497                       return $title ? "$title $name" : $name;
498                   },
499               );
500
501               sub add ( $self, $args ) {
502                   state $check = compile( ArrayRef [ Num, 1 ] );
503                   ($args) = $check->($args);
504                   return List;:Util::sum( $args->@* );
505               }
506           }
507
508       See MooseX::Extended::Types for more information.
509

ASSEMBLING YOUR OWN MOOSE

511       After you get used to "MooseX::Extended", you might get tired of
512       exchanging the old boilerplate for new boilerplate. So don't do that.
513       Instead, create your own.
514
515       Define your own version of MooseX::Extended:
516
517           package My::Moose::Role {
518               use MooseX::Extended::Role::Custom;
519
520               sub import {
521                   my ( $class, %args ) = @_;
522                   MooseX::Extended::Role::Custom->create(
523                       excludes => [qw/ carp /],
524                       includes => ['multi'],
525                       %args    # you need this to allow customization of your customization
526                   );
527               }
528           }
529
530           # no need for a true value
531
532       And then use it:
533
534           package Some::Class::Role {
535               use My::Moose::Role types => [qw/ArrayRef Num/];
536
537               param numbers => ( isa => ArrayRef[Num] );
538
539               multi sub foo ($self)       { ... }
540               multi sub foo ($self, $bar) { ... }
541           }
542
543       See MooseX::Extended::Custom for more information.
544

ROLES

546       Of course we support roles. Here's a simple role to add a "created"
547       field to your class:
548
549           package Not::Corinna::Role::Created {
550               use MooseX::Extended::Role types => ['PositiveInt'];
551
552               # mark it as non-lazy to ensure it's run at construction time
553               field created => ( isa => PositiveInt, lazy => 0, default => sub {time} );
554           }
555
556       And then consume like you would any other role:
557
558           package My::Class {
559               use MooseX::Extended types => [qw(compile Num NonEmptyStr Str PositiveInt ArrayRef)];
560
561               with qw(Not::Corinna::Role::Created);
562
563               ...
564           }
565
566       See MooseX::Extended::Role for information about what features it
567       provides and how to adjust its behavior.
568

MIGRATING FROM MOOSE

570       For a clean Moose hierarchy, switching to MooseX::Extended is often as
571       simple at replacing Moose with MooseX::Extended and running your tests.
572       Then you can start deleting various bits of boilerplate in your code
573       (such as the "make_immutable" call).
574
575       Unfortunately, many Moose hierarchies are not clean. You might fail on
576       the "StrictConstructor", or find that you use multiple inheritance and
577       rely on dfs (depth-first search) instead of the C3 mro, or maybe
578       (horrors!), you have classes that aren't declared as immutable and you
579       have code that relies on this. A brute-force approach to handling this
580       could be the following:
581
582           package My::Moose {
583               use MooseX::Extended::Custom;
584
585               sub import {
586                   my ( $class, %args ) = @_;
587                   MooseX::Extended::Custom->create(
588                       excludes => [qw/
589                           StrictConstructor autoclean
590                           c3                carp
591                           immutable         true
592                           field             param
593                       /],
594                       %args    # you need this to pass your own import list
595                   );
596               }
597           }
598           # no need for a true value
599
600       With the above, you've excluded almost everything except signatures and
601       postderef features (we will work on getting around that limitation).
602       From there, you can replace Moose with "My::Moose" (and do something
603       similar with roles) and it should just work. Then, start slowing
604       deleting the items from "excludes" until your tests fail and address
605       them one-by-one.
606

MOOSE INTEROPERABILITY

608       Moose and "MooseX::Extended" should be 100% interoperable. Let us know
609       if it's not <https://github.com/Ovid/moosex-extended/issues>.
610

VERSION COMPATIBILITY

612       We use GitHub Actions <https://github.com/features/actions> to run full
613       continuous integration tests on versions of Perl from v.5.20.0 and up.
614       We do not release any code that fails any of those tests.
615

AUTHOR

617       Curtis "Ovid" Poe <curtis.poe@gmail.com>
618
620       This software is Copyright (c) 2022 by Curtis "Ovid" Poe.
621
622       This is free software, licensed under:
623
624         The Artistic License 2.0 (GPL Compatible)
625
626
627
628perl v5.36.1                      2023-06-M2o6oseX::Extended::Manual::Tutorial(3)
Impressum