Commit | Line | Data |
7b0a6766 |
1 | package Moose::Cookbook::Basics::BankAccount_MethodModifiersAndSubclassing; |
471c4f09 |
2 | |
7b0a6766 |
3 | # ABSTRACT: Demonstrates the use of method modifiers in a subclass |
daa0fd7d |
4 | |
5 | __END__ |
471c4f09 |
6 | |
471c4f09 |
7 | |
daa0fd7d |
8 | =pod |
471c4f09 |
9 | |
10 | =head1 SYNOPSIS |
11 | |
12 | package BankAccount; |
471c4f09 |
13 | use Moose; |
8a7ed43e |
14 | |
15 | has 'balance' => ( isa => 'Int', is => 'rw', default => 0 ); |
16 | |
471c4f09 |
17 | sub deposit { |
8a7ed43e |
18 | my ( $self, $amount ) = @_; |
19 | $self->balance( $self->balance + $amount ); |
471c4f09 |
20 | } |
8a7ed43e |
21 | |
471c4f09 |
22 | sub withdraw { |
8a7ed43e |
23 | my ( $self, $amount ) = @_; |
471c4f09 |
24 | my $current_balance = $self->balance(); |
8a7ed43e |
25 | ( $current_balance >= $amount ) |
471c4f09 |
26 | || confess "Account overdrawn"; |
8a7ed43e |
27 | $self->balance( $current_balance - $amount ); |
471c4f09 |
28 | } |
8a7ed43e |
29 | |
471c4f09 |
30 | package CheckingAccount; |
471c4f09 |
31 | use Moose; |
8a7ed43e |
32 | |
471c4f09 |
33 | extends 'BankAccount'; |
8a7ed43e |
34 | |
35 | has 'overdraft_account' => ( isa => 'BankAccount', is => 'rw' ); |
36 | |
471c4f09 |
37 | before 'withdraw' => sub { |
8a7ed43e |
38 | my ( $self, $amount ) = @_; |
cdcae970 |
39 | my $overdraft_amount = $amount - $self->balance(); |
8a7ed43e |
40 | if ( $self->overdraft_account && $overdraft_amount > 0 ) { |
cdcae970 |
41 | $self->overdraft_account->withdraw($overdraft_amount); |
42 | $self->deposit($overdraft_amount); |
43 | } |
471c4f09 |
44 | }; |
45 | |
46 | =head1 DESCRIPTION |
47 | |
9cf569f3 |
48 | The first recipe demonstrated how to build very basic Moose classes, |
49 | focusing on creating and manipulating attributes. The objects in that |
b0825978 |
50 | recipe were very data-oriented, and did not have much in the way of |
9cf569f3 |
51 | behavior (i.e. methods). In this recipe, we expand upon the concepts |
52 | from the first recipe to include some real behavior. In particular, we |
b0825978 |
53 | show how you can use a method modifier to implement new behavior for a |
54 | method. |
9cf569f3 |
55 | |
56 | The classes in the SYNOPSIS show two kinds of bank account. A simple |
57 | bank account has one attribute, the balance, and two behaviors, |
58 | depositing and withdrawing money. |
59 | |
60 | We then extend the basic bank account in the CheckingAccount |
61 | class. This class adds another attribute, an overdraft account. It |
62 | also adds overdraft protection to the withdraw method. If you try to |
63 | withdraw more than you have, the checking account attempts to |
64 | reconcile the difference by withdrawing money from the overdraft |
65 | account. (1) |
66 | |
67 | The first class, B<BankAccount>, introduces a new attribute feature, a |
68 | default value: |
703d9522 |
69 | |
8a7ed43e |
70 | has 'balance' => ( isa => 'Int', is => 'rw', default => 0 ); |
703d9522 |
71 | |
9cf569f3 |
72 | This says that a B<BankAccount> has a C<balance> attribute, which has |
5d1ea977 |
73 | an C<Int> type constraint, a read/write accessor, and a default value |
9cf569f3 |
74 | of C<0>. This means that every instance of B<BankAccount> that is |
75 | created will have its C<balance> slot initialized to C<0>, unless some |
76 | other value is provided to the constructor. |
703d9522 |
77 | |
9cf569f3 |
78 | The C<deposit> and C<withdraw> methods should be fairly |
69d65c16 |
79 | self-explanatory, as they are just plain old Perl 5 OO. (2) |
703d9522 |
80 | |
9cf569f3 |
81 | As you know from the first recipe, the keyword C<extends> sets a |
82 | class's superclass. Here we see that B<CheckingAccount> C<extends> |
83 | B<BankAccount>. The next line introduces yet another new attribute |
84 | feature, class-based type constraints: |
703d9522 |
85 | |
8a7ed43e |
86 | has 'overdraft_account' => ( isa => 'BankAccount', is => 'rw' ); |
703d9522 |
87 | |
9cf569f3 |
88 | Up until now, we have only seen the C<Int> type constraint, which (as |
89 | we saw in the first recipe) is a builtin type constraint. The |
90 | C<BankAccount> type constraint is new, and was actually defined the |
91 | moment we created the B<BankAccount> class itself. In fact, Moose |
92 | creates a corresponding type constraint for every class in your |
69d65c16 |
93 | program (3). |
9cf569f3 |
94 | |
95 | This means that in the first recipe, constraints for both C<Point> and |
96 | C<Point3D> were created. In this recipe, both C<BankAccount> and |
97 | C<CheckingAccount> type constraints are created automatically. Moose |
98 | does this as a convenience so that your classes and type constraint |
99 | can be kept in sync with one another. In short, Moose makes sure that |
69d65c16 |
100 | it will just DWIM (4). |
9cf569f3 |
101 | |
102 | In B<CheckingAccount>, we see another method modifier, the C<before> |
703d9522 |
103 | modifier. |
104 | |
105 | before 'withdraw' => sub { |
8a7ed43e |
106 | my ( $self, $amount ) = @_; |
703d9522 |
107 | my $overdraft_amount = $amount - $self->balance(); |
8a7ed43e |
108 | if ( $self->overdraft_account && $overdraft_amount > 0 ) { |
703d9522 |
109 | $self->overdraft_account->withdraw($overdraft_amount); |
110 | $self->deposit($overdraft_amount); |
111 | } |
112 | }; |
113 | |
9cf569f3 |
114 | Just as with the C<after> modifier from the first recipe, Moose will |
115 | handle calling the superclass method (in this case C<< |
116 | BankAccount->withdraw >>). |
703d9522 |
117 | |
9cf569f3 |
118 | The C<before> modifier will (obviously) run I<before> the code from |
119 | the superclass is run. Here, C<before> modifier implements overdraft |
120 | protection by first checking if there are available funds in the |
121 | checking account. If not (and if there is an overdraft account |
122 | available), it transfers the amount needed into the checking |
69d65c16 |
123 | account (5). |
9cf569f3 |
124 | |
125 | As with the method modifier in the first recipe, we could use |
126 | C<SUPER::> to get the same effect: |
703d9522 |
127 | |
128 | sub withdraw { |
8a7ed43e |
129 | my ( $self, $amount ) = @_; |
703d9522 |
130 | my $overdraft_amount = $amount - $self->balance(); |
8a7ed43e |
131 | if ( $self->overdraft_account && $overdraft_amount > 0 ) { |
703d9522 |
132 | $self->overdraft_account->withdraw($overdraft_amount); |
133 | $self->deposit($overdraft_amount); |
134 | } |
135 | $self->SUPER::withdraw($amount); |
136 | } |
137 | |
9cf569f3 |
138 | The benefit of taking the method modifier approach is we do not need |
139 | to remember to call C<SUPER::withdraw> and pass it the C<$amount> |
140 | argument when writing C<< CheckingAccount->withdraw >>. |
141 | |
142 | This is actually more than just a convenience for forgetful |
143 | programmers. Using method modifiers helps isolate subclasses from |
144 | changes in the superclasses. For instance, if B<< |
145 | BankAccount->withdraw >> were to add an additional argument of some |
146 | kind, the version of B<< CheckingAccount->withdraw >> which uses |
147 | C<SUPER::withdraw> would not pass that extra argument correctly, |
148 | whereas the method modifier version would automatically pass along all |
149 | arguments correctly. |
150 | |
151 | Just as with the first recipe, object instantiation uses the C<new> |
152 | method, which accepts named parameters. |
153 | |
154 | my $savings_account = BankAccount->new( balance => 250 ); |
155 | |
703d9522 |
156 | my $checking_account = CheckingAccount->new( |
9cf569f3 |
157 | balance => 100, |
158 | overdraft_account => $savings_account, |
159 | ); |
703d9522 |
160 | |
9cf569f3 |
161 | And as with the first recipe, a more in-depth example can be found in |
2c739d1a |
162 | the F<t/recipes/moose_cookbook_basics_recipe2.t> test file. |
703d9522 |
163 | |
164 | =head1 CONCLUSION |
165 | |
ec6df2e6 |
166 | This recipe expanded on the basic concepts from the first recipe with |
167 | a more "real world" use case. |
703d9522 |
168 | |
169 | =head1 FOOTNOTES |
170 | |
171 | =over 4 |
172 | |
173 | =item (1) |
174 | |
9cf569f3 |
175 | If you're paying close attention, you might realize that there's a |
176 | circular loop waiting to happen here. A smarter example would have to |
177 | make sure that we don't accidentally create a loop between the |
178 | checking account and its overdraft account. |
179 | |
180 | =item (2) |
181 | |
69d65c16 |
182 | Note that for simple methods like these, which just manipulate some |
183 | single piece of data, it is often not necessary to write them at all. |
184 | For instance, C<deposit> could be implemented via the C<inc> native |
185 | delegation for counters - see |
186 | L<Moose::Meta::Attribute::Native::Trait::Counter> for more specifics, |
187 | and L<Moose::Meta::Attribute::Native> for a broader overview. |
188 | |
189 | =item (3) |
190 | |
9cf569f3 |
191 | In reality, this creation is sensitive to the order in which modules |
192 | are loaded. In more complicated cases, you may find that you need to |
2dba68c4 |
193 | explicitly declare a class type before the corresponding class is |
194 | loaded. |
9cf569f3 |
195 | |
69d65c16 |
196 | =item (4) |
9cf569f3 |
197 | |
198 | Moose does not attempt to encode a class's is-a relationships within |
199 | the type constraint hierarchy. Instead, Moose just considers the class |
200 | type constraint to be a subtype of C<Object>, and specializes the |
201 | constraint check to allow for subclasses. This means that an instance |
202 | of B<CheckingAccount> will pass a C<BankAccount> type constraint |
203 | successfully. For more details, please refer to the |
204 | L<Moose::Util::TypeConstraints> documentation. |
205 | |
69d65c16 |
206 | =item (5) |
9cf569f3 |
207 | |
208 | If the overdraft account does not have the amount needed, it will |
209 | throw an error. Of course, the overdraft account could also have |
210 | overdraft protection. See note 1. |
703d9522 |
211 | |
212 | =back |
213 | |
396dba58 |
214 | =head1 ACKNOWLEDGMENT |
703d9522 |
215 | |
9cf569f3 |
216 | The BankAccount example in this recipe is directly taken from the |
c6182301 |
217 | examples in this chapter of "Practical Common Lisp": |
703d9522 |
218 | |
219 | L<http://www.gigamonkeys.com/book/object-reorientation-generic-functions.html> |
220 | |
c79239a2 |
221 | =begin testing |
222 | |
223 | my $savings_account; |
224 | |
225 | { |
226 | $savings_account = BankAccount->new( balance => 250 ); |
227 | isa_ok( $savings_account, 'BankAccount' ); |
228 | |
229 | is( $savings_account->balance, 250, '... got the right savings balance' ); |
b10dde3a |
230 | is( |
231 | exception { |
232 | $savings_account->withdraw(50); |
233 | }, |
234 | undef, |
235 | '... withdrew from savings successfully' |
236 | ); |
c79239a2 |
237 | is( $savings_account->balance, 200, |
2dba68c4 |
238 | '... got the right savings balance after withdrawal' ); |
c79239a2 |
239 | |
240 | $savings_account->deposit(150); |
241 | is( $savings_account->balance, 350, |
242 | '... got the right savings balance after deposit' ); |
243 | } |
244 | |
245 | { |
246 | my $checking_account = CheckingAccount->new( |
247 | balance => 100, |
248 | overdraft_account => $savings_account |
249 | ); |
250 | isa_ok( $checking_account, 'CheckingAccount' ); |
251 | isa_ok( $checking_account, 'BankAccount' ); |
252 | |
253 | is( $checking_account->overdraft_account, $savings_account, |
254 | '... got the right overdraft account' ); |
255 | |
256 | is( $checking_account->balance, 100, |
257 | '... got the right checkings balance' ); |
258 | |
b10dde3a |
259 | is( |
260 | exception { |
261 | $checking_account->withdraw(50); |
262 | }, |
263 | undef, |
264 | '... withdrew from checking successfully' |
265 | ); |
c79239a2 |
266 | is( $checking_account->balance, 50, |
2dba68c4 |
267 | '... got the right checkings balance after withdrawal' ); |
c79239a2 |
268 | is( $savings_account->balance, 350, |
2dba68c4 |
269 | '... got the right savings balance after checking withdrawal (no overdraft)' |
c79239a2 |
270 | ); |
271 | |
b10dde3a |
272 | is( |
273 | exception { |
274 | $checking_account->withdraw(200); |
275 | }, |
276 | undef, |
277 | '... withdrew from checking successfully' |
278 | ); |
c79239a2 |
279 | is( $checking_account->balance, 0, |
2dba68c4 |
280 | '... got the right checkings balance after withdrawal' ); |
c79239a2 |
281 | is( $savings_account->balance, 200, |
2dba68c4 |
282 | '... got the right savings balance after overdraft withdrawal' ); |
c79239a2 |
283 | } |
284 | |
285 | { |
286 | my $checking_account = CheckingAccount->new( |
287 | balance => 100 |
288 | |
289 | # no overdraft account |
290 | ); |
291 | isa_ok( $checking_account, 'CheckingAccount' ); |
292 | isa_ok( $checking_account, 'BankAccount' ); |
293 | |
294 | is( $checking_account->overdraft_account, undef, |
295 | '... no overdraft account' ); |
296 | |
297 | is( $checking_account->balance, 100, |
298 | '... got the right checkings balance' ); |
299 | |
b10dde3a |
300 | is( |
301 | exception { |
302 | $checking_account->withdraw(50); |
303 | }, |
304 | undef, |
305 | '... withdrew from checking successfully' |
306 | ); |
c79239a2 |
307 | is( $checking_account->balance, 50, |
2dba68c4 |
308 | '... got the right checkings balance after withdrawal' ); |
c79239a2 |
309 | |
b10dde3a |
310 | isnt( |
311 | exception { |
312 | $checking_account->withdraw(200); |
313 | }, |
314 | undef, |
315 | '... withdrawal failed due to attempted overdraft' |
316 | ); |
c79239a2 |
317 | is( $checking_account->balance, 50, |
2dba68c4 |
318 | '... got the right checkings balance after withdrawal failure' ); |
c79239a2 |
319 | } |
320 | |
321 | =end testing |
322 | |
e08c54f5 |
323 | =cut |