abstractとinterfaceの違いを自信持って使い分けてる?
abstractとinterfaceの言語仕様はよく似てる。とても混同しやすい。PHPで自身をもって使い分けている人はどれくらいいるかな?少なくとも自分は全部abstractでもいいと思っている(いた)。でも、interfaceはオブジェクト指向できっと重要な概念だろうから、しっかり区別したいな。Twitterでオブジェクト指向に詳しそうな人に聞きいたり、ググったりして調べて自分なりに理解した内容をまとめてみるよ。
似てる点
- そのままでは呼び出せない。abstractはextends、interfaceはimplementsしてはじめて使える。
- 抽象メソッドを定義できる(see code.1)。ちなみにそれらは子クラスで必ず実装しないといけない。
// code.1
abstract class AbstractFoo
{
abstract public function bar();
}
interface InterfaceFoo
{
public function bar();
}
異なる点
- classはabstractを1つだけextendsできるが、classはinterfaceを複数implementsできる(see code.2)。
- abstractは具体的な実装があるメソッドを持てる。interfaceは単に抽象メソッドを列挙するだけ(see code.3)。
// code.2
class Hoge extends AbstractFoo
{
}
class Hoge implements InterfaceFoo, InterfaceFoo2, InterfaceFoo3
{
}
// code.3
abstract class AbstractFoo
{
abstract public function bar();
public function baz()
{
echo 'baz!';
}
}
interface InterfaceFoo
{
public function bar();
public function baz();
}
この言語仕様を踏まえてどう使い分けるか?
「interfaceを使うくらいなら、abstract一辺倒でよくない?いざとなったらabstractに具体的な実装もできるしね」と思っている人はきっと自分だけじゃないと思う。ここではabstractじゃなくてinterfaceを使ったほうがいい、って例を説明していくよ。だから、interfaceのほうにフォーカスした内容になってる。読者はabstractの使い方をよく知っているし、よく使っているという前提ね。
仕様書はinterfaceにできるかも
interfaceは具体的な実装を持てない。これは設計書として利用できるかも知れない。今までドキュメントに書いていたクラスの仕様はinterfaceにすることができるってこと。例えば、こんな仕様書があったとする。
仕様書
- データベースクライアントクラスは次の4つのメソッドを提供しなければなりません。
- 書き込みメソッド。メソッド名: create()。引数は保存するデータ(連想配列)。
- 読み込みメソッド。メソッド名: load()。引数は読み込む行のID(整数)。
- 上書きメソッド。メソッド名: update()。引数は上書きする行のID(整数)と保存するデータ(連想配列)。
- 削除メソッド。メソッド名: delete()。引数は削除する行のID(整数)。
文書の仕様書を否定するつもりはないけど、人の言葉で書かれた仕様書は曖昧だったりする。上の仕様書でも、戻り値がよくわかんないし、update()の引数の順番は曖昧だし、引数は必須なのか任意なのか分かんない。
紙の仕様書は、メンテナンスされないこともあるよね。仕様書がなくともプログラムは正常に動作するから。もし、仕様書がプログラムに組み込まれたらどうだろう?仕様書がメンテナンスされなければプログラムが動かないので、仕様書をメンテするようになるよね、きっと。
上の仕様書は、interfaceに翻訳することができるんだ(code.4)。下の例のほうが文書の仕様書より曖昧性が低いよね。それに、仕様変更があったときもエラーとして気が付きやすい。例えば、複数行を取得したくなって、メソッドfind(array $condition)
を追加したとする。find()を実装していないサブクラスはエラーになるんだ。
// code.4
interface DatabaseClient
{
/**
* テーブルに行を追加する
* @returns bool 保存に成功したらtrue、失敗したらfalse
*/
public function create(array $data);
/**
* テーブルの行のデータを取得する
* @returns mixed 読み込みに成功したら配列、失敗したらfalse
*/
public function load($id);
/**
* テーブルの行を更新する
* @returns bool 保存に成功したらtrue、失敗したらfalse
*/
public function update($id, array $data);
/**
* テーブルの行を削除する
* @returns bool 削除に成功したらtrue、失敗したらfalse
*/
public function delete($id);
}
これはabstractでも同じような書き方ができるね。でも、具体的な実装を持たないならinterfaceにしたほうがいいかもしれないよ。interfaceは実装を持てない。これって、シンプルという長所でもあるよね。仕様を提供する側のコストがミニマムになる。それに、プログラマは最低限のリードタイムで「ここだけ実装しておけばいいのね」ということが分かるようになる。abstractだったら、そのクラスの内部実装にまで目を配らないといけないよね。つまり、interfaceのほうが設計者とプログラマの両者にとってストレスが少ないってことになるね。(ひとりで開発する場合は別かもしれないけど)
呼び出し方は同じだけど、内部の処理は全然ちがう場合はinterface
最近のウェブアプリはログインするものが多いよね。内部的なこと言ったら認証処理だね。認証処理ってのは、アプリ側が「あなたはだれ?」が特定できればよくて、それ以外の関心はない処理なんだけど、一方で、認証処理は様々なものがある。アプリ独自の認証方式、OpenID、LDAP、Basic認証、Twitter認証、Facebook認証、社内独自認証などなど。
このケースだと、interfaceを活用できる。ウェブアプリは、認証方法の詳細について口出ししない。その代わり、「interfaceに実装してほしいauthorize()っていうメソッドを定義しておいたよ。」っていう約束だけ決めて(code.5)、「うちはそれだけを呼び出すから、あとはうまいことやってね」と具体的な認証方法は各認証オブジェクトにまるなげする。
// code.5
interface Auth
{
/**
* 認証処理を行う
* @returns bool 成功したらtrue、失敗したらfalseを返す
*/
public function authorize();
}
それで、各認証方式を受け持つオブジェクトは、authorize()を独自に実装する(code.6)。
// code.6
class OpenID implements Auth
{
public function authorize()
{
if ( $this->_openIdAuth() )
{
return true;
}
return false;
}
protected function _openIdAuth()
{
// オープンIDで認証する処理
}
}
この例では、abstractはほとんど役に立たないよね。だって、ウェブアプリは認証方式の詳細に口出し(具体的な実装を提供)することができないから。各認証処理は全く異なった実装になるだろうね。具体的な実装の共通化は容易じゃない。無理して共通化に心を砕くくらいなら、interfaceにしたほうがいさぎよい、と思う。
まとめ
今回、調べていて気づいた点をまとめると、interfaceの出番は次のようになる。もっと、勉強してinterfaceを上手く使えるようになりたいな。
- 仕様書はinterfaceにできるかも
- 呼び出し方は同じだけど、内部の処理は全然ちがう場合はinterface