NAME

perlootut - Perl でのオブジェクト指向プログラミングのチュートリアル

DATE

この文書は 2011 年 2 月に作成され、最後に大きく見直されたのは 2013 年 2 月です。

もし将来これを読んでいる場合、最新版は変更されている可能性があります。 このバージョンではなく、Perl の最新安定リリースの perlootut 文書を 読むことから始めることを勧めます。

DESCRIPTION

この文書は Perl でのオブジェクト指向プログラミングを紹介します。 これはオブジェクト指向設計の背後にあるコンセプトの概説から始めます。 それから Perl が提供するものの上に構築されている、 CPAN にあるいくつかの OO システムを紹介します。

デフォルトでは、Perl の組み込みの OO システムはとても最小限です; ほとんどの 作業を自分でする必要があります。 この最小限主義は 1994 年には十分意味のあるものでしたが、Perl 5.0 から 時が経つにつれて、Perl OO に多くの共通のパターンが見られるようになりました。 幸い、Perl の柔軟性により、Perl OO システムの豊かなエコシステムが 発展しました。

裏で Perl OO がどのように動作するのかを知りたい場合は、perlobj 文書が 本質的な詳細について説明しています。

この文書は、Perl の文法、変数型、演算子、サブルーチン呼び出しの基本について 既に理解していると仮定しています。 まだこれらのコンセプトについてまだ理解していないなら、まず perlintro を 読んでください。 perlsyn, perlop, perlsub 文書も読むべきでしょう。

オブジェクト指向の基本

ほとんどのオブジェクトシステムは多くの共通の概念を共有しています。 おそらく以前に「クラス」、「オブジェクト」、「メソッド」、属性といった 用語について聞いたことがあるでしょう。 概念の理解は、オブジェクト指向のコードを読み書きするよりも遥かに容易です。 既にこれらの用語に親しんでいても、この章に目を通すべきです; なぜなら Perl の OO 実装の用語でそれぞれの概念を説明しているからです。

Perl の OO システムはクラスベースです。 クラスベース OO はかなり一般的です。 これは Java, C++, C#, Python, Ruby およびその他の多くの言語で使われています。 その他のオブジェクト指向パラダイムもあります。 JavaScript は、その他のパラダイムを使っている最も有名な言語です。 JavaScript の OO システムはプロトタイプベースです。

オブジェクト

オブジェクト は、データと、そのデータを操作するサブルーチンを一つに まとめたデータ構造です。 オブジェクトのデータは 属性 と呼ばれ、サブルーチンは メソッド と 呼ばれます。 オブジェクトは名詞と考えることができます (人、web サービス、 コンピュータなど)。

オブジェクトは単一のものを表現します。 たとえば、あるオブジェクトがファイルを表現しているとします。 ファイルオブジェクトの属性はパス、内容、最終更新時刻といったものになります。 オブジェクトのパスが "/etc/hostname" になるような、 "foo.example.com" という名前のマシンの /etc/hostname を表現する オブジェクトを作る場合、その内容は "foo\n" で、最終更新時刻は紀元から 1304974868 秒といったものになります。

ファイルに結びつけられたメソッドは rename()write() と いったものになります。

Perl ではほとんどのオブジェクトはハッシュですが、推奨する OO システムは これについて気にする必要がないようにします。 実際には、オブジェクトの内部構造は不透明であると考えるのが最良です。

クラス

クラス はあるカテゴリのオブジェクトの振る舞いを定義します。 クラスは("File" のような)カテゴリを表す名前です; またクラスは そのカテゴリのオブジェクトの振る舞いを定義します。

全てのオブジェクトは何らかのクラスに属します。 例えば、/etc/hostname オブジェクトは File クラスに属します。 特定のオブジェクトを作りたい場合、そのクラスから始めて、オブジェクトを 構築(construct) または インスタンス化 (instantiate) します。 特定のオブジェクトはしばしばクラスの インスタンス (instance) と 呼ばれます。

Perl では、どのパッケージもクラスになれます。 クラスであるパッケージとそうでないパッケージの違いは、パッケージがどのように 使われるかによります。 以下は File クラスのための「クラス宣言」です:

  package File;

Perl では、オブジェクトの構築のための特別なキーワードはありません。 しかし、CPAN のほとんどの OO モジュールは、新しいオブジェクトの構築に new() という名前のメソッドを使います:

  my $hostname = File->new(
      path          => '/etc/hostname',
      content       => "foo\n",
      last_mod_time => 1304974868,
  );

(-> 演算子について心配しないでください; 後で説明します。)

bless

既に述べたように、ほとんどの Perl オブジェクトはハッシュですが、 オブジェクトはどの Perl データ型 (スカラ、配列など)のインスタンスも可能です。 普通のデータ構造のオブジェクトへの変換は、データ構造に Perl の bless 関数を 使うことによる bless によって行われます。

一からオブジェクトを構築しないことを強く勧めますが、bless という 用語は知っておくべきです。 bless された データ構造 (またの名を「リファレンス先」)はオブジェクトです。 時々、オブジェクトは「クラスに bless された」と表現します。

一旦リファレンス先が bless されると、Scalar::Util コアモジュールの blessed 関数はそのクラス名を返します。 このサブルーチンは、オブジェクトが和されるとオブジェクトのクラスを返し、 さもなければ偽を返します。

  use Scalar::Util 'blessed';

  print blessed($hash);      # undef
  print blessed($hostname);  # File

コンストラクタ

コンストラクタ は新しいオブジェクトを作成します。 コンストラクタのための文法を提供しているその他の言語と異なり、Perl では クラスのコンストラクタは単なるメソッドです。 ほとんどの Perl クラスはコンストラクタの名前として new を使います:

  my $file = File->new(...);

メソッド

オブジェクトを操作するサブルーチンが メソッド であるということは 既に学んでいます。 メソッドはオブジェクトが する ことと考えることができます。 オブジェクトが名詞なら、メソッドは動詞 (保存する、表示する、開く) です。

Perl では、メソッドは単にクラスのパッケージにあるサブルーチンです。 メソッドは常に最初の引数としてオブジェクトを受け取るように書かれます:

  sub print_info {
      my $self = shift;

      print "This file is at ", $self->path, "\n";
  }

  $file->print_info;
  # The file is at /etc/hostname

メソッドを特別なものにしているのは、どのように呼び出されるか です。 矢印演算子 (->) は、メソッドとして呼び出していることを Perl に 知らせます。

メソッド呼び出しをするとき、Perl はメソッドの 呼び出し元 (invocant) を 最初の引数として渡すように用意します。 to be passed as the first argument. 呼び出し元 とは矢印の左側にあるものの名前です。 呼び出し元はクラス名とオブジェクトのどちらかです。 また、メソッドに追加の引数も渡せます:

  sub print_info {
      my $self   = shift;
      my $prefix = shift // "This file is at ";

      print $prefix, ", ", $self->path, "\n";
  }

  $file->print_info("The file is located at ");
  # The file is located at /etc/hostname

属性

各クラスは 属性 を定義します。 オブジェクトをインスタンス化するとき、これらの属性に値を代入します。 例えば、各 File オブジェクトはパスを持ちます。 属性は時々 プロパティ (properties) と呼ばれます。

Perl には属性のための特別な文法はありません。 内部では、属性はしばしばオブジェクトの基となっているハッシュのキーとして 保管されますが、これについて気にすることはありません。

属性には アクセサ (accessor) メソッドを経由してのみアクセスすることを 勧めます。 これはそれぞれの属性の値を取得または設定するメソッドです。 既に、前述した print_info() の例で、$self->path を 呼び出しているのを見ています。

ゲッター (getter) と セッター (setter) という用語も見るかも知れません。 これらはアクセサの種類です。 ゲッターは属性の値を取得し、セッターは設定します。 セッターに関する別名は ミューテータ (mutator) です。

属性は典型的には読み込み専用または読み書き可能として定義されます。 読み込み専用属性はオブジェクトが最初に作成されるときにのみ設定でき、 読み書き可能属性はいつでも変更できます。

属性の値それ自身が他のオブジェクトかも知れません。 例えば、最終更新時刻を数値として返す代わりに、File クラスはその値を 表現する DateTime オブジェクトを返すかも知れません。

設定可能な属性が一切公開されていないクラスも可能です。 全てのクラスが属性とメソッドを持っているというわけではありません。

多態性

多態性 (polymorphism) は二つの異なるクラスが API を共有しているということを 示す変わった方法です。 例えば、どちらも print_content() メソッドを持つ、File クラスと WebPage クラスを持てます。 このメソッドはクラスごとに異なった出力を生成するかも知れませんが、共通の インターフェースを共有します。

二つのクラスはいろいろな意味で異なっているかも知れませんが、 print_content() メソッドを呼び出すときには、これらは同じです。 これは、どちらのクラスのオブジェクトに対しても print_content() を 呼び出そうとすることができて、オブジェクトがどのクラスに属しているかを 知る必要がないと言うことです!

多態性はオブジェクト指向設計の鍵となる概念の一つです。

継承

継承 は既にあるクラスの特殊版を作成できるようにします。 継承は他のクラスのメソッドと属性を再利用して新しいクラスを 作成できるようにします。

例えば、File から 継承 した File::MP3 クラスを作成できます。 File::MP3 (is-a) Fileより特殊な 型です。 全ての mp3 ファイルはファイルですが、全てのファイルが mp3 ファイルというわけではありません。.

継承関係はしばしば 親-子 または スーパークラス/サブクラス 関係として 参照されます。 子は親クラスとの is-a 関係を持つと表現することがあります。

FileFile::MP3スーパークラス で、File::MP3Fileサブクラス です。

  package File::MP3;

  use parent 'File';

parent モジュールは、Perl で継承関係を定義するいくつかの方法のひとつです。

Perl は多重継承を認めています; つまり、クラスは複数の親から継承できます。 これは可能ですが、行わないように強く勧めます。 一般的に、多重継承で行えることは全て、ロール を使うことでより きれいな形で行えます。

あるクラスに対して複数のサブクラスを定義することは何も悪くないことに 注意してください。 これは一般的でかつ安全です。 例えば、mp3 ファイルの異なった種類を区別するために、 File::MP3::FixedBitrate クラスと File::MP3::VariableBitrate クラスを 定義できます。

メソッドのオーバーライドとメソッド解決

継承は二つのクラスでコードを共有できるようにします。 デフォルトでは、親クラスの全てのメソッドは子でも利用可能です。 子は、独自の実装を提供することで親のメソッドを明示的に オーバーライド (override) できます。 例えば、File::MP3 オブジェクトがあれば、File からの print_info() メソッドがあります:

  my $cage = File::MP3->new(
      path          => 'mp3s/My-Body-Is-a-Cage.mp3',
      content       => $mp3_data,
      last_mod_time => 1304974868,
      title         => 'My Body Is a Cage',
  );

  $cage->print_info;
  # The file is at mp3s/My-Body-Is-a-Cage.mp3

返り値に mp3 のタイトルを含めたい場合、メソッドをオーバーライドできます:

  package File::MP3;

  use parent 'File';

  sub print_info {
      my $self = shift;

      print "This file is at ", $self->path, "\n";
      print "Its title is ", $self->title, "\n";
  }

  $cage->print_info;
  # The file is at mp3s/My-Body-Is-a-Cage.mp3
  # Its title is My Body Is a Cage

どのメソッドが使われるべきかを決定する処理は、メソッド解決 (method resolution) と呼ばれます。 Perl が行うことは、まずオブジェクトのクラス (この場合は File::MP3) を 見ます。 このクラスにメソッドが定義されていれば、そのクラスのメソッドが呼び出されます。 さもなければ、Perl は順番に親クラスを見ます。 File::MP3 の場合、唯一の親は File です。 File::MP3 がメソッドを定義しておらず、File が定義しているなら、 Perl は File のメソッドを呼び出します。

FileDataSource から継承され、これが Thing から継承されていれば、 Perl はもし必要なら「チェーンをたどって」探し続けます。

子から親メソッドを明示的に呼び出すことは可能です:

  package File::MP3;

  use parent 'File';

  sub print_info {
      my $self = shift;

      $self->SUPER::print_info();
      print "Its title is ", $self->title, "\n";
  }

SUPER:: は、File::MP3 クラスの継承チェーンから print_info() を 探すように Perl に伝えます。 このメソッドを実装している親クラスが見つかると、そのメソッドが 呼び出されます。

以前に多重継承について述べました。 多重継承の主な問題は、メソッド解決が非常に込み入っているということです。 さらなる詳細については perlobj を参照してください。

カプセル化

カプセル化 は、オブジェクトを不透明にする考え方です。 他の開発者があなたのクラスを使うとき、どのように 実装されているかを 知る必要はなく、単に 何を するかを知る必要があるだけです。

カプセル化はいくつかの理由で重要です。 まず、これにより公的な API を内部の実装から分離できます。 これにより、API を壊すことなく実装を変更できます。

次に、クラスが十分にカプセル化されていれば、サブクラス化が容易になります。 理想的には、サブクラスは親クラスが使っているオブジェクトデータに アクセスするのに同じ API を使います。 実際には、サブクラス化は時々カプセル化に違反しますが、よい API は その必要性を最小限にします。

既に、ほとんどの Perl オブジェクトは内部ではハッシュとして実装されていると 述べました。 カプセル化の原則は、このことに依存するべきではないと伝えています。 代わりに、ハッシュのデータにアクセスするためにアクセサメソッドを使います。 後に推奨するオブジェクトシステムは全てアクセサメソッドの生成を自動化します。 これらの一つを使うなら、オブジェクトをハッシュとして直接アクセスする必要は ないはずです。

包含

オブジェクト指向のコードでは、しばしばあるオブジェクトが他のオブジェクトを 参照しています。 これは 包含 (composition)、または has-a 関係と呼ばれます。

既に、File クラスの last_mod_time アクセサが DateTime オブジェクトを 返すかも知れないことに触れました。 これは包含の完璧な例です。 さらに進めて、pathcontent アクセサもオブジェクトを返すようにも できます。 そして File クラスはいくつかのその他のオブジェクトの 包含 になります。

ロール

ロール は、クラスが 何か ではなく、クラスが 何をするか です。 ロールは Perl では比較的新しいですが、かなり人気になってきています。 ロールはクラスに 適用 されます。 クラスがロールを 消費 (consume) するということもあります。

ロールは多態性を提供するための継承の代替策です。 二つのクラス RadioComputer があると考えます。 どちらもオン/オフするスイッチがあります。 これをクラス定義でモデル化したいとします。

Machine のように、共通の親から両方のクラスを継承することもできますが、 全ての機械にオン/オフスイッチがあるわけではありません。 HasOnOffSwitch と呼ばれる親クラスを作ることもできますが、とても 不自然です。 ラジオとコンピュータはこの親の特殊化ではありません。 この親は実際にはかなりおかしなものです。

これはロールの出番です。 HasOnOffSwitch ロールを作って両方のクラスに適用することは とても合理的です。 このロールは turn_on()turn_off() メソッドを提供するような 既知の API を定義します。

Perl にはロールを記述する組み込みの方法はありません。 以前は、人々は我慢して多重継承を使っていました。 最近は、ロールを使うためのいくつかの良い選択肢が CPAN にあります。

いつ OO を使うか

オブジェクト指向は全ての問題に対する最良の解法というわけではありません。 Perl Best Practices (copyright 2004, Published by O'Reilly Media, Inc.) において、Damian Conway は OO が問題に正しく適応するかどうかを決定するときに 使う基準の一覧を提供しています:

Perl の OO システム

前述したように、Perl の組み込みの OO システムは非常に最小限ですが、 一方とても柔軟です。 年を重ねるにつれて、多くの人々がより多くの機能と利便性を提供するために Perl の組み込みのシステムの上に構築されたシステムを開発しました。

私たちはこれらのシステムの一つを使うことを強く勧めます。 それらの中の最小限のものでも多くの繰り返される定型文を排除できます。 Perl でクラスを一から書く本当にいい理由というものはありません。

これらのシステムの基礎となる内部に興味があるなら、 perlobj を 調べてください。

Moose

Moose は自分自身を「Perl 5 のためのポストモダンオブジェクトシステム」と 宣伝しています。 怖がらなくても大丈夫です; 「ポストモダン」というのはラリーが Perl のことを 「最初のポストモダンコンピュータ言語」と説明したことにちなんでいます。

Moose は完全でモダンな OO システムを提供します。 その最大の影響元は Common Lisp Object System ですが、Smalltalk およびその他の いくつかの言語からもアイデアを借りています。 Moose は Stevan Little によって作成され、彼の Raku OO 設計に関する 作業から多くを引いています。

以下は Moose を使った File クラスです:

  package File;
  use Moose;

  has path          => ( is => 'ro' );
  has content       => ( is => 'ro' );
  has last_mod_time => ( is => 'ro' );

  sub print_info {
      my $self = shift;

      print "This file is at ", $self->path, "\n";
  }

Moose は多くの機能を提供します:

もちろん、Moose は完璧ではありません。

Moose によってコードの読み込みが遅くなることがあります。 Moose 自身が小さくなく、またクラスを定義するときに 大量 の コードを生成します。 このコード生成は、実行時コードは可能な限り高速であることを意味しますが、 このためにモジュールが最初に読み込まれるときにコストを支払うことになります。

この読み込み時間は、実行時に毎回読み込みしなければならない コマンドラインスクリプトや「何の変哲もない」CGIスクリプトのように、 起動速度が重要な場合には問題になり得ます。

うろたえる前に、多くの人々がコマンドラインツールやその他の起動時間が重要な コードに Moose を使っていることを知ってください。 起動速度について気にする前にまず Moose を試してみることを勧めます。

Moose はまた他のモジュールに対していくつかの依存があります。 それらのほとんどは小さいスタンドアロンのモジュールで、いくつかは Moose から切り離されたものです。 Moose 自身と、その依存のいくつかは、コンパイラを必要とします。 コンパイラのないシステムにインストールする必要があったり、どんな 依存があっても問題がある場合は、Moose は適切ではないかも知れません。

Moo

Moose を試してみて、これらの理由の一つが Moose を使うのを 妨げているなら、次に Moo を考慮するのを勧めます。 MooMoose の機能のサブセットをより単純なパッケージで実装しています。 実装されているほとんどの機能について、エンドユーザー API は Moose同一 です; つまり Moo から Moose にとても簡単に 切り替えられるということです。

MooMoose のイントロスペクション API を実装しておらず、従って モジュールを読み込むときはしばしばより速いです。 さらに、XS が必要な依存はないので、コンパイラのないマシンでも インストールできます。

Moo の最も切実な機能は Moose との相互運用性です。 Moose のイントロスペクション API を Moo のクラスやロールで使おうと した場合、透過的に Moose のクラスやロールに変換されます。 これにより、Moo を使ったコードと Moose を使ったコードベースに組み込む、 またはその逆を行うことがより容易になります。

例えば、ある Moose のクラスは、extends を使った Moo のクラスの サブクラスになったり、with を使った Moo のロールを消費したりできます。

Moose の作者は、いつの日か Moose が十分改良されることによって Moo が古いものになることを望んでいますが、今のところは Moose に対する 価値のある代替を提供します。

Class::Accessor

Class::AccessorMoose の対極です。 非常に少ない機能しか提供しませんし、セルフホスティングでもありません。

しかし、とても単純で、ピュア Perl で、非コア依存はありません。 また、対応している機能に対する「Moose 風」 API も提供しています。

多くのことはしませんが、それでもクラスを一から書くよりは望ましいです。

以下に Class::Accessor を使った File クラスを示します:

  package File;
  use Class::Accessor 'antlers';

  has path          => ( is => 'ro' );
  has content       => ( is => 'ro' );
  has last_mod_time => ( is => 'ro' );

  sub print_info {
      my $self = shift;

      print "This file is at ", $self->path, "\n";
  }

antlers インポートフラグは、属性を Moose 風の文法で定義したいことを Class::Accessor に伝えます。 has に渡せる唯一の引数は is です。 Class::Accessor を選択した場合は、この Moose 風の文法を使うことを勧めます; なぜなら後に Moose に移行しようと決めたときによりスムーズに アップグレードできるからです。

Moose と同様、Class::Accessor はアクセサメソッドとコンストラクタを 生成します。

Class::Tiny

最後に、Class::Tiny があります。 このモジュールはまさにその名前通りです。 これは非常に最小限の API のみを持ち、最近の Perl では全く依存はありません。 それでも、一から独自の OO コードを書くよりも遙かに簡単に使えます。

以下に File クラスをもう一度示します:

  package File;
  use Class::Tiny qw( path content last_mod_time );

  sub print_info {
      my $self = shift;

      print "This file is at ", $self->path, "\n";
  }

これだけです!

Class::Tiny では、全てのアクセサは読み書き可能です。 コンストラクタと、定義したアクセサを生成します。

Moose 風の文法の Class::Tiny::Antlers も使えます。

Role::Tiny

前述したように、ロールは継承の代替策を提供しますが、Perl には組み込みの ロール対応はありません。 Moose を使うことを選んだ場合、完全なロール実装が同梱されています。 しかし、その他の推奨 OO モジュールを使う場合、Role::Tiny で ロールを使えます。

Role::Tiny は Moose のロールシステムと同じような機能を提供しますが、 パッケージは遙かに小さいです。 特に、属性宣言には対応していないので、手動で行う必要があります。 それでも、これは有用で、Class::Accessor および Class::Tiny と うまく動作します。

OO システムの要約

以下はここで取り上げた選択肢の簡潔なまとめです:

その他の OO システム

ここで扱ったものの他に、CPAN には文字通り数十のその他の OO 関連の モジュールがあり、他の人のコードを動かすときにはおそらく それらのいくつかを使っているでしょう。

さらに、世の中の多くのコードは全ての OO を「手動で」、単に Perl 組み込みの OO 機能を使って行っています。 もしそのようなコードを保守する必要があるなら、Perl の組み込みの OO が どのように動作するのかを正確に理解するためにperlobj を読むべきです。

まとめ

既に述べたように、Perl の最小限の OO システムは CPAN での豊富な OO システムを 生み出しました。 今でもこの地金まで降りていってクラスを手で書くこともできる一方、 モダン Perl においてそうする理由は本当はありません。

小さいシステムなら、Class::TinyClass::Accessor の両方は 基本的な定型文の面倒を見る最小限のオブジェクトシステムを提供します。

より大きいプロジェクトなら、Moose はあなたがビジネスロジックの実装に 集中できるような豊富な機能セットを提供します。 多くの機能が必要だけれどもより早いコンパイル時間が必要だったり XS を避けたい場合、MooMoose のよい代替案です。

どの OO システムが適しているかを見るために、Moose, Moo, Class::Accessor, Class::Tiny を試して評価することをお勧めします。