Moose > Moose::Cookbook::Extending::Recipe1

題名

Moose::Cookbook::Extending::Recipe1 - Mooseの拡張モジュール概観

本文

Mooseには拡張モジュールを割り込ませてMooseそのものの振る舞いを変えるやり方がいくつかあります。また、振る舞いを変えられる部分もたくさんあります。このレシピではそれぞれの拡張メソッドの概要とともに、おすすめのツールをいくつか紹介します。

まだメタクラスのレシピを読んでいない方は、そちらを先にご覧ください。メタクラスを理解しないままMooseの拡張モジュールを書くことはできませんし、そちらのレシピではメタクラスのサブクラスやトレートのような基本的な拡張のメカニズムを説明しています。

ほかの拡張モジュールとうまくつきあう

このドキュメントを書いた目的のひとつは、ほかの拡張モジュールと相性のよい拡張モジュールを作る手助けをすることです。これは、拡張モジュールをCPANにリリースするつもりがあるなら特に大事なことです。

Mooseには相性のよい拡張モジュールを書くときに役立つモジュールがいくつかあります。Moose::ExporterMoose::Util::MetaRoleを使うと、Mooseのコア機能だけでなく、これらのモジュールを使っているほかのCPANモジュールとも確実に組み合わせられるようになります。

Mooseを拡張できるポイント

みなさんがMooseの拡張モジュールでしたいと思うようなことは、大きくいくつかのカテゴリーに分類できます。

メタクラスの拡張

Mooseを拡張する方法のひとつは、Mooseのメタクラスを拡張することです。たとえば、Moose::Cookbook::Meta::Recipe4ではメタクラスにtableアトリビュートを追加するサブクラスの例を見ました。ORMを書いている人にとっては、これは理にかなった拡張といえるでしょう。

CPANにあがっている拡張モジュールの多くは、アトリビュートのメタクラスを拡張するものです。たとえば、MooseX::AttributeHelpersというディストリビューションは、オブジェクト以外のアトリビュート(ハッシュリファレンスや単なる数)にも振る舞いを委譲する新しいアトリビュートメタクラスを提供するものです。

メタクラスを拡張するモジュールは、サブクラスとしても、ロール/トレートとしてもパッケージングできますが、サブクラスにするよりはトレートにすることをおすすめします。サブクラスをたくさん合成するより、バラバラなトレートを合成する方がはるかに簡単ですから。

拡張モジュールをロールとして実装する場合は、Moose::Util::MetaRoleモジュールを利用できます。

シュガー関数の提供

メタクラスを拡張するモジュールの場合、その構成要素の一部として、Moose.pmがしているのと同じようにシュガー関数を提供したくなる場合もあるかもしれません。Mooseにはこれを非常に簡単にするMoose::Exporterというヘルパーモジュールがあります(Moose::Exporterはこの先のレシピでも何度か利用します)。

オブジェクトクラスの拡張

Mooseを拡張する常套手段としてはもうひとつ、デフォルトのオブジェクトクラスの振る舞いを変えるというテクニックがあります。たとえば、MooseX::Singletonモジュールはオブジェクトの振る舞いを変えてシングルトンにしてくれますし、MooseX::StrictConstructorモジュールを使うと、対応するアトリビュートがない引数はコンストラクタが拒否するようになります。

オブジェクトクラスの拡張モジュールにはしばしばメタクラスの拡張モジュールも含まれています。特に、クラスを不変化したときでもオブジェクトの拡張を動作させたい場合は、Moose::Meta::InstanceMoose::Meta::Method::ConstructorMoose::Meta::Method::Destructorといったオブジェクトをいくつか(あるいはすべて)拡張する必要があるかもしれません。

Moose::Util::MetaRoleというモジュールを使うと、前述したメタクラスだけでなく、オブジェクトのベースクラスにもロールを組み込めるようになります。

ロールを提供する

拡張モジュールの中には、ユーザに取り込んでもらうロールの形をとっているものもあります。MooseX::Object::Pluggableというモジュールがその好例です(実はこれ、MooseXという名前はついていますが、実際にはMooseの振る舞いを変更するものではなく、プラガブルにしたいオブジェクトに取り込ませることのできるロールにすぎないのです)。

この手の拡張モジュールを実装する場合は、特に何かをする必要はありません。単にロールを作って、ドキュメントにはいつものようにwithというシュガー関数経由で使ってください、と書くだけです。

   package MyApp::User;

   use Moose;

   with 'MooseX::My::Role';

新しい型

もうひとつよくあるMooseの拡張モジュールとして、Mooseの型システムに新しい型を追加するものがあります。この場合は単にモジュールの中で型を作るだけです。ユーザがそのモジュールをロードしたら、型が作られ、以後はその名前で型を参照できるようになります。これの実例としては、MooseX::Types::URIMooseX::Types::DateTimeというディストリビューションがあげられます。いずれもMooseX::Typesモジュールを下敷きにして作られたものです。

ロールとトレートとサブクラス

これはぜひ理解しておいていただきたいのですが、ロールとトレートは同じものです。トレーとはインスタンスにロールを適用したものです。唯一の違いは、トレートは名前の一部を省略してもMooseがクラス名を導き出せるようにパッケージングされていることです。言い換えると、トレートの場合は「Big」のような短縮表記で呼び出せるし、MooseはそこからMooseX::Embiggen::Meta::Attribute::Role::Bigのようなクラス名を導き出せる、ということです。

実際にトレートを利用している例については、Moose::Cookbook::Meta::Recipe3Moose::Cookbook::Meta::Recipe5をご覧ください。いずれも特にトレートの名前解決メカニズムを紹介しています。

拡張モジュールをメタクラスやベースオブジェクトのロールとして実装すると、ほかのモジュールとの相性がよくなります。エンドユーザがメタクラスのサブクラスをいくつも効果的に合成するのは大変なことですが、ロールを合成するのはごく簡単なことだからです。

自作の拡張モジュールを使う

拡張モジュールを組み込めるポイントはいくつもあります。場合によっては自作の拡張モジュールを複数の方法で取り込めるようにすることもできます。

メタクラスのトレートとしての拡張モジュール

トレートとしても使える拡張モジュールであれば、エンドユーザには単にトレートのリストの中に名前を入れておいてください、と言うことができます(いまのところこの手が使えるのは(クラスの)メタクラスとアトリビュートのメタクラスのトレートのみですが)。

  use Moose -traits => [ 'Big', 'Blue' ];

  has 'animal' => (
      traits => [ 'Big', 'Blue' ],
      ...
  );

ほかのメタクラスやオブジェクトのベースクラスに組み込める拡張モジュールの場合は、トレートのメカニズムを利用することはできません。

トレートのメカニズムの利点は、トレートがコードのどこで組み込まれているかが非常にわかりやすいことです。また、トレートを組み込む対象についても、取り込む側の方で細かくコントロールできます(特にアトリビュートのトレートの場合はそうです。アトリビュートのトレートは、クラスのひとつのアトリビュートだけに組み込むこともできます)。

メタクラス(およびベースオブジェクト)のサブクラスとしての拡張モジュール

Mooseには、取り込む側の方で使いたいサブクラスを指定できるような簡単なAPIは用意されていません(例外はアトリビュートのメタクラスです。アトリビュートの宣言には、拡張モジュールを取り込む側がサブクラスを指定するのに使えるmetaclassというオプションがあります)。

これが、拡張モジュールをサブクラスとして実装するのはよくない、というひとつの理由になっているのですが、インポート時に呼び出し元に対してMoose->init_metaを実行して、別のメタクラスやベースオブジェクトクラスを指定すれば、特定のサブクラスの利用を強制することはできます。

どうしてもそうしたい場合は、Moose::Exporterを使ってMoose.pmのシュガー関数を再エクスポートするようにしてください。Moose::Exporterを使うと、エクスポートするクラスにinit_metaメソッドがある場合は、クラスがインポートされるときに確実にそのinit_metaメソッドを呼んでくれます。

そのinit_metaの中で、呼び出し元に指定したサブクラスを利用するように手配できます。

  package MooseX::Embiggen;

  use Moose ();
  use Moose::Exporter;

  use MooseX::Embiggen::Meta::Class;
  use MooseX::Embiggen::Object;

  Moose::Exporter->setup_import_methods( also => 'Moose' );

  sub init_meta {
      shift;    # just your package name
      my %options = @_;

      return Moose->init_meta(
          for_class  => $options{for_class},
          metaclass  => 'MooseX::Embiggen::Meta::Class',
          base_class => 'MooseX::Embiggen::Object',
      );
  }

注意: init_metaは、Moose->init_metaがしているように、かならずメタクラスオブジェクトを返すようにしてください。

メタクラス(およびベースオブジェクト)のロールとしての拡張モジュール

拡張モジュールをメタクラスのロールとして実装すると、拡張モジュールを組み込みやすくなりますし、ほかのロールベースのメタクラスの拡張モジュールとの相性もよくなります。

サブクラスの場合と同様に、ロールとして取り込んでもらいたい拡張モジュールもMoose::Exporterを利用する単一のモジュールとしてパッケージしたくなるかもしれませんが、この場合はMoose::Util::MetaRoleを利用してすべてのロールを組み込むようにします。このモジュールを使う利点は、「すでにユーザのメタクラスに組み込まれているサブクラスやロールには手をつけない」ことです。これはつまり、「デフォルトで」拡張モジュールの相性がよくなるため、ユーザがほかのロールベースの拡張モジュールと簡単に組み合わせられるようになる、ということです。

  package MooseX::Embiggen;

  use Moose ();
  use Moose::Exporter;

  use MooseX::Embiggen::Role::Meta::Class;
  use MooseX::Embiggen::Role::Meta::Attribute;
  use MooseX::Embiggen::Role::Meta::Method::Constructor;
  use MooseX::Embiggen::Role::Object;

  my ( $import, $unimport, $init_meta ) = Moose::Exporter->build_import_methods(
      also => ['Moose'] metaclass_roles =>
          ['MooseX::Embiggen::Role::Meta::Class'],
      attribute_metaclass_roles => ['MooseX::Embiggen::Role::Meta::Attribute'],
      constructor_class_roles =>
          ['MooseX::Embiggen::Role::Meta::Method::Constructor'],
      base_class_roles => ['MooseX::Embiggen::Role::Object'],
      install          => [qw(import unimport)],
  );

  sub init_meta {
      my $package = shift;
      my %options = @_;

      Moose->init_meta(%options);

      return $package->$init_meta(%options);
  }

ご覧の通り、Moose::Util::MetaRoleはどんなメタクラスにロールを組み込むときでも使えます(ベースオブジェクトクラスの場合も同様です)。拡張モジュールがロールを組み込んでも、ほかの拡張モジュールが組み込んだロールはそのまま残りますし、その逆も同様です。なお、Moose::Util::MetaRoleのを必要とするほとんどの箇所ではMoose::Exporterが代わりに全て処理することも可能です。詳しくはMoose::Exporterのドキュメントをご覧下さい。

シュガー関数を提供する

Moose::Exporterを使うと、独自のシュガー関数をエクスポートすることができます。また、ほかのモジュールのシュガー関数をエクスポートすることもできます。

  package MooseX::Embiggen;

  use Moose ();
  use Moose::Exporter;

  Moose::Exporter->setup_import_methods(
      with_meta => ['embiggen'],
      also        => 'Moose',
  );

  sub embiggen {
      my $meta = shift;
      $meta->embiggen(@_);
  }

こうすると、この拡張モジュールを取り込んだクラスではembiggenサブルーチンを使うことができるようになります。

  package Consumer;

  use MooseX::Embiggen;

  extends 'Thing';

  embiggen ...;

これをメタクラスやベースクラスのロールと合成するのはごく簡単です。

従来の拡張のメカニズム

Moose::ExporterMoose::Util::MetaRoleが登場する前にも、Mooseを拡張する方法はいくつもありました。ただし、一般的に、これらのメソッドはあまり相性がよくありませんし、拡張モジュールがひとつのときにしかうまく動きませんでした。

そういったメソッドの例としては、metaclass.pmMoose::Policy(これは裏でmetaclass.pmを利用しています)、それからMoose::Exporterがしているさまざまなハックをするものがありますが、みなさんの拡張モジュールではそういったメソッドを使わないようにしてください。

念のため、相性のよい拡張モジュールを書けば、古い拡張モジュールと組み合わせることもできます(一般的に古い拡張モジュールはお互いに相性がよくないものですが)。

まとめ

拡張モジュールを書くとき、メタクラスやベースオブジェクトのロールとして書けそうならぜひそうしてください。Moose::ExporterMoose::Util::MetaRoleのドキュメントを読んでおくのもお忘れなく。

作者

Dave Rolsky <autarch@urth.org>

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

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