【PHP】ざっくりオブジェクト指向について③

【PHP】ざっくりオブジェクト指向について③

はじめに

本記事は、オブジェクト指向について学習した内容をまとめています。3 つの記事に分かれており、本記事は 3 記事になります。

サンプルコードの PHP のバージョンは 8.1.20 で、開発環境に Docker を使用しています。

インターフェース

Wikipedia 引用

インタフェース (英: interface) は、Java や C#などのオブジェクト指向プログラミング言語においてサポートされる、実装を持たない抽象型のことである。これらの言語において、クラスは実装の多重継承をサポートしない代わりに、任意の数のインタフェースを実装 (implement) することができ、これにより型の多重継承をサポートする。複数の種類のオブジェクトを、インタフェースを用いた多態性によって統一的に扱うことができるようになる。インターフェイスやインターフェースなどと表記することもある

上記の内容は Java や C#で解説されていますが、PHP においても同様のことが言えます。

インターフェースとは、全てのメソッドが実装されていない仕様の集合を定義します(つまりすべて抽象メソッドである)。インタフェースは具体的な実装を一切持たず、どのようなメソッドが必要かだけを定義します。

また、インターフェースで定義したメソッドはインターフェースを実装したクラスでは必ずそのメソッドを記述しなければいけません。

これは前回解説した、抽象クラス及び抽象メソッドと同様です。

インターフェースのサンプルコードとしてIAnimalインターフェース及びIAnimal.phpを作成します。

"SampleInterface/IAnimal.php"
<?php

namespace SampleInterface;

interface IAnimal
{
    // 定数を定義することが出来ます
    public const BEST_SLEEP = "良質な睡眠";

    // 具体的な実装を定義することは出来ません
    // アクセス修飾子は必ず public にする必要があります
    public function sleep(); // 動物は眠ります
}

インターフェースで定義するメソッドのアクセス修飾子はpublicにする必要があります。また、定数も定義することが出来ます。定義された定数はメソッドと違い実装の強制はされません。

次に、IAnimal インターフェースを実装したDogクラス及びDog.phpファイルを作成します。

"SampleInterface/Dog.php"
<?php

namespace SampleInterface;

use SampleInterface\IAnimal;

class Dog implements IAnimal // implements クラス名 でインターフェースを実装する
{
    // インターフェースで定義されたメソッドは必ず実装する必要がある
    public function sleep()
    {
        // インターフェース実装先クラスで処理を記述する
        return "犬は 12~14 時間寝ます。";
    }
}

最後にCatクラス及びCat.phpファイルを作成します。

"SampleInterface/Cat.php"
<?php

namespace SampleInterface;

use SampleInterface\IAnimal;

class Cat implements IAnimal
{
    public function sleep()
    {
        return "猫は 12~16 時間寝ます。";
    }
}

これらの作成したクラスをオートローダーに登録します。オートローダーの詳細については前回の記事を参考してください。

"composer.json"
{
    "name": "hn_pgtech/sample_php_project",
    "description": "PHPの機能を検証する",
    "require": {},
    "autoload": {
        "psr-4": {
            "BaseClass\\": "ObjectSample/BaseClass/",
            "ExtendTrait\\": "ObjectSample/ExtendTrait/",
            "SampleStatic\\": "ObjectSample/SampleStatic/",
            "SampleConst\\": "ObjectSample/SampleConst/",
            "SampleAbstract\\": "ObjectSample/SampleAbstract/",
+           "SampleInterface\\": "ObjectSample/SampleInterface/"
        }
    }
}
$ composer dump-autoload

作成したクラスを実行します。

"sampleInterface.php"
<?php

require __DIR__ . '/../vendor/autoload.php';

use SampleInterface\Cat;
use SampleInterface\Dog;

$dog = new Dog();
echo $dog->sleep() . "\n"; // 犬は 12~14 時間寝ます。

$dog = new Cat();
echo $dog->sleep() . "\n"; // 猫は 12~16 時間寝ます。

どちらのクラスもインターフェースで定義したメソッドを実装していますが、具体的な処理はサブクラス側で実装しているので内容は異なります。

複数のインターフェースを実装

抽象クラスは一つのクラスしか継承することが出来ませんでしたが、インターフェースは複数実装することが出来ます。サンプルとして新たにIAnimal2.phpIAnimal3.phpを作成します。

"SampleInterface/IAnimal2.php"
<?php

namespace SampleInterface;

interface IAnimal2
{
    public function eat(); // 動物は食べます
}
"SampleInterface/IAnimal3.php"
<?php

namespace SampleInterface;

interface IAnimal3
{
    public function run(); // 動物は走ります
}

これらのインターフェースを実装します。

"SampleInterface/Cat.php"
<?php

namespace SampleInterface\le\SampleInterface;

use SampleInterface\IAnimal;
use SampleInterface\IAnimal2;
use SampleInterface\IAnimal3;

class Cat implements IAnimal, IAnimal2, IAnimal3 // カンマ区切りで複数実装することが出来る
{
    public function sleep()
    {
        return "猫は 12~16 時間寝ます。";
    }

    public function run()
    {
        return "猫は素早く走ります。";
    }

    public function eat()
    {
        return "猫はよく食べます。";
    }
}

Dog.phpにも同様に実装します。

"SampleInterface/Dog.php"
<?php

namespace SampleInterface;

use SampleInterface\IAnimal;
use SampleInterface\IAnimal3;

class Dog implements IAnimal, IAnimal3 // 実装するインターフェースを任意に選択することも出来る
{
    public function sleep()
    {
        return "犬は 12~14 時間寝ます。";
    }

    public function run()
    {
        return "犬の走る速度は人間より速いです。";
    }
}

このように必要な機能を選択して実装することが出来ます。

インターフェースまとめ

  • インターフェースとは、全てのメソッドで具体的な実装がされていない仕様を定義した集まりのこと(どのようなメソッドが必要かだけ定義する)
  • 抽象クラスでは具体的な処理内容を記述したメソッドも定義できたが、インターフェースでは抽象メソッドしか定義できない
  • インターフェースでは継承ではなく、実装と呼ぶ
  • インターフェースの実装にはimplementsを使う
  • 抽象メソッドと同様に、インターフェースに定義したメソッドは実装先で必ず記述する必要がある
  • 必ず実装してもらうためのクラスのテンプレートとしてインターフェースを使
  • 複数のインタフェースを実装することが出来る

ポリモーフィズム(多態性)

ポリモーフィズム(多態性)とは、オブジェクト毎に実装した異なる動作を、同じ呼び出し方法で実現することです。クラスは異なっても同じ名前のメソッドで色々な処理を実現させる事が出来ます。

これらを実現する為にインターフェースが必要になってきます。

新たにBird.phpを作成してその動作を確認してきます。

"SampleInterface/Bird.php"
<?php

namespace SampleInterface;

use SampleInterface\IAnimal;

class Bird implements IAnimal
{
    public function sleep()
    {
        return "鳥の睡眠時間は短いです。";
    }
}

DogクラスとCatクラス同様にsleepメソッドを実装しました。それぞれのクラスがIAnimalインターフェースを通じて独自の実装を完了していることが分かります。

これらのクラスの実行を管理するAnimalClientというクラスを作成します。

このクラス内でstaticメソッドを定義し、このクラスから同じ呼び出し方法でオブジェクト毎に異なる動作を呼び出します。SampleStaticディレクトリにAnimalClient.phpファイルを作成します。

"SampleStatic/AnimalClient.php
<?php

namespace SampleStatic;

use SampleInterface\IAnimal;

class AnimalClient
{
    // インターフェースに依存したオブジェクトを渡す
    public static function executeSleep(IAnimal $animalObj)
    {
        // IAnimalインターフェースでは sleep メソッドが
        // 実装されていることが保証されているので呼び出せる
        return $animalObj->sleep();
    }
}

executeSleep(IAnimal $animalObj)で引数にIAnimalインターフェースを指定しているのは型宣言と言い、関数の引数や戻り値に型を指定することが出来ます。

これによって、引数に渡ってくる$animalObjIAnimalインターフェースを実装したクラスでなければならないということが保証されることになります。

この指定している型をDogクラスやCatクラスなど具体的なクラス(具象クラス)にしてしまうと、引数の数を増やしたりif文で渡ってきたクラスを判定するロジックが増えてしまいます。


    // 例
    // プログラムに変更があり、クラスが増えるえれば渡す引数も増えてしまう
    public static function executeSleep(Dog $dogObj, Cat $catObj, Bird $birdObj)
    {
        // インスタンスを判定するif文も増えていく
        if($dogObj instanceof Dog) {
            return $dogObj->sleep();
        }

        if($catObj instanceof Cat) {
            return $catObj->sleep();
        }

        if($birdObj instanceof Bird) {
            return $birdObj->sleep();
        }

        // ...

        // 変更するたびに、逐一条件分岐をテストする工数も増える
    }

このように、呼び出し側が依存するプログラムを具象クラスではなくインターフェースにすることで、ポリモーフィズムとしての機能を実現することができ、同じ呼び出し方で実装することが出来ます。

このメソッドを以下のように利用します。

"sampleInterface.php"
<?php

require __DIR__ . '/../vendor/autoload.php';

use SampleInterface\Bird;
use SampleInterface\Cat;
use SampleInterface\Dog;
use SampleStatic\AnimalClient;

// $dog = new Dog();
// echo $dog->sleep() . "\n"; // 犬は 12~14 時間寝ます。

// $dog = new Cat();
// echo $dog->sleep() . "\n"; // 猫は 12~16 時間寝ます。

// DogクラスはIAnimalインタフェースを実装しているので引数に渡すことが出来ます
echo AnimalClient::executeSleep(new Dog()). "\n"; // 犬は 12~14 時間寝ます。

echo AnimalClient::executeSleep(new Cat()). "\n"; // 猫は 12~16 時間寝ます。

echo AnimalClient::executeSleep(new Bird()). "\n"; // 鳥の睡眠時間は短いです。

また、AnimalClient::executeSleep(new Dog())のように、何かに依存している(ここではIAnimalインターフェース)インスタンスを外から渡す(注入)することを、依存注入(Dependency Injection)と言います(DI と略される事が多い)。

依存注入に関しては以下の記事で解説しています。

上記の記事では C#で解説していますが、考え方は PHP でも同じです。

ポリモーフィズムまとめ

  • ポリモーフィズムは異なる動作を同じ操作で実現すること
  • クラスは異なっても同じ名前のメソッドで色々な動きを実現させる事を言い、それにはインターフェースが絡んでくる
  • 何かに依存したインスタンスを外部から渡す(メソッドの引数やコンストラクタの引数など)を、依存注入(Dependency Injection)と言う

まとめ

3 回に分けてざっくりオブジェクト指向について解説してきました。簡単なサンプルコードで解説されていることは多いですが、実際にどのような場面で使用するのが良いのか自分なりに考えて試してみることで、より理解が深まったと感じています。

以下は、今回使用したコードのリポジトリです。