読者です 読者をやめる 読者になる 読者になる

Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

PHP 7 の無名クラスから考えるクラスの在り方

いよいよ、PHP の次期メジャーバージョンの PHP 7 がリリースされます。すでに、RC4 が登場しており、来月予定されている本リリースが楽しみです。

PHP 7 には幾つかの新機能が盛り込まれているのですが、その中でも気になるのが、無名クラスです。

無名クラスとは

無名クラスは、クラスの定義をすることなく、オンザフライでオブジェクトを生成する機能です。匿名関数(ネイティブクロージャ)のクラス版だと思うとイメージしやすいです。

無名クラスは、new classで、生成します。下記が、そのサンプルです。ここでは、sayメソッドを持つ無名クラスを定義して、$objectに格納しています。$object の say メソッドを実行すると、'Hello'という文字列が出力されます。

<?php
$object = new class {
    public function say()
    {
        echo 'Hello' . PHP_EOL;
    }
};

$object->say();
// Hello

JavaScript の IIFE のように即時実行することもできます。

<?php
(new class {
    public function say()
    {
        echo 'Hello' . PHP_EOL;
    }
})->say();
// Hello

無名クラスは、通常のクラスと同様、プロパティを保持することができます。クラスの継承、インターフェイスの実装、トレイトの追加も可能です。

<?php
class SomeClass {}
interface SomeInterface {}
trait SomeTrait {}

$object = new class extends SomeClass implements SomeInterface {
    use SomeTrait;

    private $message = 'Hello';

    public function say($name) {
        echo $this->message . ' ' . $name . PHP_EOL;
    }
};

$object->say('Jun');
// Hello Jun

クラスに変わる無名クラス

これまで、タイプヒンティングでスカラー型以外の型が要求されている場合、クラスの実装が必要でした*1。無名クラスを使うことで、クラスを定義をすることなく、要求されている型のオブジェクトを簡単に作ることができます。

ところで、クラスはどういったことを定義するものでしょうか。いくつかの考えはあると思いますが、オブジェクトの「仕様」とその「実装」を定義するものという見方ができます。「契約による設計」という観点でいうと、契約とその契約で履行される内容を同時に定義したものともいえます。

こうした仕様や実装はクラスを使わずとも表現できます。そう、「仕様」であればインターフェイス、「実装」であればトレイトが使えます。PHP 5でもこうした機能はあるのですが、オブジェクトとして利用するには、これらを結びつけるクラスが必要でした。

コードで例を見てみましょう。下記のように、Writer インターフェイスと、その実装である FileWriter トレイトと DatabaseWriter トレイトがあります。

<?php
interface Writer
{
    public function write(string $to, string $data);
}

trait FileWriterTrait
{
    public function write(string $to, string $data)
    {
        var_dump(__METHOD__);
    }
}

trait DatabaseWriterTrait
{
    public function write(string $to, string $data)
    {
        var_dump(__METHOD__);
    }
}

Writerインターフェイスを要求する store 関数は下記です。

<?php
function store(Writer $writer) {
    // something logic

    $to = 'to';
    $data = 'data';

    $writer->write($to, $data);
}

store 関数を実行する場合、PHP 5 だと、Writer インターフェイスを実装したクラスを定義する必要があります。ここでは、FileWriterTrait を利用していますが、他のトレイトを利用する際は、別のクラスを定義する必要があります。

<?php
// クラス定義
class FileWriter implements Writer
{
    use FileWriterTrait;
}

// オブジェクトを変数へ
$writer = new FileWriter();
store($writer);
// string(22) "FileWriterTrait::write"

// 即時実行
store(new FileWriter());
// string(22) "FileWriterTrait::write"

これが、PHP 7の無名クラスを使うと、下記のようになります。インターフェイスやトレイトは同じですが、それらを結びつけるクラス定義をあらかじめ行う必要がありません。利用する際に必要なものを無名クラスに実装するだけで良くなります。さらに、即時実行すれば、オブジェクトを変数に格納することなく、トレイトのコードを実行することができます。

<?php

// オブジェクトを変数へ
$writer = new class implements Writer {
    use FileWriterTrait;
};
store($writer);
// string(22) "FileWriterTrait::write"

// 即時実行
store(new class implements Writer {
    use DatabaseWriterTrait;
});
// string(26) "DatabaseWriterTrait::write"

仕様はインターフェース、実装はトレイト、無名クラスで利用

PHP 7からは,インターフェイスで「仕様」を定義し、トレイトで「実装」、そして必要な時に無名クラスで「利用」することができます。これは、これまでのクラスの考え方を変える可能性があります。

クラスは、これらの役割を一つで担うことができる便利なものでした。ただ、一つのものが多くの役割を担うことで、そのコードで表現したいことが、仕様なのか、実装なのかを意識せずに使ってしまうという面がありました。

例えば、仕様(型)を表現した方がベターな箇所(タイプヒンティングなど)で、インターフェイスではなく具象クラスを指定してしまうといった場合です。

無名クラスを前提に考えると、インターフェイス、トレイト、無名クラスの内、タイプヒンティングに指定できるのはインターフェイスなので、自然にこれを指定することになります。

タイプヒンティングで必要なのは、依存クラス(オブジェクト)の仕様であり、実装では無いので、インターフェイスを指定するが自然です。また、そのインターフェイスを満たしていればどのようなオブジェクトでも受け入れることができるので、柔軟性を持った疎結合な構成にすることができます。

さいごに

無名クラスの登場により、インターフェイスやトレイトを定義した仕様や実装を柔軟に結びつけて利用することが可能になります。極論を言うと、クラスを一切定義せずに OOP で実装するということもできます。*2

無名クラスをクラスの特殊系として見るのではなく、無名クラスが基本形で、それに名前を付けたのがクラスであるという捉え方もできます。

クラスは仕様と実装を結びつけたものに名前を付けたものという考え方は、PHP 5 でも適用できるます。こうした見方をすると、インターフェイスやトレイト、クラスの活用する方法が見えてくるのではないでしょうか。

*1:タイプヒンティングにインターフェイスを指定しても、それを実装したクラス定義が必要

*2:もちろん、クラスにはオブジェクトの属性を保持し、その操作を行うという側面もあるので、クラスは利用します。