インターフェイス

インターフェイスとは

PHPではインターフェイスが定義できます。

インターフェイスとは抽象メソッドのみ定義可能なクラスみたいなもんです。
抽象クラスでは普通のメソッドも定義できましたが、インターフェイスでは抽象メソッドしか定義できません。

こんな感じです。

<?php

interface IProduct
{
    public function applyPriceDown();

    public function getPrice();    

    public function setPrice($price);
}

?>

「class」の代わりに「interface」というキーワードを使用して定義します。
そして以下の様なルールがあります。

  • 定義できるのは抽象メソッドのみ
  • 抽象メソッドだが「abstract」はいらない
  • アクセス修飾子には「public」しか指定できない
  • 直接インスタンスの生成はできない

直接インスタンスを生成できないので継承するわけですが、インターフェイスの場合、継承とは呼ばず、「実装」と呼びます。

正しい実装
<?php

class Product implements IProduct
{
    public function applyPriceDown()
    {
        // 処理を記述
    }

    public function getPrice()
    {
        // 処理を記述
    }    

    public function setPrice($price)
    {
        // 処理を記述
    }
}

?>

実装には「extends」ではなく「implements」というキーワードを使用します。

実装の際にはインターフェイスで定義されているメソッドは全て実装する必要があり、アクセス修飾子は当然publicで、引数の数も名前も完全に一致している必要があります。

正しくない実装1
<?php

class Product implements IProduct
{
    public function applyPriceDown()
    {
        // 処理を記述
    }

    public function getPrice()
    {
        // 処理を記述
    }    

    // setPriceを実装していないためエラーとなる
}


?>
Fatal error: Class Product contains 1 abstract method and must therefore be declared 
abstract or implement the remaining methods (IProduct::setPrice) in ・・・
正しくない実装2
<?php

class Product implements IProduct
{
    public function applyPriceDown()
    {
        // 処理を記述
    }

    public function getPrice()
    {
        // 処理を記述
    }    

    public function setPrice() // 引数が一致しないためエラーとなる
    {
        // 処理を記述
    }
}

?>
Fatal error: Declaration of Product::setPrice() must be compatible with that of 
IProduct::setPrice() in D:\public_html\phprect\doc\test.php on line 18

インターフェイスの利用

実際にどのように利用するかを見てみましょう

<?php

// 野菜クラス
class Begetable implements IProduct
{
    private $price = 1000;

    public function applyPriceDown()
    {
        // 常に3割引
        $this->price = $this->price * 0.7;
    }

    public function getPrice()
    {
        return $this->price;
    }
}

// 肉クラス
class Meet implements IProduct
{
    private $price = 2000;

    public function applyPriceDown()
    {
        if ('29' == date('d')) {
            // 29日なら半額
            $this->price = $this->price / 2;
        }
    }

    public function getPrice()
    {
        return $this->price;
    }
}

// 冷凍食品クラス
class FreezedFood implements IProduct
{
    private $price = 500;

    public function applyPriceDown()
    {
        // 値引きなし
    }

    public function getPrice()
    {
        return $this->price;
    }
}

?>

IProductインターフェイスを実装した3つのクラスを作りました。
違いは、applyPriceDownメソッドの中身です。

このショップでは野菜が安く、常に3割引です。
肉は、毎月29日のみ半額になります。
冷凍食品は一切値引きなし。

以上の事を実現するために、applyPriceDownメソッドの実装内容を上記のようにしました。
getPriceが全て同じ処理内容で冗長な感じがしますが、今回はそこは無視してください。

そして通販サイトなのでカートが必要です。
カートクラスを作ってみます。

<?php

class Cart
{
    // 商品インスタンス保持用配列
    private $products = array();

    // 商品追加
    public function addProduct($product)
    {        
        $product->applyPriceDown();    
        $this->products[] = $product;
    }

    // 商品の合計価格取得
    public function getTotalPrice()
    {
        $total = 0;
        foreach ($this->products as $product) {
            $total += $product->getPrice();
        }
        return $total;
    }        
}

?>

カートクラスを定義し、商品追加用のメソッドを作りました。

このメソッドの仕様は、
まず引数として商品クラスのインスタンスを受け取ります。
そしてこのインスタンスのapplyPriceDownメソッドを実行して値引きの適用を行い、その後、フィールドである配列に追加します。

そしてもう一つ、カート内商品の合計価格を取得するメソッドを作りました。
これは単純にproductsフィールドに格納されている全ての商品のgetPriceメソッドによって価格を取得し、合計しているだけのものです。

これらのカートと商品のクラスを使って処理をしてみましょう。

<?php

// カート@
$cart = new Cart();

// 商品@
$meet = new Meet();
$begetable = new Begetable();
$freezed = new FreezedFood();

// カートに商品追加@
$cart->addProduct($meet);
$cart->addProduct($begetable);
$cart->addProduct($freezed);

// 現在の合計価格を取得@
$total = $cart->getTotalPrice();

var_dump($total);

?>
float(2700)

結果、野菜が三割引きされ、700 + 1000 + 1000 = 2700 という結果になりました。
もし、29日に実行すれば、700 + 500 + 1000 = 2200 という結果になるでしょう。


別になんてこともないように見える処理ですが、
このカートクラスの二つのメソッドは、商品クラスインスタンスにapplyPriceDownやgetPriceという名前のメソッドが定義されている前提で成り立つということにお気づきでしょうか。

仮に、ある日突然、ある商品のクラスからapplyPriceDownメソッドが消えてしまったら、途端にカートのaddProductメソッドでエラーとなります。
なので、商品クラスにはこれらのメソッドが確実に存在することを保証しなければなりません。

この役目を果たすのがインターフェイスなのです。
約束事として、商品クラスは必ずIProductを実装すれば、IProductに定義されているメソッドは100%に存在することになります。

インターフェイスと抽象クラスの違い

抽象クラスは抽象メソッドが定義できて、直接インスタンスの生成が出来ないクラスでした。
インターフェイスは抽象メソッドのみ定義できて、直接インスタンスの生成が出来ないクラスのようなものでした。

違いは、普通のメソッドやフィールドが定義できるか出来ないか、です。
じゃあ、抽象メソッドの方が便利で、インターフェイスの存在意義がないじゃん、となるかもしれません。
しかもインターフェイスの抽象メソッドはpublicしかだめだし。

実質的な話、そうなるかもしれません。
本当の違いは使い方の「意味合い」です。

抽象クラスはあくまで複数のクラスの共通部分を抽出して一つのクラスとし、
処理の中身は違うけど必ず必要なメソッドは抽象メソッドとして定義し、
各クラスはこの抽象クラスを継承して独自の処理は勝手に作ってねってなわけです。

抽象メソッド自体にあまり意味合いもなく、単に継承先に実装を強要するだけのもので、
protectedにすることも出来ます。
どちらかというとクラス設計を楽にする、言ってみれば抽象メソッドはクラス設計者向けの仕組みです。


それに対してインターフェイスはクラス使用者向けの仕組みと言えます。
クラスを使用してアプリ実装を行う、クラス使用者です。
どういうことかというと、

インターフェイスを実装しているクラスは、そのインターフェイスで定義されているメソッドが存在することを保証しています。
インターフェイスは、これを実装するクラスの扱い方を筋道だてているのです。
そのクラスはどんなクラスがメソッドが公開されていて、どういう使い方をするのか、いちいちクラスのプログラムソースを追わなくてもよいような仕組みを提供するのです。

上のカートの例で言えば、カートからしてみれば、商品がインターフェイスを実装していなければ、その商品の値引きを適用するにはどうしたらよいかは、そのクラスの中身を見て確認する必要があります。

インターフェイスによって、システム上の仕様を口約束だけにとどめず、実際のプログラムの構造に起こすのです。

多重実装

もう一つ、インターフェイスと抽象クラスの違いがあります。

多重継承(実装)できるか出来ないか、です。

PHPは言語仕様上、多重継承ができません
多重継承というのは、複数のクラスを継承してひとつのクラスを定義することを言います。これは、多重継承によりクラス関係が複雑になりすぎるなどの問題があるからです。
JavaとかC#とかRubyとか、最近のオブジェクト指向言語はみんなそんな感じになっています。

しかし、インターフェイスは多重実装が出来るのです。複数のインターフェイスを実装することが出来ます。

以下のような感じです。

<?php

// 商品インターフェイス
interface IProduct
{
    public function applyPriceDown();

    public function getPrice();    

    public function setPrice($price);
}

// 食品インターフェイス
interface IFood
{
    public function setExpire();

    public function checkExpire();    
}

?>
<?php

// 食料品クラス
class FoodProduct implements IProduct, IFood
{
    public function applyPriceDown()
    {

    }

    public function getPrice()
    {

    }

    public function setPrice($price)
    {

    }

    public function setExpire()
    {

    }

    public function checkExpire()
    {

    }
}

?>

implementsの後に、カンマ区切りで複数のインターフェイスを指定します。

上記の例では、
価格操作計のメソッドを定義した商品インターフェイスと、
賞味期限を設定するメソッドと、賞味期限が過ぎていないかをチェックするメソッドを持つ食べ物インターフェイスを定義しました。

そして商品インターフェイスと食べ物インターフェイスを多重実装し、商品と食べ物の性質を併せ持つ食料品クラスを定義しました。

実際、多重継承なんて登場するパターンはよっぽど大規模な開発になると思うので、普通はなかなか使う事はないかもしれませんが、インターフェイスの優位性としてご紹介しておきます。