Skip to content

第 8 章: デザインパターンの適用

8.1 値オブジェクト(Value Object)

これまでの generate メソッドは文字列を返していました。しかし、FizzBuzz の結果には「元の数値」と「変換後の文字列」の 2 つの情報が含まれます。この 2 つを 1 つのオブジェクトとして表現するのが値オブジェクトです。

値オブジェクトの特徴

特徴 説明
不変性 一度生成したら変更できない(readonly)
等価性 同じ値を持つオブジェクトは等しい
自己記述性 文字列表現を持つ(__toString()

テスト

public function test_正の値で生成できる(): void
{
    $value = new FizzBuzzValue(1, '1');
    $this->assertSame(1, $value->getNumber());
    $this->assertSame('1', $value->getValue());
}

public function test_負の値で例外を発生する(): void
{
    $this->expectException(\InvalidArgumentException::class);
    new FizzBuzzValue(-1, '-1');
}

public function test_同じ値は等しい(): void
{
    $v1 = new FizzBuzzValue(1, '1');
    $v2 = new FizzBuzzValue(1, '1');
    $this->assertTrue($v1->equals($v2));
}

public function test_異なる値は等しくない(): void
{
    $v1 = new FizzBuzzValue(1, '1');
    $v2 = new FizzBuzzValue(2, '2');
    $this->assertFalse($v1->equals($v2));
}

public function test_文字列表現を返す(): void
{
    $value = new FizzBuzzValue(3, 'Fizz');
    $this->assertSame('Fizz', (string) $value);
}

実装

FizzBuzzValue の実装
<?php

declare(strict_types=1);

namespace App\Domain\Model;

final class FizzBuzzValue
{
    public function __construct(
        private readonly int $number,
        private readonly string $value,
    ) {
        if ($number < 0) {
            throw new \InvalidArgumentException('値は正の値のみ許可します');
        }
    }

    public function getNumber(): int
    {
        return $this->number;
    }

    public function getValue(): string
    {
        return $this->value;
    }

    public function equals(self $other): bool
    {
        return $this->number === $other->number
            && $this->value === $other->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

PHP の値オブジェクト設計のポイント:

  • コンストラクタプロモーション + readonly で不変性を保証
  • final クラスで継承を禁止(値オブジェクトは拡張しない)
  • __toString() マジックメソッドで文字列表現を提供
  • equals() メソッドで値の等価性を比較(PHP にはオブジェクト比較演算子のオーバーロードがないため)

8.2 FizzBuzzType の更新

タイプの generate メソッドが FizzBuzzValue を返すように更新します。

interface FizzBuzzType
{
    public function generate(int $number): FizzBuzzValue;
}

class FizzBuzzType01 implements FizzBuzzType
{
    public function generate(int $number): FizzBuzzValue
    {
        if ($number % 3 === 0 && $number % 5 === 0) {
            return new FizzBuzzValue($number, 'FizzBuzz');
        }
        if ($number % 3 === 0) {
            return new FizzBuzzValue($number, 'Fizz');
        }
        if ($number % 5 === 0) {
            return new FizzBuzzValue($number, 'Buzz');
        }

        return new FizzBuzzValue($number, (string) $number);
    }
}

8.3 ファーストクラスコレクション

FizzBuzz のリスト(FizzBuzzValue[])を直接操作する代わりに、専用のコレクション型で包みます。

テスト

public function test_配列からリストを生成する(): void
{
    $values = [
        new FizzBuzzValue(1, '1'),
        new FizzBuzzValue(2, '2'),
    ];
    $list = new FizzBuzzList($values);
    $this->assertSame(2, $list->count());
}

public function test_上限を超えると例外を発生する(): void
{
    $this->expectException(\InvalidArgumentException::class);
    $values = [];
    for ($i = 0; $i <= 100; $i++) {
        $values[] = new FizzBuzzValue($i, (string) $i);
    }
    new FizzBuzzList($values);
}

public function test_文字列配列を返す(): void
{
    $values = [
        new FizzBuzzValue(1, '1'),
        new FizzBuzzValue(3, 'Fizz'),
    ];
    $list = new FizzBuzzList($values);
    $strings = $list->toStringArray();
    $this->assertSame(['1', 'Fizz'], $strings);
}

public function test_文字列表現を返す(): void
{
    $values = [
        new FizzBuzzValue(1, '1'),
        new FizzBuzzValue(3, 'Fizz'),
    ];
    $list = new FizzBuzzList($values);
    $this->assertSame('1,Fizz', (string) $list);
}

実装

FizzBuzzList の実装
<?php

declare(strict_types=1);

namespace App\Domain\Model;

final class FizzBuzzList
{
    private const MAX_COUNT = 100;

    /** @var FizzBuzzValue[] */
    private readonly array $value;

    /**
     * @param FizzBuzzValue[] $value
     */
    public function __construct(array $value)
    {
        if (count($value) > self::MAX_COUNT) {
            throw new \InvalidArgumentException(
                sprintf('上限は%d件までです', self::MAX_COUNT)
            );
        }
        $this->value = $value;
    }

    /**
     * @return FizzBuzzValue[]
     */
    public function getValue(): array
    {
        return $this->value;
    }

    public function count(): int
    {
        return count($this->value);
    }

    /**
     * @return string[]
     */
    public function toStringArray(): array
    {
        return array_map(
            fn(FizzBuzzValue $v): string => $v->getValue(),
            $this->value
        );
    }

    public function __toString(): string
    {
        return implode(',', $this->toStringArray());
    }
}

ファーストクラスコレクションの特徴

特徴 実装方法
不変性 readonly プロパティ
カプセル化 コレクション操作をメソッドに集約
上限管理 MAX_COUNT で件数を制限
変換 toStringArray() で文字列配列に変換

8.4 Command パターン

FizzBuzz の操作をコマンドオブジェクトとしてカプセル化します。

テスト

public function test_FizzBuzzValueCommandで値を生成する(): void
{
    $type = new FizzBuzzType01();
    $command = new FizzBuzzValueCommand($type);
    $result = $command->execute(3);
    $this->assertInstanceOf(FizzBuzzValue::class, $result);
    $this->assertSame('Fizz', $result->getValue());
}

public function test_FizzBuzzListCommandでリストを生成する(): void
{
    $type = new FizzBuzzType01();
    $command = new FizzBuzzListCommand($type);
    $result = $command->execute();
    $this->assertInstanceOf(FizzBuzzList::class, $result);
    $this->assertSame(100, $result->count());
}

実装

Command パターンの実装
<?php

declare(strict_types=1);

namespace App\Application;

interface FizzBuzzCommand
{
    public function execute(int $number = 0): mixed;
}
<?php

declare(strict_types=1);

namespace App\Application;

use App\Domain\Model\FizzBuzzValue;
use App\Domain\Type\FizzBuzzType;

final class FizzBuzzValueCommand implements FizzBuzzCommand
{
    public function __construct(
        private readonly FizzBuzzType $type,
    ) {
    }

    public function execute(int $number = 0): FizzBuzzValue
    {
        return $this->type->generate($number);
    }
}
<?php

declare(strict_types=1);

namespace App\Application;

use App\Domain\Model\FizzBuzzList;
use App\Domain\Model\FizzBuzzValue;
use App\Domain\Type\FizzBuzzType;

final class FizzBuzzListCommand implements FizzBuzzCommand
{
    private const MAX_NUMBER = 100;

    public function __construct(
        private readonly FizzBuzzType $type,
        private readonly int $maxNumber = self::MAX_NUMBER,
    ) {
        if ($maxNumber > self::MAX_NUMBER) {
            throw new \InvalidArgumentException(
                sprintf('最大値は%d以下である必要があります', self::MAX_NUMBER)
            );
        }
    }

    public function execute(int $number = 0): FizzBuzzList
    {
        $values = [];
        for ($i = 1; $i <= $this->maxNumber; $i++) {
            $values[] = $this->type->generate($i);
        }

        return new FizzBuzzList($values);
    }
}

8.5 適用したデザインパターン

uml diagram

パターン 構成要素 役割
Value Object FizzBuzzValue 不変の値を表現
First-Class Collection FizzBuzzList コレクション操作のカプセル化
Strategy FizzBuzzType + 実装クラス アルゴリズムの交換
Factory Method FizzBuzz::create() インスタンス生成の集約
Command FizzBuzzCommand + 実装クラス 操作のオブジェクト化

8.6 まとめ

第 8 章で達成したこと:

  • 値オブジェクト(FizzBuzzValue): readonly + 等価性 + __toString()
  • ファーストクラスコレクション(FizzBuzzList): readonly + 上限管理
  • Command パターン: 操作のオブジェクト化
  • FizzBuzzType の戻り値を FizzBuzzValue に更新

TDD サイクルの実践

uml diagram