This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
# PODNAME: Moose::Cookbook::Basics::BankAccount_MethodModifiersAndSubclassing
|
||||
# ABSTRACT: Demonstrates the use of method modifiers in a subclass
|
||||
|
||||
__END__
|
||||
|
||||
=pod
|
||||
|
||||
=encoding UTF-8
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Moose::Cookbook::Basics::BankAccount_MethodModifiersAndSubclassing - Demonstrates the use of method modifiers in a subclass
|
||||
|
||||
=head1 VERSION
|
||||
|
||||
version 2.2207
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
The first recipe demonstrated how to build very basic Moose classes,
|
||||
focusing on creating and manipulating attributes. The objects in that
|
||||
recipe were very data-oriented, and did not have much in the way of
|
||||
behavior (i.e. methods). In this recipe, we expand upon the concepts
|
||||
from the first recipe to include some real behavior. In particular, we
|
||||
show how you can use a method modifier to implement new behavior for a
|
||||
method.
|
||||
|
||||
The classes in the SYNOPSIS show two kinds of bank account. A simple
|
||||
bank account has one attribute, the balance, and two behaviors,
|
||||
depositing and withdrawing money.
|
||||
|
||||
We then extend the basic bank account in the CheckingAccount
|
||||
class. This class adds another attribute, an overdraft account. It
|
||||
also adds overdraft protection to the withdraw method. If you try to
|
||||
withdraw more than you have, the checking account attempts to
|
||||
reconcile the difference by withdrawing money from the overdraft
|
||||
account. (1)
|
||||
|
||||
The first class, B<BankAccount>, introduces a new attribute feature, a
|
||||
default value:
|
||||
|
||||
has 'balance' => ( isa => 'Int', is => 'rw', default => 0 );
|
||||
|
||||
This says that a B<BankAccount> has a C<balance> attribute, which has
|
||||
an C<Int> type constraint, a read/write accessor, and a default value
|
||||
of C<0>. This means that every instance of B<BankAccount> that is
|
||||
created will have its C<balance> slot initialized to C<0>, unless some
|
||||
other value is provided to the constructor.
|
||||
|
||||
The C<deposit> and C<withdraw> methods should be fairly
|
||||
self-explanatory, as they are just plain old Perl 5 OO. (2)
|
||||
|
||||
As you know from the first recipe, the keyword C<extends> sets a
|
||||
class's superclass. Here we see that B<CheckingAccount> C<extends>
|
||||
B<BankAccount>. The next line introduces yet another new attribute
|
||||
feature, class-based type constraints:
|
||||
|
||||
has 'overdraft_account' => ( isa => 'BankAccount', is => 'rw' );
|
||||
|
||||
Up until now, we have only seen the C<Int> type constraint, which (as
|
||||
we saw in the first recipe) is a builtin type constraint. The
|
||||
C<BankAccount> type constraint is new, and was actually defined the
|
||||
moment we created the B<BankAccount> class itself. In fact, Moose
|
||||
creates a corresponding type constraint for every class in your
|
||||
program (3).
|
||||
|
||||
This means that in the first recipe, constraints for both C<Point> and
|
||||
C<Point3D> were created. In this recipe, both C<BankAccount> and
|
||||
C<CheckingAccount> type constraints are created automatically. Moose
|
||||
does this as a convenience so that your classes and type constraint
|
||||
can be kept in sync with one another. In short, Moose makes sure that
|
||||
it will just DWIM (4).
|
||||
|
||||
In B<CheckingAccount>, we see another method modifier, the C<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 C<after> modifier from the first recipe, Moose will
|
||||
handle calling the superclass method (in this case C<<
|
||||
BankAccount->withdraw >>).
|
||||
|
||||
The C<before> modifier will (obviously) run I<before> the code from
|
||||
the superclass is run. Here, C<before> modifier implements overdraft
|
||||
protection by first checking if there are available funds in the
|
||||
checking account. If not (and if there is an overdraft account
|
||||
available), it transfers the amount needed into the checking
|
||||
account (5).
|
||||
|
||||
As with the method modifier in the first recipe, we could use
|
||||
C<SUPER::> to get the same effect:
|
||||
|
||||
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 benefit of taking the method modifier approach is we do not need
|
||||
to remember to call C<SUPER::withdraw> and pass it the C<$amount>
|
||||
argument when writing C<< CheckingAccount->withdraw >>.
|
||||
|
||||
This is actually more than just a convenience for forgetful
|
||||
programmers. Using method modifiers helps isolate subclasses from
|
||||
changes in the superclasses. For instance, if B<<
|
||||
BankAccount->withdraw >> were to add an additional argument of some
|
||||
kind, the version of B<< CheckingAccount->withdraw >> which uses
|
||||
C<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 uses the C<new>
|
||||
method, which accepts named parameters.
|
||||
|
||||
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 can be found in
|
||||
the F<t/recipes/basics_bankaccount_methodmodifiersandsubclassing.t> test file.
|
||||
|
||||
=head1 CONCLUSION
|
||||
|
||||
This recipe expanded on the basic concepts from the first recipe with
|
||||
a more "real world" use case.
|
||||
|
||||
=head1 FOOTNOTES
|
||||
|
||||
=over 4
|
||||
|
||||
=item (1)
|
||||
|
||||
If you're paying close attention, you might realize that there's a
|
||||
circular loop waiting to happen here. A smarter example would have to
|
||||
make sure that we don't accidentally create a loop between the
|
||||
checking account and its overdraft account.
|
||||
|
||||
=item (2)
|
||||
|
||||
Note that for simple methods like these, which just manipulate some
|
||||
single piece of data, it is often not necessary to write them at all.
|
||||
For instance, C<deposit> could be implemented via the C<inc> native
|
||||
delegation for counters - see
|
||||
L<Moose::Meta::Attribute::Native::Trait::Counter> for more specifics,
|
||||
and L<Moose::Meta::Attribute::Native> for a broader overview.
|
||||
|
||||
=item (3)
|
||||
|
||||
In reality, this creation is sensitive to the order in which modules
|
||||
are loaded. In more complicated cases, you may find that you need to
|
||||
explicitly declare a class type before the corresponding class is
|
||||
loaded.
|
||||
|
||||
=item (4)
|
||||
|
||||
Moose does not attempt to encode a class's is-a relationships within
|
||||
the type constraint hierarchy. Instead, Moose just considers the class
|
||||
type constraint to be a subtype of C<Object>, and specializes the
|
||||
constraint check to allow for subclasses. This means that an instance
|
||||
of B<CheckingAccount> will pass a C<BankAccount> type constraint
|
||||
successfully. For more details, please refer to the
|
||||
L<Moose::Util::TypeConstraints> documentation.
|
||||
|
||||
=item (5)
|
||||
|
||||
If the overdraft account does not have the amount needed, it will
|
||||
throw an error. Of course, the overdraft account could also have
|
||||
overdraft protection. See note 1.
|
||||
|
||||
=back
|
||||
|
||||
=head1 ACKNOWLEDGMENT
|
||||
|
||||
The BankAccount example in this recipe is directly taken from the
|
||||
examples in this chapter of "Practical Common Lisp":
|
||||
|
||||
L<http://www.gigamonkeys.com/book/object-reorientation-generic-functions.html>
|
||||
|
||||
=begin testing
|
||||
|
||||
my $savings_account;
|
||||
|
||||
{
|
||||
$savings_account = BankAccount->new( balance => 250 );
|
||||
isa_ok( $savings_account, 'BankAccount' );
|
||||
|
||||
is( $savings_account->balance, 250, '... got the right savings balance' );
|
||||
is(
|
||||
exception {
|
||||
$savings_account->withdraw(50);
|
||||
},
|
||||
undef,
|
||||
'... withdrew from savings successfully'
|
||||
);
|
||||
is( $savings_account->balance, 200,
|
||||
'... got the right savings balance after withdrawal' );
|
||||
|
||||
$savings_account->deposit(150);
|
||||
is( $savings_account->balance, 350,
|
||||
'... got the right savings balance after deposit' );
|
||||
}
|
||||
|
||||
{
|
||||
my $checking_account = CheckingAccount->new(
|
||||
balance => 100,
|
||||
overdraft_account => $savings_account
|
||||
);
|
||||
isa_ok( $checking_account, 'CheckingAccount' );
|
||||
isa_ok( $checking_account, 'BankAccount' );
|
||||
|
||||
is( $checking_account->overdraft_account, $savings_account,
|
||||
'... got the right overdraft account' );
|
||||
|
||||
is( $checking_account->balance, 100,
|
||||
'... got the right checkings balance' );
|
||||
|
||||
is(
|
||||
exception {
|
||||
$checking_account->withdraw(50);
|
||||
},
|
||||
undef,
|
||||
'... withdrew from checking successfully'
|
||||
);
|
||||
is( $checking_account->balance, 50,
|
||||
'... got the right checkings balance after withdrawal' );
|
||||
is( $savings_account->balance, 350,
|
||||
'... got the right savings balance after checking withdrawal (no overdraft)'
|
||||
);
|
||||
|
||||
is(
|
||||
exception {
|
||||
$checking_account->withdraw(200);
|
||||
},
|
||||
undef,
|
||||
'... withdrew from checking successfully'
|
||||
);
|
||||
is( $checking_account->balance, 0,
|
||||
'... got the right checkings balance after withdrawal' );
|
||||
is( $savings_account->balance, 200,
|
||||
'... got the right savings balance after overdraft withdrawal' );
|
||||
}
|
||||
|
||||
{
|
||||
my $checking_account = CheckingAccount->new(
|
||||
balance => 100
|
||||
|
||||
# no overdraft account
|
||||
);
|
||||
isa_ok( $checking_account, 'CheckingAccount' );
|
||||
isa_ok( $checking_account, 'BankAccount' );
|
||||
|
||||
is( $checking_account->overdraft_account, undef,
|
||||
'... no overdraft account' );
|
||||
|
||||
is( $checking_account->balance, 100,
|
||||
'... got the right checkings balance' );
|
||||
|
||||
is(
|
||||
exception {
|
||||
$checking_account->withdraw(50);
|
||||
},
|
||||
undef,
|
||||
'... withdrew from checking successfully'
|
||||
);
|
||||
is( $checking_account->balance, 50,
|
||||
'... got the right checkings balance after withdrawal' );
|
||||
|
||||
isnt(
|
||||
exception {
|
||||
$checking_account->withdraw(200);
|
||||
},
|
||||
undef,
|
||||
'... withdrawal failed due to attempted overdraft'
|
||||
);
|
||||
is( $checking_account->balance, 50,
|
||||
'... got the right checkings balance after withdrawal failure' );
|
||||
}
|
||||
|
||||
=end testing
|
||||
|
||||
=head1 AUTHORS
|
||||
|
||||
=over 4
|
||||
|
||||
=item *
|
||||
|
||||
Stevan Little <stevan@cpan.org>
|
||||
|
||||
=item *
|
||||
|
||||
Dave Rolsky <autarch@urth.org>
|
||||
|
||||
=item *
|
||||
|
||||
Jesse Luehrs <doy@cpan.org>
|
||||
|
||||
=item *
|
||||
|
||||
Shawn M Moore <sartak@cpan.org>
|
||||
|
||||
=item *
|
||||
|
||||
יובל קוג'מן (Yuval Kogman) <nothingmuch@woobling.org>
|
||||
|
||||
=item *
|
||||
|
||||
Karen Etheridge <ether@cpan.org>
|
||||
|
||||
=item *
|
||||
|
||||
Florian Ragwitz <rafl@debian.org>
|
||||
|
||||
=item *
|
||||
|
||||
Hans Dieter Pearcey <hdp@cpan.org>
|
||||
|
||||
=item *
|
||||
|
||||
Chris Prather <chris@prather.org>
|
||||
|
||||
=item *
|
||||
|
||||
Matt S Trout <mstrout@cpan.org>
|
||||
|
||||
=back
|
||||
|
||||
=head1 COPYRIGHT AND LICENSE
|
||||
|
||||
This software is copyright (c) 2006 by Infinity Interactive, Inc.
|
||||
|
||||
This is free software; you can redistribute it and/or modify it under
|
||||
the same terms as the Perl 5 programming language system itself.
|
||||
|
||||
=cut
|
||||
Reference in New Issue
Block a user