The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Moose::Cookbook::Recipe2 - A simple BankAccount example

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);
      }
  };

DESCRIPTION

In the first recipe we showed how to build basic Moose classes whose attributes had various accessor schemes and built in type constraints. However our objects were very data-oriented, and did not have many behavioral aspects to them (i.e. - methods). 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 an 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, that of 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 explanitory, 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 built-in type constraint that Moose provides for you. The BankAccount type constraint is new, and was actually defined at the moment we created the BankAccount class itself. In fact, for every Moose class that you define, a corresponding type constraint will be created for that class. This means that in the first recipe, a Point and Point3D type constraint were created, and in this recipe, both a BankAccount and a CheckingAccount type constraint were created. Moose does this as a convenience for you so that your class model and the type constraint model can both 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 we have 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 assures 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.

CONCLUSION

The aim of this recipe was to take the knowledge learned in the first recipe and expand upon it within a more realistic use case. I hope that this recipe has accomplished this goal. The next recipe will expand even more upon the capabilties of attributes in Moose to create a behaviorally sophisticated class almost entirely defined by attributes.

FOOTNOTES

(1)

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 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 Moose::Util::TypeConstraints documentation.

SEE ALSO

Acknowledgement

The BankAccount example in this recipe is directly taken from the examples in this chapter of "Practical Common Lisp". A link to that can be found here:

http://www.gigamonkeys.com/book/object-reorientation-generic-functions.html

AUTHOR

Stevan Little <stevan@iinteractive.com>

COPYRIGHT AND LICENSE

Copyright 2006, 2007 by Infinity Interactive, Inc.

http://www.iinteractive.com

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.