デザインワン・ジャパン Tech Blog

DesignOne Japan | Activate the World.

【PHP】【初学者向け】抽象メソッド&インターフェース入門

はじめに

こんにちは!株式会社デザインワン・ジャパンで口コミサービス エキテンのリニューアルを担当しているサービス開発部の鈴木セシル(@suzuki_cecil_)です。

弊社では少し前からインターンシップとして大学院生のK君が入社されたのですが、そんなK君から「インターフェースに定義するメソッドと抽象クラスに定義する抽象メソッドは何が違うのでしょう?」と質問を受けました。どうやらK君は抽象クラスとインターフェースについて以下のように理解していたようでした。

  • インターフェースはメソッドの定義はできるが実装をすることはできない
  • 抽象クラスにはメソッドを実装することができるが、抽象メソッドとして定義だけを行うことができる

理解としては間違っていないのですが、メソッドの定義のみを行うという点が共通であるがために、どういう時にインターフェースに定義し、どういう時に抽象メソッドとして定義すればいいのかが整理できていないようでした。

確かに抽象クラスやインターフェースはPHPをはじめとするオブジェクト指向初学者の方の多くが躓きがちな点だと思います(かくゆう私も初学者の頃は、あまり理解できていなかったです)

せっかくなので今回はK君に対して行った解説をもとに記事にしてTechBlogに投稿します。少しでも多くのオブジェクト指向初学者の方に届いてくれればいいなと思います。

インターフェースと抽象クラスの言語仕様の違い

そもそもPHPの言語仕様としてインターフェースと抽象クラスの違いは以下の3つだと思います。

多重継承が不可能である

C++など一部の言語においては複数の(抽象)クラスを継承(多重継承)することが可能ですが、PHPJavaなど多くのプログラミング言語では多重継承ができません。これは 菱形継承問題による複雑さを排除するためです。

一方で複数のインターフェースを実装することは可能です。ゆえにインターフェース分離の原則(ISP)でも述べられている通り、インターフェースには最小限のメソッドの定義のみを行うということが可能になっています。またPHP8.0, 8.1で登場したUNION型や交差型を活用することでインターフェースを組み合わせることで実装の幅を広げることも可能になっています(いわゆる型パズル)。

この辺りに関しては語り出すとキリがないので、本記事では割愛させていただきます。

定義できるメソッドのアクセス修飾子が異なる

インターフェースは「接点」「境界面」といった意味を持つ通り、外部からの参照を前提としております。そのためインターフェースにはアクセス修飾子がpublicのメソッドのみ定義できます。

一方で抽象クラスは外部からの参照を前提としていないためアクセス修飾子がpublicもしくはprotectedのメソッドを定義することが可能です(継承を前提としているためprivateのメソッドを定義することはできません)

インターフェースではプロパティの定義ができない

インターフェースは具体的な処理を持たない関係からプロパティを定義することができず、定義できるのはpublicの定数のみで、またインターフェースを実装したクラスで定数をオーバーライドすることができません。

一方で抽象クラスではプロパティ/ローカル変数/定数いずれも定義することができ、アクセス修飾子にはpublic, protected, privateを指定することができます。またインターフェースとは異なりサブクラスでプロパティ/定数をオーバーライドすることが可能です。

インターフェースと抽象メソッドの活用例

インターフェースと抽象クラスの言語仕様の違いが分かったところで、本題のインターフェースと抽象メソッドの活用例をサンプルコードとともに見ていきましょう。

インターフェースの活用例

答えから言うとインターフェースは依存関係を整理するために使います。とは言っても、この一文だけでは理解が難しいかと思いますので詳しく解説させていただきます。

サンプルコード1

<?php
class FizzBuzz
{
    public function __construct(private DisplayPrinter $displayPrinter)
    {
    }

    public function execute(int $n)
    {
        for ($i = 1; $i <= $n; $i++) {
            if ($i % 15 === 0) {
                $this->displayPrinter->output("FizzBuzz");
            } elseif ($i % 5 === 0) {
                $this->displayPrinter->output("Buzz");
            } elseif ($i % 3 === 0) {
                $this->displayPrinter->output("Fizz");
            } else {
                $this->displayPrinter->output($i);
            }
        }
    }
}

class DisplayPrinter
{
    public function output(int|string $value): void
    {
        echo $value . "\n";
    }
}

$fizzBuzz = new FizzBuzz(new DisplayPrinter());
$fizzBuzz->execute(15);

サンプルコード1は見ての通り、FizzBuzzを行うプログラムで結果は標準出力しております。

FizzBuzzクラスから責務を分離するために出力処理はDisplayPrinterに移譲しております。

FizzBuzzクラスはDisplayPrinterクラスを参照しています。このように他のクラスやメソッドを参照する関係性のことを依存関係と言い、FizzBuzzクラスはDisplayPrinterクラスに依存している、というように表現されます。

では、結果を標準出力するのではなくテキストファイルに出力するように修正するとしたらどうでしょう。

サンプルコード2

<?php
class FizzBuzz
{
    public function __construct(private FilePrinter $filePrinter)
    {
    }

    public function execute(int $n)
    {
        for ($i = 1; $i <= $n; $i++) {
            if ($i % 15 === 0) {
                $this->filePrinter->output("FizzBuzz");
            } elseif ($i % 5 === 0) {
                $this->filePrinter->output("Buzz");
            } elseif ($i % 3 === 0) {
                $this->filePrinter->output("Fizz");
            } else {
                $this->filePrinter->output($i);
            }
        }
    }
}

class FilePrinter
{
    public function output(int|string $value): void
    {
        file_put_contents("result.txt", $value . "\n", FILE_APPEND);
    }
}

$fizzBuzz = new FizzBuzz(new FilePrinter());
$fizzBuzz->execute(15);

FizzBuzzの結果をファイル出力するようにしたプログラムがサンプルコード2になります。

サンプルコード1と比較すると以下の点が変更されています。

  1. 結果を出力するクラスがDisplayPrinterクラスからFilePrinterクラスに変更されている
  2. FizzBuzzクラスのコンストラクタで受け取るインスタンスがDisplayPrinter型からFilePrinter型に変更されている、ついでにプロパティの命名も変更している

責務を分離するためにFizzBuzzクラスから結果を出力する処理を切り出したのに、結果を出力する処理を変更するのにFizzBuzzクラスを修正するという矛盾が発生してしまいました。

この矛盾を回避するために活用されるのがインターフェースです。インターフェースを用いた場合どうなるでしょうか。

サンプルコード3

<?php
class FizzBuzz
{
    public function __construct(private Printer $printer)
    {
    }

    public function execute(int $n)
    {
        for ($i = 1; $i <= $n; $i++) {
            if ($i % 15 === 0) {
                $this->printer->output("FizzBuzz");
            } elseif ($i % 5 === 0) {
                $this->printer->output("Buzz");
            } elseif ($i % 3 === 0) {
                $this->printer->output("Fizz");
            } else {
                $this->printer->output($i);
            }
        }
    }
}

interface Printer
{
    public function output(int|string $value): void;
}

class DisplayPrinter implements Printer
{
    public function output(int|string $value): void
    {
        echo $value . "\n";
    }
}

class FilePrinter implements Printer
{
    public function output(int|string $value): void
    {
        file_put_contents("result.txt", $value . "\n", FILE_APPEND);
    }
}

$fizzBuzz = new FizzBuzz(new FilePrinter());
$fizzBuzz->execute(15);

インターフェースを用いた場合のプログラムがサンプルコード3になります。

サンプルコード2と比較すると以下の点が変更されています。

  1. Printerインターフェースを定義している
  2. DisplayPrinterクラスとFilePrinterクラスはPrinterインターフェースを実装している
  3. FizzBuzzクラスのコンストラクタで受け取るインスタンスがFilePrinter型からPrinter型に変更されている、ついでにプロパティの命名も変更している

サンプルコード3のようにインターフェースを用いることで、例えば結果を標準出力するように再変更したい場合、FizzBuzzクラスのコンストラクタに渡すインスタンスをDisplayPrinterのインスタンスにするだけで完遂するため、FizzBuzzクラスを修正する必要はなくなりました。

サンプルコード1ではFizzBuzzクラスはDisplayPrinterクラスに依存しているのに対して、サンプルコード3ではFizzBuzzクラスもDisplayPrinterクラス(およびFilePrinterクラス)もPrinterインターフェースに依存しています。

書籍「アジャイルソフトウェア開発の奥義」では、このように書かれています。

上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきである。

上位や下位といったレイヤーの解説は一旦省略させていただきますが、大規模なソースコードを作り上げていく上で抽象(インターフェース)に依存するようにすることで正しい依存関係を作り上げ、 依存関係を整理する ことは非常に重要であるということは覚えておくと必ず役に立ちます。

抽象メソッドの活用例

インターフェースが依存関係を整理する のに活用されるのに対して、抽象クラスを含むクラスの継承はクラスの再利用をする際に活用されます。特に抽象メソッドに関して着目してみると、個人的な意見としてはTemplateMethodパターンを適用する際が一番の活用どころではないかと考えます。

TemplateMethodパターンとは一連の流れが定まっている処理(テンプレート)に対して一部の処理のみを場合わけしたいときに活用されるデザインパターンです。

先ほどのサンプルコード同じくFizzBuzzを例にTemplateMethodパターンのソースコードを見てみたいと思います。

サンプルコード4

<?php
abstract class FizzBuzz
{
    public function execute(int $n)
    {
        for ($i = 1; $i <= $n; $i++) {
            if ($i % 15 === 0) {
                $this->output("FizzBuzz");
            } elseif ($i % 5 === 0) {
                $this->output("Buzz");
            } elseif ($i % 3 === 0) {
                $this->output("Fizz");
            } else {
                $this->output($i);
            }
        }
    }

    abstract protected function output(int|string $value): void;
}

class DisplayPrintedFizzBuzz extends FizzBuzz
{
    protected function output(int|string $value): void
    {
        echo $value . "\n";
    }
}

class FilePrintedFizzBuzz extends FizzBuzz
{
    protected function output(int|string $value): void
    {
        file_put_contents("result.txt", $value . "\n", FILE_APPEND);
    }
}

$displayPrintedFizzBuzz = new DisplayPrintedFizzBuzz();
$displayPrintedFizzBuzz->execute(15);

$filePrintedFizzBuzz = new FilePrintedFizzBuzz();
$filePrintedFizzBuzz->execute(15);

FizzBuzzの一連の処理は以下のように分解することができます。

  1. 1からnまで繰り返す
  2. 15で割り切れる場合はFizzBuzz, 5で割り切れる場合はBuzz, 3で割り切れる場合はFizz、それ以外の場合はその数字を結果とする
  3. 結果を出力する

この一連の処理をそのままに「3. 結果を出力する」の具体的な処理だけを場合分けするようにしたのがサンプルコード4のプログラムになります。

FizzBuzzクラスのexecuteメソッドを見てみると1からnまで繰り返し、15で割り切れる場合はFizzBuzz, 5で割り切れる場合はBuzz, 3で割り切れる場合はFizz、それ以外の場合はその数字を結果とし、その結果を出力するという、前述した1~3の処理の流れが記述されており、結果の出力の具体的な処理がDisplayPrintedFizzBuzzクラスおよびFilePrintedFizzBuzzクラスのoutputメソッドに記述されているのがわかるかと思います。

このようにTemplateMethodパターンでは一連の流れをスーパークラス(今回の場合はFizzBuzzクラス)、場合分けしたい処理をサブクラス(今回の場合はDisplayPrintedFizzBuzzクラスおよびFilePrintedFizzBuzzクラス)に記述します。

スーパークラスに一連の流れを記述する際に場合わけする処理を具体的に書く必要はありませんが、一連の処理の中に組み込む必要があります。その際に場合分けしたい処理を抽象メソッドとして切り出すことによって具体的な処理を記述することなく一連の処理の中に組み込めるようになります。

TemplateMethodパターンを例に解説したように抽象メソッドはインターフェースとは異なりクラス内部からの参照のために活用されることが多いため、言語仕様としてはアクセス修飾子にpublicもしくはprotectedを指定することがきますが、実際に活用する場面を考えてみると基本的にはprotectedメソッドとして定義されることがほとんどだと思います。

まとめ

サンプルコード3とサンプルコード4の共通点としてFizzBuzzクラスから出力処理を移譲するという点で共通しています。

ただしインターフェースを活用したサンプルコード3ではFizzBuzzクラス外に移譲しているのに対して、抽象メソッドを活用したサンプルコード4ではFizzBuzzクラス内(サブクラス)の他のメソッドに移譲しているというように、移譲先が異なる点がサンプルコード3とサンプルコード4の決定的な違いとなります。

一概にどちらの方が優れていると断言することは難しいです(※私個人としてはインターフェースの方が可読性に長けていると考えています)が、作りたいプログラムに応じて、クラス外に処理を移譲したい場合はインターフェースを、クラス内に処理を移譲したい場合は抽象メソッドを活用するといいでしょう。

仲間を募集しております

募集中の職種については以下を御覧ください。

www.wantedly.com