Commit | Line | Data |
daa0fd7d |
1 | package Moose::Cookbook::Basics::Recipe2; |
471c4f09 |
2 | |
daa0fd7d |
3 | # ABSTRACT: A simple B<BankAccount> example |
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 |
79 | self-explanatory, as they are just plain old Perl 5 OO. |
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 |
93 | program (2). |
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 |
100 | it will just DWIM (3). |
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 |
123 | account (4). |
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 | |
182 | In reality, this creation is sensitive to the order in which modules |
183 | are loaded. In more complicated cases, you may find that you need to |
184 | explicitly declare a class type before the corresponding is loaded. |
185 | |
186 | =item (3) |
187 | |
188 | Moose does not attempt to encode a class's is-a relationships within |
189 | the type constraint hierarchy. Instead, Moose just considers the class |
190 | type constraint to be a subtype of C<Object>, and specializes the |
191 | constraint check to allow for subclasses. This means that an instance |
192 | of B<CheckingAccount> will pass a C<BankAccount> type constraint |
193 | successfully. For more details, please refer to the |
194 | L<Moose::Util::TypeConstraints> documentation. |
195 | |
196 | =item (4) |
197 | |
198 | If the overdraft account does not have the amount needed, it will |
199 | throw an error. Of course, the overdraft account could also have |
200 | overdraft protection. See note 1. |
703d9522 |
201 | |
202 | =back |
203 | |
204 | =head1 SEE ALSO |
205 | |
206 | =over 4 |
207 | |
4711f5f7 |
208 | =item Acknowledgment |
703d9522 |
209 | |
9cf569f3 |
210 | The BankAccount example in this recipe is directly taken from the |
c6182301 |
211 | examples in this chapter of "Practical Common Lisp": |
703d9522 |
212 | |
213 | L<http://www.gigamonkeys.com/book/object-reorientation-generic-functions.html> |
214 | |
215 | =back |
216 | |
c79239a2 |
217 | =begin testing |
218 | |
219 | my $savings_account; |
220 | |
221 | { |
222 | $savings_account = BankAccount->new( balance => 250 ); |
223 | isa_ok( $savings_account, 'BankAccount' ); |
224 | |
225 | is( $savings_account->balance, 250, '... got the right savings balance' ); |
b10dde3a |
226 | is( |
227 | exception { |
228 | $savings_account->withdraw(50); |
229 | }, |
230 | undef, |
231 | '... withdrew from savings successfully' |
232 | ); |
c79239a2 |
233 | is( $savings_account->balance, 200, |
234 | '... got the right savings balance after withdrawl' ); |
235 | |
236 | $savings_account->deposit(150); |
237 | is( $savings_account->balance, 350, |
238 | '... got the right savings balance after deposit' ); |
239 | } |
240 | |
241 | { |
242 | my $checking_account = CheckingAccount->new( |
243 | balance => 100, |
244 | overdraft_account => $savings_account |
245 | ); |
246 | isa_ok( $checking_account, 'CheckingAccount' ); |
247 | isa_ok( $checking_account, 'BankAccount' ); |
248 | |
249 | is( $checking_account->overdraft_account, $savings_account, |
250 | '... got the right overdraft account' ); |
251 | |
252 | is( $checking_account->balance, 100, |
253 | '... got the right checkings balance' ); |
254 | |
b10dde3a |
255 | is( |
256 | exception { |
257 | $checking_account->withdraw(50); |
258 | }, |
259 | undef, |
260 | '... withdrew from checking successfully' |
261 | ); |
c79239a2 |
262 | is( $checking_account->balance, 50, |
263 | '... got the right checkings balance after withdrawl' ); |
264 | is( $savings_account->balance, 350, |
265 | '... got the right savings balance after checking withdrawl (no overdraft)' |
266 | ); |
267 | |
b10dde3a |
268 | is( |
269 | exception { |
270 | $checking_account->withdraw(200); |
271 | }, |
272 | undef, |
273 | '... withdrew from checking successfully' |
274 | ); |
c79239a2 |
275 | is( $checking_account->balance, 0, |
276 | '... got the right checkings balance after withdrawl' ); |
277 | is( $savings_account->balance, 200, |
278 | '... got the right savings balance after overdraft withdrawl' ); |
279 | } |
280 | |
281 | { |
282 | my $checking_account = CheckingAccount->new( |
283 | balance => 100 |
284 | |
285 | # no overdraft account |
286 | ); |
287 | isa_ok( $checking_account, 'CheckingAccount' ); |
288 | isa_ok( $checking_account, 'BankAccount' ); |
289 | |
290 | is( $checking_account->overdraft_account, undef, |
291 | '... no overdraft account' ); |
292 | |
293 | is( $checking_account->balance, 100, |
294 | '... got the right checkings balance' ); |
295 | |
b10dde3a |
296 | is( |
297 | exception { |
298 | $checking_account->withdraw(50); |
299 | }, |
300 | undef, |
301 | '... withdrew from checking successfully' |
302 | ); |
c79239a2 |
303 | is( $checking_account->balance, 50, |
304 | '... got the right checkings balance after withdrawl' ); |
305 | |
b10dde3a |
306 | isnt( |
307 | exception { |
308 | $checking_account->withdraw(200); |
309 | }, |
310 | undef, |
311 | '... withdrawal failed due to attempted overdraft' |
312 | ); |
c79239a2 |
313 | is( $checking_account->balance, 50, |
314 | '... got the right checkings balance after withdrawl failure' ); |
315 | } |
316 | |
317 | =end testing |
318 | |
e08c54f5 |
319 | =cut |