Moose > Moose::Cookbook::Basics::Recipe2

題名

Moose::Cookbook::Basics::Recipe2 - 簡単なBankAccountの例

概要

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

本文

最初のレシピでは、アトリビュートの生成や操作に焦点をあてながら、非常に基本的なMooseのクラスの作り方を説明しました。最初のレシピのオブジェクトは非常にデータ指向で、振る舞い(メソッド)についてはたいしたことをしませんでしたが、このレシピでは最初のレシピのコンセプトを拡張して、本当の振る舞いをいくつか追加します。とりわけここではメソッドの新しい振る舞いを実装するためにメソッドモディファイアを使うやり方を紹介します。

概要に掲載したクラスは2種類の銀行口座をあらわすものです。単純な銀行口座の方は、残高(balance)というアトリビュートがひとつに、預け入れ(deposit)、引き出し(withdraw)という2つの振る舞いがあります。

この基本の銀行口座を拡張したのがCheckingAccount(当座預金口座)クラスです。このクラスにはもうひとつ、当座貸越口座(overdraft_account)というアトリビュートを追加しました。また、withdrawメソッドには当座貸越保護の仕組みを追加して、預金残高以上に引き出そうとした場合は当座貸越口座から差額の調整を試みるようにしてあります。(1)

最初のBankAccountクラスではアトリビュートのデフォルト値という新機能が使われています。

  has 'balance' => ( isa => 'Int', is => 'rw', default => 0 );

これは、BankAccountにはbalanceというアトリビュートがあり、このアトリビュートにはInt型の制約と、読み書き可能なアクセサ、0というデフォルト値が用意されている、という意味。つまり、コンストラクタになにか別の値を渡さない限り、生成されたBankAccountのインスタンスのbalanceスロットは0に初期化される、ということです。

depositメソッドとwithdrawメソッドは、昔ながらのプレーンなPerl 5のオブジェクト指向ですから、見ればだいたい意味が通じるでしょう。

最初のレシピでおわかりの通り、extendsキーワードはクラスのスーパークラスを設定するものです(ここではCheckingAccountBankAccountextendsしています)。次の行は、これもまたクラスベースの型制約というアトリビュートの新機能です。

  has 'overdraft_account' => ( isa => 'BankAccount', is => 'rw' );

ここまで見てきたのはInt型の制約だけでした。これは(最初のレシピでも見たように)組み込みの型制約ですが、このBankAccount型の制約は新たに(実際にはBankAccountクラスそのものを作成した瞬間に)定義されたものです。Mooseはプログラムの中で使われているひとつひとつのクラスに対応する型制約を生成するのです。 (2)

つまり、最初のレシピであれば、PointPoint3Dの双方に制約が生成されていましたし、このレシピであれば、BankAccountCheckingAccountの型制約が自動的に生成されます(Mooseが気を利かせて、クラスと型制約がお互いに同期した状態を保てるようにしてくれているのです)。要するに、Mooseを使うと確実に期待した通りのことができるようになります。(3)

CheckingAccountにはもうひとつ、beforeというメソッドモディファイアも見られます。

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

最初のレシピにあったafterモディファイアと同じく、スーパークラスのメソッド(この場合はBankAccount->withdraw)を呼ぶのはMooseの方でしてくれます。

beforeモディファイアは(当然ながら)スーパークラスのコードが実行される「前に」実行されます。ここでは、beforeモディファイアは当座貸越保護を実装するために、まずは当座預金口座の残高を確認しています。残高がない(ただし当座貸越口座は利用できる)場合は、必要な額を当座預金口座に移します。(4)

最初のレシピのメソッドモディファイアと同じく、ここでもSUPER::を使えば同じ効果を得ることはできます。

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

メソッドモディファイアを使うアプローチの利点は、CheckingAccount->withdrawを書くときにSUPER::withdrawを呼んだり$amount引数を渡したりするのを覚えておく必要がないことです。

ただし、これは忘れっぽいプログラマにとって便利な機能というだけではありません。メソッドモディファイアを使うと、サブクラスがスーパークラスの変化を受けづらくなります。たとえば、BankAccount->withdrawになんらかの引数が追加されたとします。SUPER::withdrawを使っている場合、CheckingAccount->withdrawは追加された引数を正しく渡せなくなりますが、メソッドモディファイアを使っていれば自動的にすべての引数を正しく渡せます。

最初のレシピと同じく、オブジェクトのインスタンス化には名前付きのパラメータを受け取れるnewメソッドを使っています。

  my $savings_account = BankAccount->new( balance => 250 );

  my $checking_account = CheckingAccount->new(
      balance           => 100,
      overdraft_account => $savings_account,
  );

これもまた最初のレシピと同じく、t/000_recipes/moose_cookbook_basics_recipe2.tというテストファイルにはもっと詳細な例があります。

まとめ

このレシピでは、より「現実の世界」に近いユースケースを使って、最初のレシピの基本的なコンセプトを拡張しました。

1

よく見てみると、ここでは円形ループが起こりそうになっていることに気がつかれるかもしれません。もっと賢くするなら、当座預金口座と当座貸越口座の間で偶発的にループが発生してしまわないようにする必要があるでしょう。

(2)

実際には、この型制約の生成はモジュールのロード順の影響を受けます。もっと複雑な例になると、クラスに対応する型制約がロードされないうちにクラスの型を明示的に宣言する必要がある場合が出てくるかもしれません。

(3)

Mooseはクラスのis-a関係を型制約の階層構造の中に持ち込むことはしません。クラスの型制約はObjectのサブタイプにすぎないとみなして、型チェックを特殊化し、サブクラスを受け付けるようにします。つまり、CheckingAccountのインスタンスはBankAccount型の制約を満たすわけです。詳しくはMoose::Util::TypeConstraintsのドキュメントをご覧ください。

(4)

当座貸越口座に必要な残高がない場合は、エラーが発生します。もちろん当座貸越口座にも当座貸越保護を持たせることもできるでしょう。注釈(1)をご覧ください。

参照

謝辞

このレシピのBankAccountの例は『実践Common Lisp』の該当の章から直接引用したものです。

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

作者

Stevan Little <stevan@iinteractive.com>

Dave Rolsky <autarch@urth.org>

コピーライト & ライセンス

Copyright 2006-2009 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.