NAME

MooseX::Extended::Manual::Overview - Work-in-progress overview for MooseX::Extended

VERSION

version 0.35

CONVERTING FROM MOOSE

For most sane codebases, converting from Moose to MooseX::Extended is as simple as s/Moose/MooseX::Extended/. We try to be backwards-compatible, but some issues are glaring enough that they can't be fixed. Run your tests, folks.

From there, convert your various has attributes to field or param (and run your tests), and you can also delete the annoying __PACKAGE__->meta->make_immutable at the end of every package (run your tests, folks).

From there, if you're not using signatures, convert your methods to using signatures. You'll almost definitely get test failures there as many functions handle arguments poorly.

For roles, just s/Moose::Role/MooseX::Extended::Roles/ and repeat the process (and run your tests, folks).

Along the way, you can probably delete your references to namespace::autoclean and friends because we provide that for you.

RATIONALE

You can skip this section if you like.

MooseX::Extended is built on years of experience hacking on Moose and being the lead designer of the Corinna project to bring modern OO to the Perl language. We love Moose, but over the years, it's become clear that there are some problematic design choices. Plus, Corinna is not yet in core as We write this (though the Perl Steering Committee has accepted it), so for now, let's see how far we can push the envelope.

BOILERPLATE

This:

package My::Class {
    use MooseX::Extended;

    ... your code here
}

Is sort of the equivalent to:

package My::Class {
    use v5.20.0;
    use Moose;
    use MooseX::StrictConstructor;
    use feature 'signatures';
    no warnings 'experimental::signatures';
    use namespace::autoclean;
    use Carp;
    use mro 'c3';

    ... your code here

    __PACKAGE__->meta->make_immutable;
}

1;

We get tired of typing a lot of boilerplate, so MooseX::Extended does away with it.

CONSTRUCTOR

The constructor behavior for Moose could use some love.

What's allowed in the constructor?

We've regularly face the following problem:

package Some::Class;

use Moose;

has name     => (...);
has uuid     => (...);
has id       => (...);
has backlog  => (...);
has auth     => (...);
has username => (...);
has password => (...);
has cache    => (...);
has this     => (...);
has that     => (...);

Which of those should be passed to the constructor and which should not? Just because you can pass something to the constructor doesn't mean you should. Unfortunately, Moose defaults to "opt-out" rather than "opt-in" for constructor arguments. This makes it really easy to build objects, but means that you can pass things to the constructor and it won't always work the way you want it to.

There's an arcane init_arg => undef pair to pass to each to say "this cannot be set via the constructor," but many developers are either unaware of this is simply forget about it. MooseX::Extended solves with by separating has into param (allowed in the constructor, but you can also use default or builder) and field, which is forbidden in the constructor. We can rewrite the above as this:

package Some::Class;

use MooseX::Extended;

param name     => (...);
param backlog  => (...);
param auth     => (...);
param username => (...);
param password => (...);

field cache    => (...);
field this     => (...);
field that     => (...);
field uuid     => (...);
field id       => (...);

And now you can instantly see what is and is not intended to be allowed in the constructor.

Note that in our experience, field attributes often depend on param attributes, so they're lazy by default (a nice performance win if you don't call them), but you can still pass lazy => 0 to override this.

Unknown arguments to the constructor

Here's another fun bug:

my $object = Some::Class->new(
    name   => $name,
    seriel => $serial,
);

# later in your code

if ( $object->serial ) {
    # unreachable code
}

This is because Moose, by default, ignores any unknown arguments to the constructor and in the above example, we misspelled "serial" as "seriel".

MooseX::Extended applies MooseX::StrictConstructor to your class so you never have to face this problem again.

WARNING: Be careful when using this in existing class hierarchies. While MooseX::Extended is compatible with Moose (Moose classes can always use MooseX::Extended classes and roles), the reverse isn't always true. We've found, for example, that trying to use MooseX::StrictConstructor with DBIx::Class does not work.

Method Resolution Order (mro)

If you don't use multiple inheritance, you won't need to worry about this. However, by now, it's generally agreed that the C3 method resolution order (breadth-first, for Perl) is superior to the depth-first default. You can read about the diamond inheritance problem if you'd like to learn more.

Rather than remembering to include use mro 'c3' in your code, MooseX::Extended does it for you.

If you have existing code that breaks under this, you should investigate carefully. You probably have a bug in your code.

Making Your Class Immutable

It's recommended that you end your Moose classes with this:

__PACKAGE__->meta->make_immutable;

That causes a lot of things to happen under the hood. It makes your class much harder to debug as you're in a twisty maze of evaled methods, but it makes your class run much faster. The performance gain is often significant and you almost always want to use this, but it's easy to forget, so we add it for you.

To do this, we use the after_runtime function from B::Hooks::AtRuntime. However, that doesn't work under the debugger. So if you're running under the debugger, we disable this. Please keep that in mind. As a convenience, when running under the debugger, we issue a series of warnings for every class that is impacted. For a large codebase, that could be a considerable number of classes if they use MooseX::Extended:

We are running under the debugger. My::Name is not immutable
We are running under the debugger. My::Product is not immutable
We are running under the debugger. My::Order is not immutable

IMMUTABLE OBJECTS

The subject of immutable objects has been done to death. If We set the value of an attribute but another section of the code has already fetched that value, you might have two sections of the code operating under completely different assumptions of what they're allowed to do. So by default, all attributes are "read-only":

param name  => ( isa => NonEmptyStr );
field cache => ( isa => HashRef );

You can change this if you need to:

param name  => ( is => 'rw', isa => NonEmptyStr );
field cache => ( is => 'rw', isa => HashRef );

However, what's going on with that cache attribute? It returns a reference. If your code mutates that reference, every bit of code holding a reference to that object silently has its state changed. So we fixed that, too:

field cache => ( isa => HashRef, clone => 1 );

Now, every time you get or set that data, it's cloned, ensuring that you can do this:

# assumes that the original ->cache has a SeKreT key.
my $hash1 = $object->cache;
delete $hash1->{SeKreT};
my $hash2 = $object->cache;
my $SeKreT = $hash2->{SeKreT}; # you get the original value

Internally, we use Storable's dclone method for this. Be aware that many things cannot be safely cloned (e.g., database handles).

The clone => 1 feature is safest when you don't have objects that you're trying to clone. As a result, this feature is EXPERIMENTAL.

ATTRIBUTES

By now, you know that is => 'ro' is the default for all param and field attributes. You can still use has, but you will still need to use is => ... with that:

has name => ( is => 'ro, ... );

However, I hate typing out something like predicate => 'is_assigned'. Or should it be predicate => 'has_assigned'? For a variety of attributes, we've made this simpler.

When using field or param, we have some attribute shortcuts:

param name => (
    isa       => NonEmptyStr,
    writer    => 1,   # set_name
    reader    => 1,   # get_name
    predicate => 1,   # has_name
    clearer   => 1,   # clear_name
    builder   => 1,   # _build_name
);

sub _build_name ($self) {
    ...
}

These can also be used when you pass an array reference to the function:

package Point {
    use MooseX::Extended types => 'Int';

    param [ 'x', 'y' ] => (
        isa     => Int,
        clearer => 1,     # clear_x and clear_y available
        default => 0,
    ) :;
}

Note that these are shortcuts and they make attributes easier to write and more consistent. However, you can still use full names:

field authz_delegate => (
    builder => '_build_my_darned_authz_delegate',
);

writer

If an attribute has writer is set to 1 (the number one), a method named set_$attribute_name is created.

This:

param title => (
    isa       => Undef | NonEmptyStr,
    default   => undef,
    writer => 1,
);

Is the same as this:

has title => (
    is      => 'rw',                  # we change this from 'ro'
    isa     => Undef | NonEmptyStr,
    default => undef,
    writer  => 'set_title',
);

reader

By default, the reader (accessor) for the attribute is the same as the name. You can always change this:

has payload => ( is => 'ro', reader => 'the_payload' );

However, if you want to change the reader name

If an attribute has reader is set to 1 (the number one), a method named get_$attribute_name is created.

This:

param title => (
    isa       => Undef | NonEmptyStr,
    default   => undef,
    reader => 1,
);

Is the same as this:

has title => (
    is      => 'rw',                  # we change this from 'ro'
    isa     => Undef | NonEmptyStr,
    default => undef,
    reader  => 'get_title',
);

predicate

If an attribute has predicate is set to 1 (the number one), a method named has_$attribute_name is created.

This:

param title => (
    isa       => Undef | NonEmptyStr,
    default   => undef,
    predicate => 1,
);

Is the same as this:

has title => (
    is        => 'ro',
    isa       => Undef | NonEmptyStr,
    default   => undef,
    predicate => 'has_title',
);

clearer

If an attribute has clearer is set to 1 (the number one), a method named clear_$attribute_name is created.

This:

param title => (
    isa     => Undef | NonEmptyStr,
    default => undef,
    clearer => 1,
);

Is the same as this:

has title => (
    is      => 'ro',
    isa     => Undef | NonEmptyStr,
    default => undef,
    clearer => 'clear_title',
);

builder

If an attribute has builder is set to 1 (the number one), a method named _build_$attribute_name.

This:

param title => (
    isa     =>  NonEmptyStr,
    builder => 1,
);

Is the same as this:

has title => (
    is      => 'ro',
    isa     => NonEmptyStr,
    builder => '_build_title',
);

Obviously, a "private" attribute, such as _auth_token would get a build named _build__auth_token (note the two underscores between "build" and "auth_token").

AUTHOR

Curtis "Ovid" Poe <curtis.poe@gmail.com>

COPYRIGHT AND LICENSE

This software is Copyright (c) 2022 by Curtis "Ovid" Poe.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)