これは モバイルファクトリー Advent Calendar 2018 6日目の記事です。 前日の記事は @koropicot さんの ブロックチェーンの学び方 でした。
こんにちは、新卒ソフトウェアエンジニアの id:mp0liiu です。
吉祥寺.pm#16で「Perlにおけるクラスの実装パターン」というタイトルでLTをしたのですが、
この記事では紹介したクラスの実装パターンのうち、個人的に気に入っている InsideOutオブジェクト(資料でいう18~21P)について詳しく掘り下げてみてみます。
吉祥寺.pm#16で発表した資料はこちらです。
InsideOutテクニックとは?
インスタンスのアドレスを数値として評価し、それをキーとしてパッケージローカルなハッシュ変数などにインスタンス変数を格納することで、完全なカプセル化を実現するクラスの実装パターンです。
Perl Best Practiceで推奨されている方法ですが、あまり普及しませんでした。
メリット
- インスタンス変数を完全にクラスの外部から隠蔽し、カプセル化を実現する
- 拡張性がある
- カプセル化を実現しているクラスの実装パターンのなかでは速度が最も速い
デメリット
- インスタンス変数の呼び出しに比較的時間がかかる
- 毎回blessしているオブジェクトのアドレスを数値に変換する手間がかかるため(高速化のテクニックもあります)
- この実装パターンを知らない人にとってはコードが何をやっているのかかなり理解しにくい
- メモリを開放する処理を自前で実装しなければならない
- インスタンスが必要なくなってもGCがメモリを開放するのはインスタンスの領域だけで、 各インスタンス変数のデータはパッケージローカルな変数に残り続ける
- デバッグ、シリアライズがしづらい
Hash::Util::FieldHash
クラスビルダーというよりかは、InsideOutテクニックでクラスを作るのをサポートするようなユーティリティを提供するモジュールです。
特徴としては、
- インスタンス変数に相当する値へのアクセス、メモリ解放を自動でやってくれる
- コアモジュール(perlv5.9.4から)
- コンストラクタやアクセサは手書き
このモジュールで提供される機能を用いてクラスを作る場合は、以下のようになります。
package Point { use Hash::Util::FieldHash qw( fieldhash ); fieldhash my %x_fields; fieldhash my %y_fields; sub new { my ($class, $x, $y) = @_; my $self = bless \(my $anon), $class; $x_fields{$self} = $x; $y_fields{$self} = $y; $self; } sub x { my $self = shift; $x_fields{$self}; } sub y { my $self = shift; $y_fields{$self}; } } my $p = Point->new(1, 2);
fieldhash
関数で登録したハッシュにインスタンス変数に相当する値を格納しておくと、インスタンスのメモリが開放されるとタイミングで、そのインスタンスのインスタンス変数も自動的に開放されるようになります。
クラスを作る手間はハッシュベースクラスを1からつくるのと同程度になりそうです。
コアモジュールしか使えない環境でInsideOutクラスを作りたいような場合、選択肢に入りそうです。
CPANTSで直接調べられなかったので、CPAN上にあるモジュールでHash::Util::FieldHashに依存しているモジュールの数は正確にはわかりませんでしたが、 perl5.10以前のperlにも対応させたHash::Util::FieldHash::Compatというモジュールに依存しているモジュールの数は、19個でした。
Object::InsideOut
Object::InsideOutは非常に多機能なInsideOutクラスのクラスビルダーです。
特徴としては、
- メモリ解放を自動でやってくれる
- コンストラクタ、アクセサの自動生成機能を提供
- コンストラクタでの引数チェックの機能を提供
- インスタンス変数の型制約をサポート
- Storableでのシリアライズをサポート
- インスタンス変数をハッシュでなく配列に格納させることができるので、他のクラスビルダーと比べてインスタンス変数へのアクセスが高速にできる
- エラー出力が丁寧
- attributeを多用する
Object::InsideOutでクラスを作る場合は、以下のようになります。
package Point { use Object::InsideOut; my @y_fields : Field # InsideOutオブジェクトのインスタンス変数格納に使用し、この配列に登録されたインスタンス変数の開放処理は自動でされるようになる : Type('Numeric') # 数値しか受け付けない : Accessor('y'); # 'y' という名前の読み書き可能なアクセサを生成 my @x_fields : Field : Type('Numeric') : Accessor('x'); # コンストラクタでどのような引数を受け付けるかの設定 my %init_args : InitArgs = ( x => +{ Mandatory => 1, # 必須の引数 Type => 'Numeric', # 数値しか受け付けない }, y => +{ Mandatory => 1, Type => 'Numeric', }, ); # オブジェクトの初期化 sub init : Init { my ($self, $args) = @_; $self->set( \@x_fields, $args->{x} ); $self->set( \@y_fields, $args->{y} ); } } my $p = Point->new( x => 10, y => 20, );
コードをMooseに匹敵するほど多機能なことがなんとなくわかるかと思います。
しかしattributeを多用しているため、Perlに詳しくない人にはとっつきにくそうです。
また、PSGIアプリやmod_perlでの利用に不安がありそうです。(ドキュメントを見た限りではmod_perl上での実行はサポートしているとのことでした。)
CPAN上にあるモジュールでどれほど利用されているか調べたかったのですが、不具合で調べられないようでした。
Class::InsideOut
Class::InsideOutの特徴は、
- メモリ解放を自動でやってくれる
- コンストラクタ、アクセサの自動生成機能を提供
- 継承ツリーを汚さない
- Storableでのシリアライズをサポート
- attributeを使うクラスビルダー(Object::InsideOutなど)とは違って、PSGIやmod_perlアプリでもきちんと動作する
Class::InsideOutでクラスを作る場合は、以下のようになります。
package Point { use Class::InsideOut qw( new public readonly private ); # read / write 可能なアクセサの生成し, このハッシュに登録されたインスタンスのインスタンス変数の開放処理は自動でされる public x => my %x_fields; # read only なアクセサの生成し, このハッシュに登録されたインスタンスのインスタンス変数の開放処理は自動でされる readonly y => my %y_fields; # アクセサは生成せずに、インスタンスのインスタンス変数を保持するだけのハッシュを作ることもできる # private memo => my %memo_fields; } my $p = Point->new( x => 10, y => 20, )
Mooseなどのように、普通の関数でアクセサの生成を行ったりしているので、コードはだいぶ読みやすいなと思いました。
CPAN上にあるモジュールでClass::InsideOutに依存しているモジュールの数は23個のようです。
(Moose|Moo)X::InsideOut
Moose, あるいはMooのバックエンドでInsideOutクラスを使うようにするモジュールもあります。
使い方は簡単で、(Moose|Moo)をuseしたあとに(Moose|Moo)X::InsideOutをuseするだけです。
次のコードはMooでの例です。
package Point { use Moo; use MooX::InsideOut; has x => ( is => 'ro', required => 1, ); has y => ( is => 'ro', required => 1, ); __PACKAGE__->meta->make_immutable; } my $p = Point->new( x => 10, y => 20, )
簡単にInsideOutクラスによるカプセル化が有効になり、かつMoose系のモジュールの機能が利用できてとても良さそうです。
しかし、インスタンス変数に相当する値が our
で宣言されたハッシュに入っているため、外部からインスタンス変数を操作することができてしまいます。
Mooだと、具体的には以下のようにしてカプセル化を破ることができます。
my $p = Point->new(x => 1, y => 4); say $p->x; # -> 1 $MooX::InsideOut::Role::GenerateAccessor::FIELDS{$p}->{x} = 100; say $p->x; # -> 100
なぜ our
にしてあるのかはわからないですが、個人的にはせっかくのカプセル化の利点がなくなってしまっているので使う理由はあまりないのでは、という気がしています。
とはいえハッシュベースクラスのインスタンスほどカジュアルにカプセル化に違反することはできませんし、そもそものちゃんとしたInsideOutクラスでも強引にカプセル化を破ることはできなくもないので、これを利用するかどうかは状況によって判断が変わってきそうです。
なお、CPAN上にあるモジュールでMoox::InsideOutに依存しているモジュールの数は3個で、MooseX::InsideOutに依存しているモジュールの数は不明です。
その他
- Hash::FieldHash
- id:gfx 氏によって高速化&シンプルにされた Hash::Util::FieldHash のようなモジュールです
- Class::Std
- Damian Conway氏作のPerl Best Practiceで利用を推奨していたクラスビルダーです
- attributeを利用してアクセサやインスタンス変数格納用のハッシュを作ります
- 昔流行していたためか、けっこうClass::Stdに依存しているモジュールは多くて、55個ありました
- Dios
- これもDamian Conway氏作のInsideOutクラスのクラスビルダーです
- これを利用して書かれたクラスの外見はまるでPerl6のクラスのようになります
- 実験的なモジュールのようで、残念なことに最近のバージョンのPerlでは動かないようです。。
おわりに
InsideOutクラスのクラスビルダーはたくさん種類があるのですが、やはり広く利用はされていないんだなあ、と感じました。
個人的には、どうしてもInsideOutクラスを作りたい場合、読みやすくてかつ機能がそろっているClass::InsideOutを使うのが良さそうと思いました。
コアモジュールにInsideOutクラスの作成を支援するモジュールがあったのは意外でした。
7日目の担当は yunagi さんです。お楽しみに。