「PHPで、どのようにオブジェクト指向プログラミングをしていくか」を解説する連載。今回は、PHPにおけるインターフェースと抽象クラスの概要や使い方、両者の違い、多重継承、トレイトについて解説します。
「PHPで、どのようにオブジェクト指向プログラミングをしていくか」を解説する本連載。今までは、下記のように、PHPを例とした、オブジェクト指向について学んできました。
前回の「便利だけど使いどころが難しいPHPの代表的なマジックメソッドと無名関数の使い方」では、PHPに固有(に近い)仕様である、「マジックメソッド」について説明しました。今回は、再び「比較的どの言語においても通用しやすい」お話である、インターフェースと抽象クラス、および多重継承についてあらためて学んでいきましょう。
インターフェースは英語で「interface」と書きますが、英語本来の意味としては「二者間の接点/接続面」「接触や連絡」といった意味です。
「ユーザーインターフェース(UI)」という言葉は割と一般的に使われているように思いますが、これは「コンピューターとユーザーという二者の間にある、情報をやりとりするための決まりごと(接点)」というような意味です。
OOP(object-oriented programming:オブジェクト指向プログラミング)におけるインターフェースは、おおむね「そのインスタンスは必ず、このメソッドを持っていることを約束します」という感じで使われることが多いです。
まずは実装を確認してみましょう。PHPにおけるインターフェースの宣言はリスト1のような形式で書きます。
<?php // interface hoge { public function t1(); }
インターフェースで記述できるのは「publicなfunctionのみ」です。プロパティは書けませんし、メソッドにしても、protectedやprivateなものは書くことができません。また、「実装」を書くことはできず、あくまでも「このメソッドがあるよ」という「宣言」のみの記述です。
インターフェースを使うときは、クラスの継承と同じように記述しますが、「extends」ではなく、「implements」を使います。
<?php // interface hoge { public function t1(); } class foo implements hoge { public function t1() { echo "foo's t1()\n"; } } // $obj = new foo(); $obj->t1();
インターフェースのお話は、いったん置いておきまして、続いて「抽象クラス」を見ていきましょう。
抽象クラスは「抽象メソッドを含むクラス」です。抽象クラスを、interfaceと同じような内容で実装してみましょう。
PHPにおける抽象クラスの宣言は、リスト3のような形式で書きます。
<?php // abstract class hoge { abstract public function t1(); }
抽象クラスは「クラスの一種」なので、プロパティを含めることができます。また、抽象メソッドにおいては、publicのみではなくprotectedなメソッドに対して「abstract」をマークすることも可能です。ただし「abstract private」は記述できません。
<?php // abstract class hoge { abstract public function t1(); abstract protected function t2(); // private $i_; }
抽象クラスは、これまでの連載で紹介した「継承」と同様に「extends」キーワードで利用が可能です。
<?php // abstract class hoge { abstract public function t1(); abstract protected function t2(); // private $i_; } class foo extends hoge { public function t1() { echo "foo's t1()\n"; $this->t2(); } protected function t2() { echo "foo's t2()\n"; } } // $obj = new foo(); $obj->t1();
インターフェースと抽象クラスは、大変に「似ている」部分があります。まずはインターフェースと抽象クラスを、実装面から比較していきましょう。
どちらも「単体ではnewできない」という大きな制約があります。実際に試してみましょう。
<?php // interface hoge { public function t1(); } // abstract class foo { abstract public function t1(); } // $obj = new hoge(); // Fatal error: Cannot instantiate interface hoge $obj = new foo(); // Fatal error: Cannot instantiate abstract class foo
4行目にあるようにinterfaceで宣言できるのは「public function」のみです。また「部分的に実装をする」ようなことはできず、全て「宣言のみ」です。一方で抽象クラスは「通常のクラスで記述可能なあらゆるもの」が記述できるので部分的に実装することも可能なのと、抽象メソッドもpublic以外にprotectedが指定できます。
<?php // interface hoge { public function t1(); protected function t2(); // Fatal error: Access type for interface method hoge::t2() must be omitted public function t3() { // Fatal error: Interface function hoge::t3() cannot contain body } // private $i_; // Fatal error: Interfaces may not include member variables }
<?php // abstract class hoge { abstract public function t1(); abstract protected function t2(); public function t3() { } private $i_; }
このように、実装の面から見ると「インターフェースでできることは全て抽象クラスでもできる(後述しますが、インターフェースにしかできないことも実際にはあります)」一方で、「抽象クラスでできるが、インターフェースではできないこと」があります。
これだけを見ると「では抽象クラスばかりが使えて、インターフェースは実際には使われないのではないか?」という疑問が出てくると思います。では、本当に「インターフェースは実際には不要なもの」なのでしょうか?
インターフェースと抽象クラスを、実装ではなく「本来の目的」で見ると、その違いが見えてきます。
抽象クラスは本質的に「クラス」であり、その目的は「継承」です。抽象メソッドは「自分では実装しない(できない)けど、子クラスで実装しておいてください」という継承先での実装の制約ですが、同時に「親クラスで実装可能なものは実装しておきます」という、「実装の継承」を想定しているケースも多いです。
一方で、インターフェースが本質的に持っているのは「外部に対する保証と約束」です。
「このインターフェースをimplementsしている場合、必ずこのメソッドが(publicで/外に開いて)存在していることを約束します」という感じです。プログラムにおいて「型」という概念は幅広い意味と定義を含んでいます。
インスタンスの「型」が「このメソッド群を全て持っているかどうか?」という観点で考えられる場合(これを「ダックタイピング」と言います)、インターフェースは「型のみを定義できる記法である」と言うこともできるでしょう。
このように、インターフェースと抽象クラスの本質は「実装の継承の一手段なのか」「型の継承なのか」という、本質的なところでは「大きく異なっている」ことが分かります。
ただ、例えば典型的にはC++には「抽象のみで、インターフェースがない」ので、実装として「どちらを使うか」については、現場や今までの技術経歴によっても、大分と異なっているようです。
とはいえ、PHPではどちらも実装があるので、「実装の継承」であれば抽象クラスが、「型の継承(とメソッドの保証/約束)」であればインターフェースが、それぞれ「より適している」ので、そのように使うと食い違いが減るのではないか、と思います。
Copyright © ITmedia, Inc. All Rights Reserved.