PHPのinterfaceとは何か

Posted by Hiraku on 2013-10-14

久しぶりの更新です。最近、修行だと思って色々な本を読み漁っているのですが、やっとこさPHPのinterfaceが分かるようになってきた感じがあるので、まとめてみます。

interfaceとは

インターフェースは、クラスから"ユーザー定義型"の能力のみを分離した言語機構です。PHPのそれはJavaのinterfaceのパクリです。

"ユーザー定義型"という単語を使いました。動的型付き言語のことを「型のない言語」と言う人がたまにいるんですが、とんでもねー間違いです。PHPにだって型はあります。変数が型を持たず、値が型を持っているというだけの話です。

interfaceを宣言する文法はクラスとよく似ていますが、キーワードclassの代わりにキーワードinterfaceを使います。中身は定義のないメソッドの宣言を書きます。なお、定数も含めることが可能です。


<?php
interface FooInterface
{
  const MOGE = 1;
  public function doSomething1();
  public function doSomething2(DateTime $date);
}

できあがったFooInterfaceは、クラスではないのでnewでインスタンス化することはできません。implementsキーワードを使い、クラスとして中身を実装するとnewできるようになります。

//ソースコードはさっきのものから続いているイメージです
class Foo implements FooInterface
{
  public function doSomething1()
  {
  }

  public function doSomething2(DateTime $date)
  {
  }
}

$foo = new Foo;
var_dump($foo instanceof FooInterface); //true

interfaceは何の役に立つのか

私は最初、interfaceの使い道がよくわかりませんでした。インスタンス化できないクラスの出来損ないで、宣言すれば記述量が増えるだけです。実際、interfaceを一切使わずともPHPのコードは書けます。…まあ、そんなことを言い始めると、goto文さえあればfor文もtry catchも関数も不要、ってことになってしまいます。interfaceが存在するのは便利な場面があるからです。

interfaceはそれ単独では役に立ちません。もっぱら、タイプヒンティングと組み合わせて使います。タイプヒンティングに使われていないinterfaceがソースコード中に出現したら、「お前は何がしたいんだ」とレビューコメントに書いてOKです。(正確には他にも使い道があるのですが、ややこしくなるので一旦そういうことにしておいてください)

//...
function useFoo(FooInterface $foo) {
  $foo->doSomething1();
  $foo->doSomething2(new DateTime);
}

$foo instanceof FooInterface を満たしていない$fooを渡した場合、このuseFoo()関数を実行するとエラーが発生してPHPが停止します。動的検査なので効果は限定的ですが、カバレッジ100%のユニットテストと組み合わせれば早期にプログラムの問題に気づくことができるでしょう。

この場合、タイプヒンティングを通過した$fooは、メソッドdoSomething1とdoSomething2を持っていることとと、メソッドの引数の型が保証されています。

クラスによるタイプヒンティングとの違い

ところで、タイプヒンティングにはクラスも使えます。

function useFoo2(Foo $foo) {
    $foo->doSomething1();
    $foo->doSomething2(new DateTime);
}

じゃあ、やっぱりinterfaceなんて使わずに、クラスだけ使ったほうがお手軽じゃないか、と思うかもしれません。

この辺りはもう少し実例を出さないとピンとこないものです。interfaceが役に立つのは、もう少し大きな粒度、いくつかのクラスをまとめたパッケージのような概念が出てきた時です。

よくある例として、Webアプリケーションフレームワークと、テンプレートエンジンを組み合わせて使うことを考えます。

フレームワークは何でもよいです。SymfonyでもZFでも好きなものを想像してください。なんかわからんがフレームワークというものがある、とします。

blog_interface1.png

大抵のフレームワークはテンプレートエンジンを使って、テンプレート上に変数を展開して、HTMLを生成します。しかし世の中にはSmartyやらTwigやらMustacheやら、多くのテンプレートエンジンがあるので、好みに応じて差し替えられるようにしたいです。

テンプレートエンジンも、これまた何かよくわかりませんが、「テンプレートの文字列と変数の組を渡せばHTMLに変換してくれる、道具のようなオブジェクト」とします。フレームワークがテンプレートエンジンを使う関係です。

blog_interface2.png

さて、テンプレートエンジンは差し替えられるようにしたいのでした。しかしテンプレートエンジンが「何だかわからんが道具的なオブジェクト」というだけでは、仕様がわからないので使いようがありません。そこで、仕様を決め打ちして使うことにします。もし、テンプレートエンジン側が決め打ちしたものと違う仕様ならば、差分を取り持つラッパーオブジェクトを作ればいいのです。

このとき、フレームワーク設計者は、クラスでタイプヒンティングするか、interfaceでタイプヒンティングするか選ぶことができます。

namespace MyFramework;

class DefaultTemplateEngine {
  public function render($template, array $context)
  {
    //...
  }
}
//...

class FrameworkController {
  //...
  public function renderView(DefaultTemplateEngine $templateEngine, $template, array $context)
  {
    return $templateEngine->render($template, $context);
  }
  //...
}
namespace MyFramework;

interface TemplateEngineInterface {
  public function render($template, array $context);
}

class FrameworkController {
  //...
  public function renderView(TemplateEngineInterface $templateEngine, $template, array $context)
  {
    return $templateEngine->render($template, $context);
  }
  //...
}

…まだピンとこないですね。では先ほどの図に、もう一人登場人物を増やしてみましょう。テンプレートエンジンはメールの定型文を扱うのにも便利なので、メーラーも同じテンプレートエンジンを使うとします。

blog_interface3.png

このメーラーもクラスを要求するか、インターフェースを要求するか選べます。さてさて、どっちが優れているでしょうか?

両者がクラスを要求したとします。すると、操作をテンプレートエンジンに移譲するだけのクラスをパッケージごとに作らないといけません。なんか面倒くさいですね。実装も面倒ですし、インスタンスを使い分けなければならないので管理も煩雑になります。

//フレームワーク用のテンプレートエンジンクラス
class FrameworkTemplateEngine extends MyFramework\DefaultTemplateEngine
{
  //...
}

//メーラー用のテンプレートエンジンクラス
class MailerTemplateEngine extends MyMailer\DefaultTemplateEngine
{
  //...
}

両者がインターフェースを要求したとします。すると、二つのインターフェースを実装した一つのクラスを作るだけで済みます。クラスが一個ということはインスタンスも一つでよいので、インスタンスの管理も単純明快です。基底クラスはテンプレートエンジンの側のクラスを使うことができるので、実装も簡単になります。

//フレームワークもメーラーも両方使いまわせるテンプレートエンジンクラス
class TemplateEngine extends HogeTemplateEngine implements
  MyMailer\ViewInterface,
  MyFramework\TemplateEngineInterface
{
  //...
}

PHPのクラスは単一継承しか許されていません。クラスでタイプヒンティングするということは、そのクラスか、もしくは派生クラスを要求するということです。これは実は、非常に厳しい制約です。インターフェースの場合は単一継承の制約を受けないため、制約が緩くなります。

パッケージがクラスを要求すると、わざわざ専用にクラスを作らなくてはならないし、しかも作ったクラスは他所のパッケージで使いまわしできなくなります。クラスを要求するというのは「オレ様の流儀に従え!!再利用?オレの知ったことではない!」みたいに上から目線の偉そうな設計なのです。下品ですね。

というわけで、タイプヒンティングでクラスではなくインターフェースを要求することで、謙虚で組み合わせやすいライブラリが作れて、しかも型による(ある程度の)品質保証ができます。無闇にクラスを要求するのは不作法なのでやめましょう。

PHPのinterfaceの駄目なところ

ざっとinterfaceの使いどころについて書いてきましたが、よく考えるとPHPのinterfaceはイマイチなところがあります。「動的な検査しかできない」というのは言語的に仕方ないのですが、それ以外にも駄目なところが一杯です。

型の記述力が貧弱

interfaceを型と言うには、記述力が必要です。しかしPHPの型システムはとても貧弱です。interfaceにはメソッドが実在することと、メソッドの引数の型しか記述できません。メソッドの戻り値の型は示せないし、型を引数に取るような型や、関数を示す型も記述できません。

マジックメソッドとの相性が悪い

PHPのクラスには存在しないメソッドをあたかも存在するように見せかけるための__callや__getといったマジックメソッドが存在します。しかし、interfaceによるタイプヒンティングでは、これらの中身までは保証できないため、組み合わせて使うことができません。

マジックメソッドを保証したい場合は、finalを適宜組み合わせた上で、クラスでのタイプヒンティングが必須になります。

多重実装が保証されていない

何より致命的なのは、この点です。PHPにはメソッドのオーバーロード機能がないので、シグネチャが競合すると実装できなくなってしまうのです。せっかく言語的に必要もないのにinterfaceを定義して、お膳立てしてやっても使えないことがあるのだからたまらんです。現実に、Psr\Log\LoggerInterfaceと、Zend\Log\LoggerInterfaceはシグネチャが競合するため、同時に実装できません。

実装できないケースは以前にQiitaでまとめたのでそちらを参照してください。

PHP - interfaceがエラーになるケース - Qiita [キータ]

こんな風に残念なところが多いため、PHPにおいては、あまりインターフェースを乱発すると使いにくいソースコードになるような気がします。少なくとも自分で制御できるパッケージ内の型については、クラスでタイプヒントしても問題ないと思います。

とはいえ、抽象クラスを強要するよりはinterfaceを強要した方が多重実装の可能性が残されているだけまだマシなので、パッケージ外部との境界上は、やはりinterfaceを使って記述しておいた方が親切です。

PHPのinterfaceは公称型である

ところで、PHPのinterfaceはあくまで宣言時に型が決まり、偶然同じシグネチャを持っていても同じ型だとは見なされません。こういう性質を公称型と呼びます。

<?php
interface FooInterface {
    public function doSomething();
}

interface MooInterface {
    public function doSomething();
}

class Hoge implements FooInterface {
    public function doSomething() { }
}

$hoge = new Hoge;

var_dump($hoge instanceof FooInterface); // true
var_dump($hoge instanceof MooInterface); // false. 同じシグネチャを持っているけど別の型

公称型とは逆に、宣言時の名前はあまり重要視されず、偶然同じシグネチャを持っていれば同じ型とみなす言語もあります。この立場を構造的部分型(Structural Subtyping)と言います。PHPに構造的部分型は実装されていないので、残念ながら使えません。構造的部分型が実装されている言語としてはTypeScriptやScalaなどがあります。

//TypeScriptの例
class A { }
interface I { }
 var a : A = new A;
 var i : I = a;  //無関係のI型変数に代入しているが、エラーにならない。

構造的部分型は静的なダックタイピングであると言われることがあります。公称型はあくまで宣言時に書いてあることしか信用しないので、お役所的というか、融通が利かないイメージです。個人的には、構造的部分型の方が中身を見てくれるので柔軟で便利な気がしています。

公称型の利点

しかし、公称型だからこその長所もあります。

構造的部分型の場合、偶然のシグネチャ一致なのか、意図して一致させたのかが区別できません。公称型の場合、少なくとも宣言時に指定しているということは、interfaceの仕様を読み、意図して実装しているという表明になるのです。

この性質は、ひるがえせば型の記述力が貧弱でもカバーできるということです。だって中身より表明していることが大事だと言うのですから。ちょっとスピリチュアルな感じです。

そしてこの公称型の性質を最大限活用するパターンこそがマーカーインターフェースです。マーカーインターフェースとは、中身が空になっているインターフェースのことです。

interface Marker { }

ファッ!? 何それ、インターフェースの意味ないじゃん。いやいや、公称型であれば使い道があるんです。あとで詳しく説明します。

先ほどPHPのイケてない所として、多重実装ができないケースがあると述べましたが、マーカーインターフェースは中身がないので競合が絶対に起こりません。マーカーインターフェースであれば、PHPでも多重実装が保証されるので、安心して使えます。

マーカーインターフェースを使いこなす

マーカーインターフェースの使い道としては、例外の柔軟な捕捉や、アノテーションの代替が挙げられます。

例外の柔軟な捕捉

例外の捕捉にはタイプヒンティングを利用できます。

SPLに標準的な例外が一通り実装されているので、通常はこちらの型階層を使って例外捕捉を記述するはずです。しかしながら、独自の例外を作って独自の例外処理をしたい場合もあるでしょう。この時、独自の例外クラスを定義してしまうと、SPLとは別の継承ツリーに属してしまうため、一般的なSPLの例外捕捉が使えなくなってしまいます。

こんな時、マーカーインターフェースを使って型をミックスインすると、SPLでも独自型でも例外を捕捉できるようになります。

interface MyExceptionMarker { }

class MyInvalidArgumentException extends InvalidArgumentException implements
  MyExceptionMarker
{
}

class MyOutOfBoundsException extends OutOfBoundsException implements
  MyExceptionMarker
{
}


//...

//これでも例外を捕捉できるし
try {
  doSomething();
} catch (InvalidArgumentException $e) {
  //...
} catch (OutOfBoundsException $e) {
  //...
}

//これでも例外を捕捉できる
try {
  doSomething();
} catch (MyExceptionMarker $e) {
  //...
}

このパターンはZend Frameworkなどで使われています。

アノテーションの代替

マーカーインターフェースは、クラスに付与されたメタ情報と見なすこともできます。クラスにタグを打っているような感じです。なので、クラスアノテーションの代替手段として利用することができます。

たとえばDIコンテナで特定のクラスをインスタンス化する際、あるマーカーインターフェースが付与されていればプロパティをインジェクションするとか、インスタンスの管理をシングルトンに変更するといったことが可能です。ここでは具体的な実装は示しませんが、「クラスそのものを扱うライブラリ」と組み合わせると色々な使い方があるはずです。

namespace My\MarkerInterface as at;

//クラスに「シングルトンとして扱え」「レイジーロードで読み込め」といった印をつけている
class Hoge implements
  at\Singleton,
  at\LazyLoad
{
  //...
}

アノテーションのライブラリはdoctrine/commonなどがありますが、マーカーインターフェースの場合はリフレクションすら使わず、素のPHPの機能だけで実現できるため、ライブラリに比べて圧倒的に高速に動作します。なのでマーカーインターフェースの使いどころは色々研究できるのではないでしょうか。

まとめ

  • ライブラリのプラグイン機構部分で特定のクラスを要求するのは上から目線でムカつく作りです。そういう偉そうなライブラリは作らないようにしてください。要求するなら少なくともinterfaceを要求してください。
  • PHPのinterfaceはそこまで洗練されたものではないので期待しすぎてはいけません。
  • 公称型を採用しているがゆえに何とか使い道が残っているので、利点を最大限活用しましょう。
  • 特にマーカーインターフェースはPHPでも完全な能力を発揮するので、もっとみんな使えばいいと思います。

参考文献

interfaceといえばJavaですので、Effective Javaあたりを読むとマスターした感じになれると思います。ただ、絶版になっちゃったんですよねえ…。 PHPの解説本にこの手の内容が書いてあることは稀な気がします。他にいい本ないでしょうかね。



keyword: interface デザインパターン OOP PHP

PHPの最新記事

×

この広告は180日以上新しい記事の投稿がないブログに表示されております。