File Coverage

blib/lib/Role/Subsystem.pm
Criterion Covered Total %
statement 12 15 80.0
branch 2 2 100.0
condition 1 4 25.0
subroutine 5 6 83.3
pod n/a
total 20 27 74.0


line stmt bran cond sub pod time code
1             package Role::Subsystem 0.101342;
2 2     2   2010 use MooseX::Role::Parameterized;
  2         155601  
  2         8  
3             # ABSTRACT: a parameterized role for object subsystems, helpers, and delegates
4              
5             #pod =head1 DESCRIPTION
6             #pod
7             #pod Role::Subsystem is a L<parameterized role|MooseX::Role::Parameterized>. It's
8             #pod meant to simplify creating classes that encapsulate specific parts of the
9             #pod business logic related to parent classes. As in the L<synopsis|/What?>
10             #pod below, it can be used to write "helpers." The subsystems it creates must have
11             #pod a reference to a parent object, which might be referenced by id or with an
12             #pod actual object reference. Role::Subsystem tries to guarantee that no matter
13             #pod which kind of reference you have, the other kind can be obtained and stored for
14             #pod use.
15             #pod
16             #pod =head2 What??
17             #pod
18             #pod Okay, imagine you have a big class called Account. An Account is the central
19             #pod point for a lot of behavior, and rather than dump all that logic in one place,
20             #pod you partition it into subsytems. Let's say we want to write a subsystem that
21             #pod handles all of an Account's Services. We might write this:
22             #pod
23             #pod package Account::ServiceManager;
24             #pod use Moose;
25             #pod use Account;
26             #pod
27             #pod with 'Role::Subsystem' => {
28             #pod ident => 'acct-service-mgr',
29             #pod type => 'Account',
30             #pod what => 'account',
31             #pod getter => sub { Account->retrieve_by_id( $_[0] ) },
32             #pod };
33             #pod
34             #pod sub add_service {
35             #pod my ($self, @args) = @_;
36             #pod
37             #pod # ... do some preliminary business logic
38             #pod
39             #pod $self->account->insert_related_rows(...);
40             #pod
41             #pod # ... do some cleanup business logic
42             #pod }
43             #pod
44             #pod Then you might add to F<Account.pm>:
45             #pod
46             #pod package Account;
47             #pod sub service_mgr {
48             #pod my ($self) = @_;
49             #pod return Account::ServiceManager->for_account($self);
50             #pod }
51             #pod
52             #pod Then, to add a service you can write:
53             #pod
54             #pod $account->service_mgr->add_service(...);
55             #pod
56             #pod You could also just grab the service manager object and use it as a handle for
57             #pod performing operations.
58             #pod
59             #pod If you don't have an Account object, just a reference to its id, you could get
60             #pod the service manager like this:
61             #pod
62             #pod my $service_mgr = Account::ServiceManager->for_account_id( $account_id );
63             #pod
64             #pod =head2 Why?
65             #pod
66             #pod Here's an overview of everything this role will do for you, in terms of the
67             #pod Account::ServiceManager example above.
68             #pod
69             #pod It will create the C<for_account> and C<for_account_id> constructors on your
70             #pod subsystem. (The C<for_account_id> constructor will only be created if a
71             #pod C<getter> is supplied.)
72             #pod
73             #pod It will defer retrieval of C<account> objects if you construct with only a
74             #pod C<account_id>, so that if you never need the full object, you never waste time
75             #pod getting it.
76             #pod
77             #pod It will ensure that any C<account> and C<account_id> encountered match the
78             #pod C<type> and C<id_type> types, respectively. This will prevent a bogus
79             #pod identifier from being accepted, only to die later when it can't be used for
80             #pod lazy retrieval.
81             #pod
82             #pod If you create a subsystem object by passing in the parent object (the
83             #pod C<account>), it will take a weak reference to it to prevent cyclical references
84             #pod from interfering with garbage collection. If the reference goes away, or if
85             #pod you did not start with a reference, a strong reference will be constructed to
86             #pod allow the subsystem to function efficiently afterward. (This behavior can be
87             #pod disabled, if you never want to take a weak reference.)
88             #pod
89             #pod =head3 Swappable Subsystem Implementations
90             #pod
91             #pod You can also have multiple implementations of a single kind of subsystem. For
92             #pod example, you may eventually want to do something like this:
93             #pod
94             #pod package Account::ServiceManager;
95             #pod use Moose::Role;
96             #pod
97             #pod with 'Role::Subsystem' => { ... };
98             #pod
99             #pod requries 'add_service';
100             #pod requries 'remove_service';
101             #pod requries 'service_summary';
102             #pod
103             #pod ...and then...
104             #pod
105             #pod package Account::ServiceManager::Legacy;
106             #pod with 'Account::ServiceManager';
107             #pod
108             #pod sub add_service { ... };
109             #pod
110             #pod ...and...
111             #pod
112             #pod package Account::ServiceManager::Simple;
113             #pod with 'Account::ServiceManager';
114             #pod
115             #pod sub add_service { ... };
116             #pod
117             #pod ...and finally...
118             #pod
119             #pod package Account;
120             #pod
121             #pod sub settings_mgr {
122             #pod my ($self) = @_;
123             #pod
124             #pod my $mgr_class = $self->schema_version > 1
125             #pod ? 'Account::ServiceManager::Simple'
126             #pod : 'Account::ServiceManager::Legacy';
127             #pod
128             #pod return $mgr_class->for_account($self);
129             #pod }
130             #pod
131             #pod This requires a bit more work, but lets you replace subsystem implementations
132             #pod as fairly isolated units.
133             #pod
134             #pod =head1 PARAMETERS
135             #pod
136             #pod These parameters can be given when including Role::Subsystem; these are in
137             #pod contrast to the L<attributes|/ATTRIBUTES> and L<methods|/METHODS> below, which
138             #pod are added to the classe composing this role.
139             #pod
140             #pod =head2 ident
141             #pod
142             #pod This is a simple name for the role to use when describing itself in messages.
143             #pod It is required.
144             #pod
145             #pod =cut
146              
147             parameter ident => (isa => 'Str', required => 1);
148              
149             #pod =head2 what
150             #pod
151             #pod This is the name of the attribute that will hold the parent object, like the
152             #pod C<account> in the synopsis above.
153             #pod
154             #pod This attribute is required.
155             #pod
156             #pod =cut
157              
158             parameter what => (
159             isa => 'Str',
160             required => 1,
161             );
162              
163             #pod =head2 what_id
164             #pod
165             #pod This is the name of the attribute that will hold the parent object's
166             #pod identifier, like the C<account_id> in the synopsis above.
167             #pod
168             #pod If not given, it will be the value of C<what> with "_id" stuck on the end.
169             #pod
170             #pod =cut
171              
172             parameter what_id => (
173             isa => 'Str',
174             lazy => 1,
175             default => sub { $_[0]->what . '_id' },
176             );
177              
178             #pod =head2 type
179             #pod
180             #pod This is the type that the C<what> must be. It may be a stringly Moose type or
181             #pod an L<MooseX::Types> type. (Or anything else, right now, but anything else will
182             #pod probably cause runtime failures or worse.)
183             #pod
184             #pod This attribute is required.
185             #pod
186             #pod =cut
187              
188             parameter type => (isa => 'Defined', required => 1);
189              
190             #pod =head2 id_type
191             #pod
192             #pod This parameter is like C<type>, but is used to check the C<what>'s id,
193             #pod discussed more below. If not given, it defaults to C<Defined>.
194             #pod
195             #pod =cut
196              
197             parameter id_type => (isa => 'Defined', default => 'Defined');
198              
199             #pod =head2 id_method
200             #pod
201             #pod This is the name of a method to call on C<what> to get its id. It defaults to
202             #pod C<id>.
203             #pod
204             #pod =cut
205              
206             parameter id_method => (isa => 'Str', default => 'id');
207              
208             #pod =head2 getter
209             #pod
210             #pod This (optional) attribute supplied a callback that will produce the parent
211             #pod object from the C<what_id>.
212             #pod
213             #pod =cut
214              
215             parameter getter => (
216             isa => 'CodeRef',
217             );
218              
219             #pod =head2 weak_ref
220             #pod
221             #pod If true, when a subsytem object is created with a defined parent object (that
222             #pod is, a value for C<what>), the reference to the object will be weakened. This
223             #pod allows the parent and the subsystem to store references to one another without
224             #pod creating a problematic circular reference.
225             #pod
226             #pod If the parent object is subsequently garbage collected, a new value for C<what>
227             #pod will be retreived and stored, and it will B<not> be weakened. To allow this,
228             #pod setting C<weak_ref> to true requires that C<getter> be supplied.
229             #pod
230             #pod C<weak_ref> is true by default.
231             #pod
232             #pod =cut
233              
234             parameter weak_ref => (
235             isa => 'Bool',
236             default => 1,
237             );
238              
239             role {
240             my ($p) = @_;
241              
242             my $what = $p->what;
243             my $ident = $p->ident;
244             my $what_id = $p->what_id;
245             my $getter = $p->getter;
246             my $id_method = $p->id_method;
247             my $weak_ref = $p->weak_ref;
248              
249             my $w_pred = "has_initialized_$what";
250             my $wi_pred = "has_initialized_$what_id";
251             my $w_reader = "_$what";
252             my $w_clearer = "_clear_$what";
253              
254             confess "cannot use weak references for $ident without a getter"
255             if $weak_ref and not $getter;
256              
257             has $what => (
258             is => 'bare',
259             reader => $w_reader,
260             isa => $p->type,
261             lazy => 1,
262             predicate => $w_pred,
263             clearer => $w_clearer,
264             default => sub {
265             # Basically, this should never happen. We should not be generating the
266             # for_what_id method if there is no getter, and we should be blowing up
267             # if produced without a what without a getter. Still, CYA.
268             # -- rjbs, 2010-05-05
269             confess "cannot get a $what based on $what_id; no getter" unless $getter;
270              
271             $getter->( $_[0]->$what_id );
272             },
273             );
274              
275             if ($weak_ref) {
276             method $what => sub {
277 5     5   1386 my ($self) = @_;
278 5         140 my $value = $self->$w_reader;
279 5 100       50 return $value if defined $value;
280 1         25 $self->$w_clearer;
281 1         22 return $self->$w_reader;
282             };
283             } else {
284             my $reader = "_$what";
285 3     3   626 method $what => sub { $_[0]->$reader },
286             }
287              
288             has $what_id => (
289             is => 'ro',
290             isa => $p->id_type,
291             lazy => 1,
292             predicate => $wi_pred,
293             default => sub { $_[0]->$what->$id_method },
294             );
295              
296       2     method BUILD => sub {};
297              
298             after BUILD => sub {
299             my ($self) = @_;
300              
301             # So, now we protect ourselves from pathological cases. These are:
302             # 1. neither $what nor $what_id given
303             unless ($self->$w_pred or $self->$wi_pred) {
304             confess "neither $what nor $what_id given in constructing $ident";
305             }
306              
307             # 2. both $what and $what_id given, but not matching
308             if (
309             $self->$w_pred and $self->$wi_pred
310             and $self->$what->$id_method ne $self->$what_id
311             ) {
312             confess "the result of $what->$id_method is not equal to the $what_id"
313             }
314              
315             # 3. only $what_id given, but no getter
316             if ($self->$wi_pred and ! $self->$w_pred and ! $getter) {
317             confess "can't build $ident with only $what_id; no getter";
318             }
319              
320             if ($weak_ref) {
321             # We get the id immediately, if we have a weak ref, on the assumption
322             # that if the ref expires, we will need the id for the getter
323             # to function. -- rjbs, 2010-05-05
324             $self->$what_id unless $self->$wi_pred;
325              
326             # We only *really* weaken this if we're starting off with an object from
327             # outside, because if we got the object from our getter, nothing else is
328             # likely to be holding a reference to it. -- rjbs, 2010-05-05
329             Scalar::Util::weaken $self->{$what} if $self->$w_pred;
330             }
331             };
332              
333             method "for_$what" => sub {
334 2     2   11150 my ($class, $entity, $arg) = @_;
335 2   50     14 $arg ||= {};
336              
337 2         12 $class->new({
338             %$arg,
339             $what => $entity,
340             });
341             };
342              
343             if ($getter) {
344             method "for_$what_id" => sub {
345 0     0   0 my ($class, $id, $arg) = @_;
346 0   0     0 $arg ||= {};
347              
348 0         0 $class->new({
349             %$arg,
350             $what_id => $id,
351             });
352             };
353             }
354             };
355              
356             #pod =head1 ATTRIBUTES
357             #pod
358             #pod The following attributes are added classes composing Role::Subsystem.
359             #pod
360             #pod =head2 $what
361             #pod
362             #pod This will refer to the parent object of the subsystem. It will be a value of
363             #pod the C<type> type defined when parameterizing Role::Subsystem. It may be lazily
364             #pod computed if it was not supplied during creation or if the initial value was
365             #pod weak and subsequently garbage collected.
366             #pod
367             #pod If the value of C<what> when parameterizing Role::Subsystem was C<account>,
368             #pod that will be the name of this attribute, as well as the method used to read it.
369             #pod
370             #pod =head2 $what_id
371             #pod
372             #pod This method gets the id of the parent object. It will be a defined value of
373             #pod the C<id_type> provided when parameterizing Role::Subsystem. It may be lazily
374             #pod computed by calling the C<id_method> on C<what> as needed.
375             #pod
376             #pod =head1 METHODS
377             #pod
378             #pod =head2 for_$what
379             #pod
380             #pod my $settings_mgr = Account::ServiceManager->for_account($account);
381             #pod
382             #pod This is a convenience constructor, returning a subsystem object for the given
383             #pod C<what>.
384             #pod
385             #pod =head2 for_$what_id
386             #pod
387             #pod my $settings_mgr = Account::ServiceManager->for_account_id($account_id);
388             #pod
389             #pod This is a convenience constructor, returning a subsystem object for the given
390             #pod C<what_id>.
391             #pod
392             #pod =cut
393              
394             __END__
395              
396             =pod
397              
398             =encoding UTF-8
399              
400             =head1 NAME
401              
402             Role::Subsystem - a parameterized role for object subsystems, helpers, and delegates
403              
404             =head1 VERSION
405              
406             version 0.101342
407              
408             =head1 DESCRIPTION
409              
410             Role::Subsystem is a L<parameterized role|MooseX::Role::Parameterized>. It's
411             meant to simplify creating classes that encapsulate specific parts of the
412             business logic related to parent classes. As in the L<synopsis|/What?>
413             below, it can be used to write "helpers." The subsystems it creates must have
414             a reference to a parent object, which might be referenced by id or with an
415             actual object reference. Role::Subsystem tries to guarantee that no matter
416             which kind of reference you have, the other kind can be obtained and stored for
417             use.
418              
419             =head2 What??
420              
421             Okay, imagine you have a big class called Account. An Account is the central
422             point for a lot of behavior, and rather than dump all that logic in one place,
423             you partition it into subsytems. Let's say we want to write a subsystem that
424             handles all of an Account's Services. We might write this:
425              
426             package Account::ServiceManager;
427             use Moose;
428             use Account;
429              
430             with 'Role::Subsystem' => {
431             ident => 'acct-service-mgr',
432             type => 'Account',
433             what => 'account',
434             getter => sub { Account->retrieve_by_id( $_[0] ) },
435             };
436              
437             sub add_service {
438             my ($self, @args) = @_;
439              
440             # ... do some preliminary business logic
441              
442             $self->account->insert_related_rows(...);
443              
444             # ... do some cleanup business logic
445             }
446              
447             Then you might add to F<Account.pm>:
448              
449             package Account;
450             sub service_mgr {
451             my ($self) = @_;
452             return Account::ServiceManager->for_account($self);
453             }
454              
455             Then, to add a service you can write:
456              
457             $account->service_mgr->add_service(...);
458              
459             You could also just grab the service manager object and use it as a handle for
460             performing operations.
461              
462             If you don't have an Account object, just a reference to its id, you could get
463             the service manager like this:
464              
465             my $service_mgr = Account::ServiceManager->for_account_id( $account_id );
466              
467             =head2 Why?
468              
469             Here's an overview of everything this role will do for you, in terms of the
470             Account::ServiceManager example above.
471              
472             It will create the C<for_account> and C<for_account_id> constructors on your
473             subsystem. (The C<for_account_id> constructor will only be created if a
474             C<getter> is supplied.)
475              
476             It will defer retrieval of C<account> objects if you construct with only a
477             C<account_id>, so that if you never need the full object, you never waste time
478             getting it.
479              
480             It will ensure that any C<account> and C<account_id> encountered match the
481             C<type> and C<id_type> types, respectively. This will prevent a bogus
482             identifier from being accepted, only to die later when it can't be used for
483             lazy retrieval.
484              
485             If you create a subsystem object by passing in the parent object (the
486             C<account>), it will take a weak reference to it to prevent cyclical references
487             from interfering with garbage collection. If the reference goes away, or if
488             you did not start with a reference, a strong reference will be constructed to
489             allow the subsystem to function efficiently afterward. (This behavior can be
490             disabled, if you never want to take a weak reference.)
491              
492             =head3 Swappable Subsystem Implementations
493              
494             You can also have multiple implementations of a single kind of subsystem. For
495             example, you may eventually want to do something like this:
496              
497             package Account::ServiceManager;
498             use Moose::Role;
499              
500             with 'Role::Subsystem' => { ... };
501              
502             requries 'add_service';
503             requries 'remove_service';
504             requries 'service_summary';
505              
506             ...and then...
507              
508             package Account::ServiceManager::Legacy;
509             with 'Account::ServiceManager';
510              
511             sub add_service { ... };
512              
513             ...and...
514              
515             package Account::ServiceManager::Simple;
516             with 'Account::ServiceManager';
517              
518             sub add_service { ... };
519              
520             ...and finally...
521              
522             package Account;
523              
524             sub settings_mgr {
525             my ($self) = @_;
526              
527             my $mgr_class = $self->schema_version > 1
528             ? 'Account::ServiceManager::Simple'
529             : 'Account::ServiceManager::Legacy';
530              
531             return $mgr_class->for_account($self);
532             }
533              
534             This requires a bit more work, but lets you replace subsystem implementations
535             as fairly isolated units.
536              
537             =head1 PERL VERSION
538              
539             This library should run on perls released even a long time ago. It should work
540             on any version of perl released in the last five years.
541              
542             Although it may work on older versions of perl, no guarantee is made that the
543             minimum required version will not be increased. The version may be increased
544             for any reason, and there is no promise that patches will be accepted to lower
545             the minimum required perl.
546              
547             =head1 PARAMETERS
548              
549             These parameters can be given when including Role::Subsystem; these are in
550             contrast to the L<attributes|/ATTRIBUTES> and L<methods|/METHODS> below, which
551             are added to the classe composing this role.
552              
553             =head2 ident
554              
555             This is a simple name for the role to use when describing itself in messages.
556             It is required.
557              
558             =head2 what
559              
560             This is the name of the attribute that will hold the parent object, like the
561             C<account> in the synopsis above.
562              
563             This attribute is required.
564              
565             =head2 what_id
566              
567             This is the name of the attribute that will hold the parent object's
568             identifier, like the C<account_id> in the synopsis above.
569              
570             If not given, it will be the value of C<what> with "_id" stuck on the end.
571              
572             =head2 type
573              
574             This is the type that the C<what> must be. It may be a stringly Moose type or
575             an L<MooseX::Types> type. (Or anything else, right now, but anything else will
576             probably cause runtime failures or worse.)
577              
578             This attribute is required.
579              
580             =head2 id_type
581              
582             This parameter is like C<type>, but is used to check the C<what>'s id,
583             discussed more below. If not given, it defaults to C<Defined>.
584              
585             =head2 id_method
586              
587             This is the name of a method to call on C<what> to get its id. It defaults to
588             C<id>.
589              
590             =head2 getter
591              
592             This (optional) attribute supplied a callback that will produce the parent
593             object from the C<what_id>.
594              
595             =head2 weak_ref
596              
597             If true, when a subsytem object is created with a defined parent object (that
598             is, a value for C<what>), the reference to the object will be weakened. This
599             allows the parent and the subsystem to store references to one another without
600             creating a problematic circular reference.
601              
602             If the parent object is subsequently garbage collected, a new value for C<what>
603             will be retreived and stored, and it will B<not> be weakened. To allow this,
604             setting C<weak_ref> to true requires that C<getter> be supplied.
605              
606             C<weak_ref> is true by default.
607              
608             =head1 ATTRIBUTES
609              
610             The following attributes are added classes composing Role::Subsystem.
611              
612             =head2 $what
613              
614             This will refer to the parent object of the subsystem. It will be a value of
615             the C<type> type defined when parameterizing Role::Subsystem. It may be lazily
616             computed if it was not supplied during creation or if the initial value was
617             weak and subsequently garbage collected.
618              
619             If the value of C<what> when parameterizing Role::Subsystem was C<account>,
620             that will be the name of this attribute, as well as the method used to read it.
621              
622             =head2 $what_id
623              
624             This method gets the id of the parent object. It will be a defined value of
625             the C<id_type> provided when parameterizing Role::Subsystem. It may be lazily
626             computed by calling the C<id_method> on C<what> as needed.
627              
628             =head1 METHODS
629              
630             =head2 for_$what
631              
632             my $settings_mgr = Account::ServiceManager->for_account($account);
633              
634             This is a convenience constructor, returning a subsystem object for the given
635             C<what>.
636              
637             =head2 for_$what_id
638              
639             my $settings_mgr = Account::ServiceManager->for_account_id($account_id);
640              
641             This is a convenience constructor, returning a subsystem object for the given
642             C<what_id>.
643              
644             =head1 AUTHOR
645              
646             Ricardo Signes <cpan@semiotic.systems>
647              
648             =head1 CONTRIBUTORS
649              
650             =for stopwords Matthew Horsfall Ricardo Signes
651              
652             =over 4
653              
654             =item *
655              
656             Matthew Horsfall <wolfsage@gmail.com>
657              
658             =item *
659              
660             Ricardo Signes <rjbs@semiotic.systems>
661              
662             =back
663              
664             =head1 COPYRIGHT AND LICENSE
665              
666             This software is copyright (c) 2010 by Ricardo Signes.
667              
668             This is free software; you can redistribute it and/or modify it under
669             the same terms as the Perl 5 programming language system itself.
670              
671             =cut