PHPにおけるインターフェースと抽象クラス、多重継承、トレイトの使い方PHPオブジェクト指向プログラミング入門(5)(2/2 ページ)

» 2015年11月04日 05時00分 公開
[古庄道明株式会社格子組]
前のページへ 1|2       

「多重継承」とは?

 もう一つのお話をするための前提知識として、多重継承について学んでいきましょう。

 これまでの連載で紹介した継承は「単一継承」といって、「1つの親クラスを継承して別のクラスを作る」継承でした。継承には、もう一つ、親が複数ある「多重継承」というものがあります。

 では多重継承のコードを書いてみましょう。

<?php
//
class hoge {
}
class foo{
}
// hogeとfooを継承する
class bar extends hoge, foo {
}
リスト9

 リスト9を動かしてみると分かりますが「Parse error: syntax error, unexpected ',', expecting 」というエラーが出ます。PHPでは、多重継承自体は「文法レベルで」存在しないために、上述のコードを実際に動かすことはできません。

 多重継承は、「ほとんどの言語で実装されているもの」ではなく、比較的多くの言語で「実装されていない」機能です。実装されている言語としてはC++とPerlとPythonなど、逆に実装されていない言語としては、C#、Java、PHP、Rubyなどがあります。

 多重継承はいくつかの点で便利な一方で、問題の多い実装にもなりやすいのです。そのため、言語によって「便利であることに重点を置いて実装を許可している言語」と「危険性に重点を置いて実装ができないようにしている言語」があります。

 一方で「便利な側面」があるため、さまざまな方法で「デメリットを最小限に抑えた上で、メリットを享受したい」と考えて、さまざまな「新しい(あるいは改良された)概念」が実装されることも多いのです。

インターフェースの多重継承

 PHP(に限らず、Javaなどもそうですが)のインターフェースは、実は多重継承が可能です。ここでは「多重継承の問題の一つ」の理解と、その問題が「インターフェースでは発生しない」ことを学んでいきましょう。

クラスの多重継承の問題点

 多重継承の問題点に「名前の衝突」があります。

 実際には動きませんが、以下の疑似コードで考えてみましょう。

<?php
//
class hoge {
    public function test() {}
}
class foo {
    public function test() {}
}
//
class bar extends hoge, foo {
    public function test2() {
        $this->test();
    }
}
リスト10

 クラスbarは、クラスhogeとクラスfooを多重継承しています。クラスbar内にある「$this->test()」は、自身がtestメソッドを持っていないので「親クラスのメソッド」を呼びにいこうとしますが、親クラスは2つあり、その2つともが「testメソッド」を持っています。

 では、この場合「どちらの親クラスのtestメソッド」がcall呼ばれるのでしょうか?

 多重継承を許可している言語は、こういったケースについて「言語ごとにルールを持っている」ものですが、そのルールを知らない、失念したなどによって「面倒が起きる可能性」が十分に想起されます。そういった辺りは「多重継承をしない/させない」というお話で、割りと出てくる論調の一つです。

インターフェースの多重継承では「名前の衝突」でもエラーにならない

 しかし、この話は「どの実装がcallされるか」が前提になっているので。インターフェースのような「型の継承」である場合、問題が発生しなくなります。

 具体的なコードを見てみましょう。

<?php
//
interface hoge {
    public function t1();
    public function t2();
}
interface foo {
    public function t2();
    public function t3();
}
// hogeとfooを継承してみる
class bar implements hoge, foo {
    public function t1() {
        echo "bar's t1()\n";
    }
    public function t2() {
        echo "bar's t2()\n";
    }
    public function t3() {
        echo "bar's t3()\n";
    }
}
リスト11

 クラスbarは、hogeを実装しているため、約束として「メソッドt1()とメソッドt2()がある」ことを保証しています(4〜5行目)。一方で、クラスbarはfooを継承しているため、約束として「メソッドt2()とメソッドt3()がある」ことを保証しています(8〜9行目)。

 このように「インターフェースの保証」であるために、重複していたとしても、そこに問題は(設計を間違えていなければ)発生しないので、インターフェースの多重継承はあまり問題が起きないことが多く、結果的に「実装の多重継承は許可しないが、インターフェースの多重継承は許可する」としている言語が一定数あります。

4つのインターフェースを多重継承しているArrayObjectクラス

 この辺りの「インターフェースの多重継承」および、そこから見て取れる「実装の約束」は、例えば「Standard PHP Library(SPL)」の、ArrayObjectクラスが興味深いでしょう。

 ArrayObjectクラスは、IteratorAggregate、ArrayAccess、Serializable、Countableの4つのインターフェースを多重継承しています。そのため、このクラス(およびこのクラスを親に持つ子クラスたち)は全て下記の特徴(あるいは約束)を持ちます。

  • IteratorAggregateインターフェースがあるので、foreachを使用して中身をたどれることを約束している
  • ArrayAccessインターフェースがあるので、[]を使って配列のようにアクセスできることを約束している
  • Serializableインターフェースがあるので、独自のシリアライズ/アンシリアライズ(次回解説)が呼び出されることを約束している
  • Countableインターフェースがあるので、count()関数を使えることを約束している

 このようにして「インターフェースの多重継承」は、PHPで使うことができます。

「多重継承」の系譜から理解する「trait(トレイト)」

 「インターフェースの多重継承」によって、「型の継承」ができることは分かりました。しかし実際には「実装の継承」あるいは「実装の再利用」が、適切な設計に基づけば「便利である」こともまた事実です。

「is-a関係」と「has-a関係」

 ここで、連載第3回で紹介した「is-a関係」と対を成す「has-a関係」と、それを実装するための方法を学んでいきましょう。

 is-a関係は、連載第3回でもやった通り「B is a A」となり「BはA(の一種)である」という感じで用いられます。これは「明確な木構造/継承ツリー」です。

 一方で、「has-a関係」というものがあります。例えば「A has a B」で「Aの中にはBが含まれる」という意味になります。

 例えば、「酢豚 has a パイナップル」であったり「酢豚 has a 豚肉」だったりします(酢豚にパイナップルを入れることに関する是非についてはいったん置いておきます)。「酢豚 has a ピーマン」でもありますし「酢豚 has a タマネギ」でもあります。

 このように、まさに「Aの中にはBが含まれる」というのが、has-a関係です。is-aは「分類」であるのに対して、has-aは「分割/部品」です。

 「この集合体を構築している部品(の群れ)」というような意味で考えるとしっくり来るでしょう。

 例えば「酢豚 is a 中華料理」です。細かい話をすると「酢豚 is a 広東料理」で「広東料理 is a 中華料理」というような継承ツリーです。つまり「中華料理の中の一つに広東料理というジャンルがあり(ちなみに、それ以外に北京料理、上海料理、四川料理があります)」「広東料理の一つのメニューとして酢豚がある」ということです。

 これらは「継承ツリー」なので、is-a関係の継承によってきれいに記述できます。

class 中華料理 {
}
//
class 広東料理 extends 中華料理 {
}
//
class 酢豚 extends 広東料理 {
}
リスト12

 一方で、構成材料である肉やらピーマンなどは、このような継承ツリーに入れるのは難しいです。なぜなら、肉やピーマンは「酢豚でのみ」使われるわけではなく、さまざまな料理で使われているからです。また、そのさまざまな料理は「酢豚と木構造/継承ツリーな関係にあるレシピのみ」とは限りません。

 このように「肉やピーマンを継承ツリーの中に入れる」のは難しいのですが、一方で「肉は肉であちこちで使われる」ので、実装としては「切り出しておいて」再利用が可能な状態になっていると便利なことが多々あります(中華の技法ですと、例えば「油通し」などは、比較的レシピに依存しない“素材にひも付けておきたい”メソッドです)。

PHP 5.4で追加された「trait(トレイト)」

 「多重継承が可能な言語」であれば、「メソッド」として切り出して「そのメソッドを持つクラスを多重に継承する」でよいのですが、それができない言語では、別の手段を講じる必要がありました。そのためPHPでは、5.4から「trait(トレイト)」という機能が追加されました。

 実際に、traitを簡単に実装してみましょう。

<?php
// traitの宣言
trait hoge {
    public function ho() {
        echo "function ho\n";
    }
public $val_;
}
class foo {
    use hoge; // trait hogeを使う宣言
}
//
$obj = new foo();
$obj->ho();
$obj->val_ = 10;
var_dump($obj);
リスト13

 リスト13のように、メソッドもプロパティも「1つのtraitにまとめて」使うことができます。言い方を変えると「1つの部品」として、「trait」という単位で、切り出すことが可能です。

 実装の再利用などを考えるときに、基本的には「継承が用いられる」ことが多いですが、一度関係性を考えてみて、再利用したい実装(群)が「クラスに対して、is-aというよりはhas-aな関係」である場合、現場環境がPHP 5.4以降であれば、traitによる設計と実装を一つの選択肢として考えておくと、設計と実装の幅が広がることでしょう。

次回は、インスタンスの保存の仕方やコピーの仕方

 今回は、インターフェースと抽象クラス、多重継承に関連することを学んでいきました。次回の最終回はインスタンスの保存の仕方やコピーの仕方などを学びましょう。

筆者紹介

古庄道明

1970年浅草生まれ。1995年に富士通系のソフトハウスに就職しプログラマーに転身。1999年個人事業主として独立し現在に至る。「寺子屋」「格子組」といったエンジニア支援活動を独自に展開し、占い師時代の「ガルーダ」という占い師名にちなんだ「がる先生」の愛称で親しまれている。

コンサルティングからシステム設計、ネットワークにセキュリティと、守備範囲は比較的多岐にわたる。「技術の基本は、その技術がないときの“困ってる”が根っこ」をモットーに、古い話から現代へ歴史をたどるように教えるのが持ち味。


前のページへ 1|2       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。