File: Recipe22.pod

package info (click to toggle)
libmoose-perl 0.54-1
  • links: PTS, VCS
  • area: main
  • in suites: lenny
  • size: 1,496 kB
  • ctags: 448
  • sloc: perl: 15,125; makefile: 10
file content (207 lines) | stat: -rw-r--r-- 7,071 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207

=pod

=head1 NAME

Moose::Cookbook::Recipe22 - The attribute trait example

=head1 SYNOPSIS

    package MyApp::Meta::Attribute::Trait::Labeled;
    use Moose::Role;

    has label => (
        is        => 'rw',
        isa       => 'Str',
        predicate => 'has_label',
    );

    package Moose::Meta::Attribute::Custom::Trait::Labeled;
    sub register_implementation { 'MyApp::Meta::Attribute::Trait::Labeled' }

    package MyApp::Website;
    use Moose;
    use MyApp::Meta::Attribute::Trait::Labeled;

    has url => (
        traits => [qw/Labeled/],
        is     => 'rw',
        isa    => 'Str',
        label  => "The site's URL",
    );

    has name => (
        is  => 'rw',
        isa => 'Str',
    );

    sub dump {
        my $self = shift;

        # iterate over all the attributes in $self
        my %attributes = %{ $self->meta->get_attribute_map };
        while (my ($name, $attribute) = each %attributes) {

            # print the label if available
            if ($attribute->does('MyApp::Meta::Attribute::Trait::Labeled')
                && $attribute->has_label) {
                    print $attribute->label;
            }
            # otherwise print the name
            else {
                print $name;
            }

            # print the attribute's value
            my $reader = $attribute->get_read_method;
            print ": " . $self->$reader . "\n";
        }
    }

    package main;
    my $app = MyApp::Website->new(url => "http://google.com", name => "Google");
    $app->dump;

=head1 BUT FIRST

This recipe is a continuation of L<Moose::Cookbook::Recipe21>. Please read that
first.

=head1 MOTIVATION

In Recipe 21, we created an attribute metaclass that gives attributes a "label"
that can be set in L<Moose/has>. That works well until you want a second
meta-attribute, or until you want to adjust the behavior of the attribute. You
could define a specialized attribute metaclass to use in every attribute.
However, you may want different attributes to have different behaviors. You
might end up with a unique attribute metaclass for B<every single attribute>,
with a lot of code copying and pasting!

Or, if you've been drinking deeply of the Moose kool-aid, you'll have a role
for each of the behaviors. One role would give a label meta-attribute. Another
role would signify that this attribute is not directly modifiable via the
REST interface. Another role would write to a logfile when this attribute
was read.

Unfortunately, you'd still be left with a bunch of attribute metaclasses that
do nothing but compose a bunch of roles. If only there were some way to specify
in L<Moose/has> a list of roles to apply to the attribute metaclass...

=head1 TRAITS

Roles that apply to metaclasses have a special name: traits. Don't let the
change in nomenclature fool you, B<traits are just roles>.

L<Moose/has> provides a C<traits> option. It takes a list of trait names to
compose into an anonymous metaclass. That means you do still have a bunch of
attribute metaclasses that do nothing but compose a bunch of roles, but they're
managed automatically by Moose. You don't need to declare them in advance, or
worry whether changing one will affect some other attribute.

What can traits do? Anything roles can do. They can add or refine attributes,
wrap methods, provide more methods, define an interface, etc. The only
difference is that you're now changing the attribute metaclass instead of a
user-level class.

=head1 DISSECTION

A side-by-side look of the code examples in this recipe and recipe 21 should
indicate that defining and using a trait is very similar to defining and using
a new attribute metaclass.

    package MyApp::Meta::Attribute::Trait::Labeled;
    use Moose::Role;

    has label => (
        is        => 'rw',
        isa       => 'Str',
        predicate => 'has_label',
    );

Instead of subclassing L<Moose::Meta::Attribute>, we define a role. Traits
don't need any special methods or attributes. You just focus on whatever it is
you actually need to get done. Here we're adding a new meta-attribute for use
in our application.

    package Moose::Meta::Attribute::Custom::Trait::Labeled;
    sub register_implementation { 'MyApp::Meta::Attribute::Trait::Labeled' }

Much like when we define a new attribute metaclass, we can provide a shorthand
name for the trait. Moose looks at the C<register_implementation> method in
C<Moose::Meta::Attribute::Custom::Trait::$TRAIT_NAME> to find the full
name of the trait.

Now we begin writing our application logic. I'll only cover what has changed
since recipe 21.

    has url => (
        traits => [qw/Labeled/],
        is     => 'rw',
        isa    => 'Str',
        label  => "The site's URL",
    );

L<Moose/has> provides a C<traits> option. Just pass the list of trait names and
it will compose them together to form the (anonymous) attribute metaclass used
by the attribute. We provide a label for the attribute in the same way.

    # print the label if available
    if ($attribute->does('MyApp::Meta::Attribute::Trait::Labeled')
        && $attribute->has_label) {
            print $attribute->label;
    }

Previously, this code asked the question "Does this attribute use our attribute
metaclass?" Since we're now using a trait, we ask "Does this attribute's
metaclass do the C<Labeled> role?" If not, the attribute metaclass won't have
the C<has_label> method, and so it would be an error to blindly call
C<< $attribute->has_label >>.

That's all. Everything else is the same!

=head1 METACLASS + TRAIT

"But wait!" you protest. "I've already written all of my extensions as
attribute metaclasses. I don't want to break all that code out there."

All is not lost. If you rewrite your extension as a trait, then you can
easily get a regular metaclass extension out of it. You just compose the trait
in the attribute metaclass, as normal.

    package MyApp::Meta::Attribute::Labeled;
    use Moose;
    extends 'Moose::Meta::Attribute';
    with 'MyApp::Meta::Attribute::Trait::Labeled';

    package Moose::Meta::Attribute::Custom::Labeled;
    sub register_implementation { 'MyApp::Meta::Attribute::Labeled' }

Unfortunately, going the other way (providing a trait created from a metaclass)
is more tricky. Thus, defining your extensions as traits is just plain better
than defining them as subclassed metaclasses.

=head1 CONCLUSION

If you're extending your attributes, it's easier and more flexible to provide
composable bits of behavior than to subclass L<Moose::Meta::Attribute>.
Using traits (which are just roles applied to a metaclass!) let you choose
exactly which behaviors each attribute will have. Moose makes it easy to create
attribute metaclasses on the fly by providing a list of trait names to
L<Moose/has>.

=head1 AUTHOR

Shawn M Moore E<lt>sartak@gmail.comE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright 2006-2008 by Infinity Interactive, Inc.

L<http://www.iinteractive.com>

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut