Moose > Moose::Cookbook::Basics::Recipe4

題名

Moose::Cookbook::Basics::Recipe4 - サブタイプと、簡単なCompanyクラス階層のモデリング

概要

  package Address;
  use Moose;
  use Moose::Util::TypeConstraints;

  use Locale::US;
  use Regexp::Common 'zip';

  my $STATES = Locale::US->new;
  subtype 'USState'
      => as Str
      => where {
             (    exists $STATES->{code2state}{ uc($_) }
               || exists $STATES->{state2code}{ uc($_) } );
         };

  subtype 'USZipCode'
      => as Value
      => where {
             /^$RE{zip}{US}{-extended => 'allow'}$/;
         };

  has 'street'   => ( is => 'rw', isa => 'Str' );
  has 'city'     => ( is => 'rw', isa => 'Str' );
  has 'state'    => ( is => 'rw', isa => 'USState' );
  has 'zip_code' => ( is => 'rw', isa => 'USZipCode' );

  package Company;
  use Moose;
  use Moose::Util::TypeConstraints;

  has 'name' => ( is => 'rw', isa => 'Str', required => 1 );
  has 'address'   => ( is => 'rw', isa => 'Address' );
  has 'employees' => ( is => 'rw', isa => 'ArrayRef[Employee]' );

  sub BUILD {
      my ( $self, $params ) = @_;
      if ( @{ $self->employees || [] } ) {
          foreach my $employee ( @{ $self->employees } ) {
              $employee->employer($self);
          }
      }
  }

  after 'employees' => sub {
      my ( $self, $employees ) = @_;
      if ($employees) {
          foreach my $employee ( @{$employees} ) {
              $employee->employer($self);
          }
      }
  };

  package Person;
  use Moose;

  has 'first_name' => ( is => 'rw', isa => 'Str', required => 1 );
  has 'last_name'  => ( is => 'rw', isa => 'Str', required => 1 );
  has 'middle_initial' => (
      is        => 'rw', isa => 'Str',
      predicate => 'has_middle_initial'
  );
  has 'address' => ( is => 'rw', isa => 'Address' );

  sub full_name {
      my $self = shift;
      return $self->first_name
          . (
          $self->has_middle_initial
          ? ' ' . $self->middle_initial . '. '
          : ' '
          ) . $self->last_name;
  }

  package Employee;
  use Moose;

  extends 'Person';

  has 'title'    => ( is => 'rw', isa => 'Str',     required => 1 );
  has 'employer' => ( is => 'rw', isa => 'Company', weak_ref => 1 );

  override 'full_name' => sub {
      my $self = shift;
      super() . ', ' . $self->title;
  };

本文

このレシピではMoose::Util::TypeConstraintsが提供しているsubtypeというシュガー関数について紹介します。subtype関数を使うとクラスをひとつまるごと用意しなくても宣言的に型制約を生成できるようになります。

また、既存のCPANツールを使ってデータを検証する方法を紹介するため、Locale::USRegexp::Commonを使った制約も作成します。

最後に、requiredというアトリビュートオプションを紹介します。

Addressクラスでは2つのサブタイプを定義しています。ひとつめのサブタイプは、Locale::USモジュールを使って州の名前を検証するものです。このサブタイプは、州の略称も正式名称も受け付けるようになっています。

州の名前は文字列として渡したいので、USState型はMoose組み込みのStr型のサブタイプにしました。これはasというシュガー関数を使って指定します。実際の制約はwhereを使って定義します(この関数はサブルーチンリファレンスをひとつ受け取ります)。このサブルーチンを呼ぶと、チェックしたい値が$_に入ります(1)。また、返り値としてはその型として有効な値かどうかをあらわす真偽値が期待されています。

これでUSState型はMooseの組み込み型と同じように使えるようになりました。

  has 'state'    => ( is => 'rw', isa => 'USState' );

stateアトリビュートに値がセットされると、USState型の制約を満たすかチェックされ、値が有効でなかった場合は例外が発生します。

次のUSZipCodeというサブタイプではRegexp::Commonを使います。Regexp::Commonにはアメリカの郵便番号を検証する正規表現が含まれていますので、これをzip_codeアトリビュートの制約として利用します。

  subtype 'USZipCode'
      => as Value
      => where {
             /^$RE{zip}{US}{-extended => 'allow'}$/;
         };

それぞれの型についてクラスを要求するかわりにサブタイプを使うと、コードが非常に簡潔になります。ここで取り上げた型の場合、値は単なる文字列ですから本当にクラスは必要ありません(ただし、有効な値かどうか確認したいのは本当です)。

ここで作成した型制約は再利用できます。型制約はグローバルなレジストリに名前付きで保存されるので、ほかのクラスからも参照できるのです。ただし、このレジストリはグローバルなので、実際のアプリケーションではMyApp.Type.USStateのようになんらかの擬似的な名前空間を利用することを強くおすすめします。

この2つのサブタイプを使うと、簡単なAddressクラスを定義できます。

続いて、Companyクラスを定義しましょう。このクラスには住所の情報を持たせます。これまでのレシピで見たように、Mooseはそれぞれのクラスに自動的に型制約を生成してくれるので、ここでもCompanyクラスのaddressアトリビュートにはその自動生成された型制約を利用します。

  has 'address'   => ( is => 'rw', isa => 'Address' );

また、会社には名前も必要です。

  has 'name' => ( is => 'rw', isa => 'Str', required => 1 );

ここではrequiredという新しいアトリビュートオプションが登場しました。アトリビュートが必須になっている場合、かならずクラスのコンストラクタにそのアトリビュートを渡さなければなりません(そうでない場合は例外が発生します)。ただし、これもぜひ理解しておいていただきたいのですが、requiredアトリビュートは、型制約が許せば偽値やundefであってもかまいません。

次のemployeesアトリビュートでは「パラメータ付きの」型制約を利用しています。

  has 'employees' => ( is => 'rw', isa => 'ArrayRef[Employee]' );

この制約の意味は、employeesは配列リファレンスでなければならず、配列のそれぞれの要素はEmployeeオブジェクトでなければならない、ということ(「空の」配列リファレンスもこの制約を満たすことは注記しておくべきでしょう)。

ArrayRef[`a]のようなパラメータ指定可能な型制約(「コンテナ」型)は、型パラメータを使うとより具体的に書けますが(実際にはこれらの型を任意の回数ネストさせてHashRef[ArrayRef[Int]]のような制約を作ることもできます)、指定された型を単独で使うだけでもかまいません(だから、ArrayRefという型制約も合法です)。(2)

途中を飛ばしてEmployeeクラスの定義を見ると、employerアトリビュートがあります。

Companyemployeesに値をセットしたときは、それぞれの従業員オブジェクトのemployerアトリビュートも確実に正しいCompanyを参照させたいところです。

そのためにはオブジェクトの生成に割り込みをかける必要があります。Mooseでは、クラスにBUILDメソッドを書くとそうすることができるようになります。定義しておいたBUILDメソッドは、オブジェクトが生成された直後、呼び出し元にそのオブジェクトを返す前に呼ばれます。(3)

CompanyクラスではBUILDメソッドを使って会社の各従業員がかならずemployerアトリビュートに適切なCompanyオブジェクトを持つようにしています。

  sub BUILD {
      my ( $self, $params ) = @_;
      if ( $self->employees ) {
          foreach my $employee ( @{ $self->employees } ) {
              $employee->employer($self);
          }
      }
  }

このBUILDメソッドは、型制約のチェックが済んだあとに実行されます。だから、$self->employeesが配列リファレンスを返すことや、その配列の要素がEmployeeオブジェクトであることは間違いないものと決めてかかっても大丈夫です。

また、Companyemployeesアトリビュートが変わったときもかならず各従業員のemployerを更新しておきたいところです。

そうするには、afterモディファイアが使えます。

  after 'employees' => sub {
      my ( $self, $employees ) = @_;
      if ($employees) {
          foreach my $employee ( @{$employees} ) {
              $employee->employer($self);
          }
      }
  };

ここでもまた、BUILDメソッドと同じく事前に型制約のチェックが済んでいることはわかっていますので、$employees引数が定義されているかどうかだけのチェックで済ませられます。

Personクラスには特に目新しいことはありません。requiredなアトリビュートがいくつかあるのと、レシピ3ではじめて使ったpredicateメソッドがひとつあります。

Employeeクラスも、新しい機能はoverrideメソッドモディファイアだけです。

  override 'full_name' => sub {
      my $self = shift;
      super() . ', ' . $self->title;
  };

これはPerlに組み込まれているSUPER::機能のかわりをするシュガー関数に過ぎないのですが、ひとつ違うところがあります。superには引数を渡せないのです(Mooseは単にメソッドに渡されたのと同じパラメータを渡します)。

t/000_recipes/moose_cookbook_basics_recipe4.tにはもっと詳しい使用例があります。

まとめ

このレシピはあえてやや長く、複雑なものにしました。ここではMooseのクラスと型制約を組み合わせて使う方法や、Mooseを使うとわずかなタイプ量でさまざまな情報を取得できるようになることを説明しました。

また、このレシピではsubtype関数やrequiredアトリビュート、overrideメソッドモディファイアの使い方も紹介しました。

型制約については型変換ともどもこの先のレシピで再度取り上げます。

1

チェックしたい値はwhereブロックの最初の引数として渡されます($_[0]でアクセスできます)。

(2)

ただし、ArrayRef[]は正しく動作しません。このような場合、Mooseはコンテナ型とはみなさず、「ArrayRef[]」という意味不明な新しい型が指定されたものとみなすためです。

(3)

BUILDメソッドは、実際にはMoose::Object->newが呼び出すMoose::Object->BUILDALLから呼ばれます。BUILDALLメソッドは、オブジェクトの継承グラフをたどって、見つけたBUILDメソッドをすべて、正しい順序で呼び出すものです。

作者

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.