Perl Hackers Hub

第13回 メタオブジェクトプロトコル入門(2)

この記事を読むのに必要な時間:およそ 5 分

Mooseでもっと簡単に!

ここまでClass::MOPによるクラス定義を紹介してきましたが,クラスを定義するためだけにリスト1のようなコードを書くのは冗長です。そこでメタレイヤ定義に必要な決まり文句的なコードの宣言を隠蔽したシンタックスシュガーや,さまざまなデフォルト動作を組み込んだツールが作られました。それがMooseです。

クラスの定義

ではさっそく先ほどMOPで直接定義したクラスをMooseで再定義してみましょうリスト5)⁠

リスト5 Mooseによるクラスの定義

package Person;
use Moose; # (1)

# (2)アトリビュートの定義
has 'name' => (
    is => 'rw',
    required => 1,
);

# (3)メソッドの定義
sub describe {
    my $self = shift;
    my $name = $self->name;
    print "$name";
}

リスト5(1)use Mooseと宣言すると,Personクラス用のメタクラスを生成するほか,このメタクラスに対して簡単に操作を行えるようにいくつかの関数をエクスポートします。strictwarnings(警告)も自動的にエクスポートされます。

アトリビュート作成はリスト5(2)hasを使うと$meta->add_attribute()を呼んでいるのと同等になり,nameアトリビュートが生成されます。引数のis => 'rw'add_attributeの際にaccessor引数を渡しているのと同等になり,読み書き可能なアクセサを作成することを意味します。また,Moose::Meta::Attributeではrequiredを指定することによってオブジェクト生成時にこのアトリビュートが引数で渡されていることが必須になります。

リスト5(3)のメソッド定義では特に何かを変える必要はありません。subでメソッドを定義するだけでMOPに自動的に$meta->add_methodが呼ばれたのと同じ状態になります。

Mooseを使用している場合,リスト6のようにして得られるメタクラスオブジェクトはClass::MOP::Class型ではなくMoose::Meta::Class型であることに注意してください。ただし実装としてはMoose::Meta::ClassはClass::MOP::Classを継承していますので,ロールなどの追加機能以外は互換性のあるAPIとなっています。

リスト6 メタクラスの親クラス

my $meta = Person->meta;
$meta->isa( 'Class::MOP::Class' ); # true!
$meta->isa( 'Moose::Meta::Class' ); # true!

クラスの継承

MooseでPersonクラスが再定義できたので,今度はEmployeeクラスを定義してみましょうリスト7)⁠

リスト7 Mooseによるサブクラスの定義

package Employee;
use Moose;
extends 'Person'; # (1)親クラス

has 'employer' => ( # (2)アトリビュート
    is => 'rw',
    isa => 'Company',
    required => 1,
);

before describe => sub { # (3)メソッドモディファイア
    my $self = shift;
    my $company = $self->employer->name;
    print "所属 $company, 社員名 ";
};

Employeeクラスでは親クラスの指定が必要ですので,リスト7(1)で親クラスを指定します。extends$meta->superclassesと同等になります。

リスト7(2)でemployerアトリビュートを追加していますが,今度はここにisa引数を指定しています。isaを追加すると動的な型制約を指定できます。

最後にリスト7(3)describeにメソッドモディファイアを追加しています。Class::MOP版に比べると冗長だったadd_before_method_modifierの呼び出しがぐっとシンプルになりました。

MOPとMooseの使い分け

MOPを直接使ってもMooseのようなシンタックスシュガーを使っても,結果的に生成されるクラスの機能は変わりませんが,通常はより定義が簡潔なMooseを使うほうがよいでしょう。機械的に複数のクラスを生成したり,高度なカスタマイズを行う場合はMOPを直接使ったほうが有効でしょう。

なおMooseは比較的好き嫌いが分かれるツールですが,Moose をお手本にして同様の機能を提供するMouseやMooなどのモジュールも開発されています。それらのモジュールは,高速化のためにMOPの一部機能だけを提供しつつもMooseと似たシンタックスシュガーを提供するようになっています。

メタクラスの拡張

ここまでMOPで通常のクラスを定義する方法を紹介してきました。ですが,メタクラスはオブジェクトですので,このオブジェクト群を拡張して適用することで,さらにさまざまな応用ができます。たとえばhas()でアトリビュートを定義したときに作成されるアクセサの挙動を変えたり,メタレイヤにそのクラスに関するメタ情報を保存したりできます。ここでは拡張を行う方法を簡単に紹介します。

メタクラスの定義

たとえば,任意のクラスで作成されたオブジェクトインスタンスを追跡するメタクラスを実装できます。オブジェクトがいつどこで作成・破棄されているかを監視するためのデバッグツールを作る場合,もしくはすべてのオブジェクトインスタンスを管理するようなもっと複雑な拡張などを作る場合にとても便利です。

この動作を実装するInstanceTrackerをリスト8に定義します。

リスト8 メタクラスの定義

package InstanceTracker;
use Moose;

# (1)Moose::Meta::Classの子クラスなので,これもメタクラスである
extends 'Moose::Meta::Class';

# (2)生成したインスタンスの配列
has instances => (
    is => 'ro',
    isa => 'ArrayRef',
    default => sub { [] },
);

# インスタンスを生成する継承したメソッドをオーバーライド
sub _construct_instance {
    my $self = shift;

    # Moose::Meta::Classの_construct_instance本体がインスタンスを返す
    my $instance = $self->next::method(@_);

    # (3)このインスタンスを見失わないようにする
    push @{ $self->instances }, $instance;

    # newにインスタンスを返る
    return $instance;
};

リスト8(1)でMoose::Meta::Classを継承しました。さらにリスト8(2)でこのメタクラスから生成したオブジェクトインスタンスを格納しておくためのinstancesアトリビュートも宣言します。

このメタクラスのキモはリスト8(3)で,_construct_instanceをオーバーライドしてなおかつインスタンスを保存しています。これでメタクラス経由ですべてのオブジェクトインスタンスを見つけることができます。

InstanceTrackerメタクラスの利用

ではInstanceTrackerを使ってみましょうリスト9)⁠

リスト9 定義したメタクラスの利用

package User;

# (1)カスタマイズしたメタクラスを利用宣言する
use Moose -metaclass => 'InstanceTracker';

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

# (2)生成時に,InstanceTrackerの_construct_instanceに
# 登録したフックを実行する
my $me = User->new(name => "SARTAK");
my $friend = User->new(name => "DMAKI");

# (3)インスタンス全部を小文字の名前に変換する
for my $instance (@{ User->meta->instances }) {
    $instance->name( lc($instance->name) );
}

print $me->name; # sartak
print $friend->name; # dmaki

リスト9ではUserというクラスのインスタンスをすべて保存することにしました。InstanceTrackerメタクラスをこのUserクラスに適用するには,use Mooseする際にリスト9(1)のように-metaclass引数にメタクラスの名前を渡すだけでOKです。

-metaclass宣言が適用されたクラスは,指定されたメタクラスを使って表現されるようになります。リスト9(2)でオブジェクトを生成するときも裏でInstanceTrackerメタクラスが使用され,最終的にコンストラクタ内でオーバーライドされた_construct_instanceが実行されてメタクラス内のinstancesアトリビュートに新しいオブジェクトが格納されてから返されます。

あとはUser->metaからメタクラスを取得して,instancesの中身に操作を加えることができます。リスト9(3)ではとりあえずすべてのユーザの名前を小文字に変換してみましたが,必要であれば同じようなロジックでデータベース移行だろうが一貫性チェックだろうが,好きなことができます。

著者プロフィール

Shawn M Moore

通称Sartak。ボストン在住。Infinity Interactive Inc.所属。

Mooseなどのツールの開発メンバーとして活躍するほか,YAPCなどでの講演も多数。大の親日家として知られ,YAPC::Asia Tokyoにも頻繁に訪れている。

URL:http://sartak.org

Twitter:@sartak


牧大輔(まきだいすけ)

ブラジル,アメリカで育ち,Network Appliance Inc.勤務後帰国。ライブドア(株)や起業などを経て,現在Japan Perl Association代表理事,NHN Japan所属。

2011年 White Camel Award受賞。著作に『モダンPerl入門』(翔泳社)がある。

コメント

コメントの記入