Moose::Cookbook::Recipe2 - A simple B<BankAccount> example |
Moose::Cookbook::Recipe2 - A simple BankAccount example
package BankAccount; use Moose; has 'balance' => (isa => 'Int', is => 'rw', default => 0); sub deposit { my ($self, $amount) = @_; $self->balance($self->balance + $amount); } sub withdraw { my ($self, $amount) = @_; my $current_balance = $self->balance(); ($current_balance >= $amount) || confess "Account overdrawn"; $self->balance($current_balance - $amount); } package CheckingAccount; use Moose; extends 'BankAccount'; has 'overdraft_account' => (isa => 'BankAccount', is => 'rw'); before 'withdraw' => sub { my ($self, $amount) = @_; my $overdraft_amount = $amount - $self->balance(); if ($self->overdraft_account && $overdraft_amount > 0) { $self->overdraft_account->withdraw($overdraft_amount); $self->deposit($overdraft_amount); } };
In the first recipe we demonstrated the construction of basic Moose classes whose attributes had various accessor schemes and builtin type constraints. However, our objects were very data- oriented, and did not have many behavioral aspects (i.e. methods) to them. In this recipe, we will expand upon the concepts from the first recipe and give a more realistic scenario of more behavior oriented classes.
We are using the example of a bank account, which has a standard account (you can deposit money, withdraw money and check your current balance), and a checking account which has optional overdraft protection. The overdraft protection will protect the owner of the checking account by automatically withdrawing the needed funds from the overdraft account to ensure that a check will not bounce.
Now, onto the code. The first class, BankAccount, introduces a new attribute feature: a default value.
has 'balance' => (isa => 'Int', is => 'rw', default => 0);
This tells us that a BankAccount has a balance
attribute,
which has the Int
type constraint, a read/write accessor,
and a default value of 0
. This means that every instance of
BankAccount that is created will have its balance
slot
initialized to 0
. Very simple really :)
Next come the methods. The deposit
and withdraw
methods
should be fairly self-explanatory; they are nothing specific to
Moose, just your standard Perl 5 OO.
Now, onto the CheckingAccount class. As you know from the
first recipe, the keyword extends
sets a class's superclass
relationship. Here we see that CheckingAccount is a
BankAccount. The next line introduces yet another new aspect
of Moose, that of class-based type-constraints:
has 'overdraft_account' => (isa => 'BankAccount', is => 'rw');
Up until now, we have only had Int
type constraints, which
(as I said in the first recipe) is a builtin type constraint
that Moose provides for you. The BankAccount
type constraint
is new, and was actually defined the moment we created the
BankAccount class itself. In fact, for every Moose class that
you define, a corresponding type constraint will be created. This
means that in the first recipe, both Point
and Point3D
type
constraints were created, and in this recipe, both BankAccount
and CheckingAccount
type constraints were created. Moose does
this as a convenience so that your class model and the type
constraint model can be kept in sync with one another. In short,
Moose makes sure that it will just DWIM (1).
Next, we come to the behavioral part of CheckingAccount, and
again we see a method modifier, but this time it is a before
modifier.
before 'withdraw' => sub { my ($self, $amount) = @_; my $overdraft_amount = $amount - $self->balance(); if ($self->overdraft_account && $overdraft_amount > 0) { $self->overdraft_account->withdraw($overdraft_amount); $self->deposit($overdraft_amount); } };
Just as with the after
modifier from the first recipe, Moose
will handle calling the superclass method (in this case the
BankAccount::withdraw
method). The before
modifier shown
above will run (obviously) before the code from the superclass
with run. The before
modifier here implements the overdraft
protection by first checking if there are enough available
funds in the checking account and if not (and if there is an overdraft
account available), it transfers the appropriate funds into the
checking account.
As with the method modifier in the first recipe, there is another
way to accomplish this same thing using the built in SUPER::
pseudo-package. So the above method is equivalent to the one here.
sub withdraw { my ($self, $amount) = @_; my $overdraft_amount = $amount - $self->balance(); if ($self->overdraft_account && $overdraft_amount > 0) { $self->overdraft_account->withdraw($overdraft_amount); $self->deposit($overdraft_amount); } $self->SUPER::withdraw($amount); }
The benefits of taking the method modifier approach is that the
author of the BankAccount subclass does not need to remember
to call SUPER::withdraw
and to pass it the $amount
argument.
Instead the method modifier ensures that all arguments make it
to the superclass method correctly. But this is actually more
than just a convenience for forgetful programmers, it also helps
isolate subclasses from changes in the superclasses. For instance,
if BankAccount::withdraw were to add an additional argument
of some kind, the version of CheckingAccount::withdraw which
uses SUPER::withdraw
would not pass that extra argument
correctly, whereas the method modifier version would automatically
pass along all arguments correctly.
Just as with the first recipe, object instantiation is a fairly normal process, here is an example:
my $savings_account = BankAccount->new(balance => 250); my $checking_account = CheckingAccount->new( balance => 100, overdraft_account => $savings_account );
And as with the first recipe, a more in-depth example of using these classes can be found in the t/002_recipe.t test file.
The aim of this recipe was to take the knowledge gained in the first recipe and expand upon it with a more realistic use case. I hope that this recipe has accomplished this goal. The next recipe will expand even more upon the capabilities of attributes in Moose to create a behaviorally sophisticated class almost entirely defined by attributes.
Object
,
and specializes the constraint check to allow for subclasses. This
means that an instance of CheckingAccount will pass a
BankAccount
type constraint successfully. For more details,
please refer to the the Moose::Util::TypeConstraints manpage documentation.
http://www.gigamonkeys.com/book/object-reorientation-generic-functions.html
Stevan Little <stevan@iinteractive.com>
Copyright 2006-2008 by Infinity Interactive, Inc.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
Moose::Cookbook::Recipe2 - A simple B<BankAccount> example |