385 lines
11 KiB
Plaintext
385 lines
11 KiB
Plaintext
# 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
|