🐝

プログラミング5年目の「FizzBuzz」

2024/04/29に公開

目次

  1. 経緯
  2. FizzBuzzとは
  3. 設計
  4. コードの解説
  5. テスト
  6. まとめ

経緯

まずは自己紹介ですが、私は大学を卒業しIT関連の企業に就職し今年でプログラミング歴が5年目となるエンジニアです。

最近、新卒でプログラミングを学びたての頃にFizzBuzzを書いてif文を理解した時のことを思い出しました。

そこで4年ほどのエンジニア生活の成長を振り返ると言う意味でも改めてFizzBuzzを書いてみようと思います。

FizzBuzzとは

そもそもFizzBuzzとは何かについてですが、ある数字を3で割り切れた場合はFizzを、5で割り切れた場合はBuzzを、3と5で割り切れた場合はFizzBuzzを出力するというものです。

プログラミングの入門書などでは大抵以下のようなコードとなっていることが多いと思います。

<?php

function fizzBuzz(int $number): string {
    if (($number % 3 === 0) && ($number % 5 === 0)) {
        return 'FizzBuzz';
    } elseif ($number % 3 === 0) {
        return 'Fizz';
    } elseif ($number % 5 === 0) {
        return 'Buzz';
    }
    return '';
}

echo fizzBuzz(15);
// FizzBuzz

設計

ルールの拡張

FizzBuzzを改めて設計するにあたり元のFizzBuzzから、ある数字を3や5で割るという計算方法やFizzBuzzという計算結果の出力という概念を次のように抽象化し拡張します。

  1. ある数字に対して、次の行為を複数回適用し、その結果をまとめて出力する
  2. ある数字に対して計算を行い、ある条件に合致した場合は何かの文字列を出力する
  3. ただし、計算方法と条件、条件に合致した場合の出力は同じものである必要はない

クラス図について

クラス図

コードの解説

上記のクラス図を元にコードを解説していきます。

NaturalNumber

このクラスは割り算を扱うに当たって0で割って例外が起こらないようにするため、0より大きい数字を持つという責務を持つ自然数の値オブジェクトクラスです。

<?php

final class NaturalNumber
{
    private const LOWER_LIMIT = 0;

    public function __construct(
        public readonly int $value,
    ) {
        if ($value <= self::LOWER_LIMIT) {
            throw new InvalidArgumentException();
        }
    }
}

Converter

このクラスはある数字に対して計算を行い条件に合致したかを判断して結果を出力する責務を持っています。

このクラスが自然数を入力し文字列を出力することから変換するという意味でConverterと命名しています。

具体的な計算手段と条件に合致した時の結果についての処理はそれぞれCalculatorResultに委譲しています。

<?php

final class Converter
{
    public function __construct(
        private readonly Result $result,
        private readonly Calculator $calculator
    ) {}

    public function convert(NaturalNumber $targetNumber): string
    {
        return $this->result->get($this->calculator->calculate($targetNumber));
    }
}

Calculator

具体的な計算手段を柔軟に切り替え可能なものとするためCalculatorインターフェースを使用します。

このインターフェースによってConverterクラスが具体的な計算手段に依存することなく、依存性が逆転した状態になります。

<?php

interface Calculator
{
    public function calculate(NaturalNumber $targetNumber): bool;
}

Division

DivisionクラスはFizzBuzzの15を3で割り切れるかの計算を行う責務を持ったクラスです。

<?php

class Division implements Calculator
{
    private const SUCCESS_VALUE = 0;

    public function __construct(
        private readonly NaturalNumber $denominator,
    ) {}

    public function calculate(NaturalNumber $targetNumber): bool
    {
        $remainder = $targetNumber->value % $this->denominator->value;

        return $remainder === self::SUCCESS_VALUE;
    }
}

Addition

AdditionクラスはDivisionクラスでは割り算しか扱えなかったため、別の計算方法である足し算を行う責務を持ったクラスです。

<?php

class Addition implements Calculator
{
    public function __construct(
        private readonly NaturalNumber $addingNumber,
        private readonly NaturalNumber $successNumber,
    ) {}

    public function calculate(NaturalNumber $targetNumber): bool
    {
        $addedNumber = $targetNumber->value + $this->addingNumber->value;

        return $addedNumber === $this->successNumber->value;
    }
}

Result

このクラスは計算の結果、条件に合致したかどうか(計算が成功したかどうか)を受け取り、成功した場合にはある文字列を、失敗した場合は空の文字列を出力する責務を持ったクラスです。

<?php

final class Result
{
    private const FAILED_VALUE = '';

    public function __construct(
        private readonly string $successValue,
    ) {
        if ($successValue === self::FAILED_VALUE) {
            throw new InvalidUrlException();
        }
    }

    public function get(bool $isSuccess): string
    {
        return $isSuccess ? $this->successValue : self::FAILED_VALUE;
    }
}

FizzBuzz

FizzBuzzを実行するクラスが以下のFizzBuzzクラスです。

FizzBuzzクラスはConverterを複数持ち、自然数を文字列に変換した結果をまとめて出力する責務を持つファーストクラスコレクションになります。

<?php

final class FizzBuzz
{
    public function __construct(
        private readonly array $converters,
    ) {}

    public function execute(NaturalNumber $targetNumber): string
    {
        return $this->summarize($targetNumber);
    }

    private function summarize(NaturalNumber $targetNumber): string
    {
        $summary = '';

        foreach($this->converters as $converter) {
            $summary .= $converter->convert($targetNumber);
        }

        return $summary;
    }
}

Factoryについて

FizzBuzzを行うには、Converterクラスを生成してFizzBuzzクラスにセットする必要がありますが、Converterクラスを生成するのが複雑になりました。

そのため、DivisionAdditionなどの計算手段に対応したConverterを生成するFactoryクラスを使用します。

Factoryクラスでは副作用を扱いたくないため、create()はあえて静的メソッドとしています。

<?php

final class DivisionConverterFactory
{
    public static function create(
        string $successValue,
        NaturalNumber $denominator
    ): Converter {
        return new Converter(
            new Result($successValue),
            new Division($denominator)
        );
    }
}

final class AdditionConverterFactory
{
    public static function create(
        string $successValue,
        NaturalNumber $addingNumber,
        NaturalNumber $successNumber
    ): Converter {
        return new Converter(
            new Result($successValue),
            new Addition($addingNumber, $successNumber)
        );
    }
}

テスト

最後にテストで動作を確認します。

<?php

use PHPUnit\Framework\TestCase;

class FizzBuzzTest extends TestCase
{
    /**
     * @dataProvider dataProviderExecute
     */
    public function testExecute(
        array $calculators,
        NaturalNumber $targetNumber,
        string $expected
    ): void {
        $fizzBuzz = new FizzBuzz($calculators);

        $this->assertSame($expected, $fizzBuzz->execute($targetNumber));
    }

    public static function dataProviderExecute(): array
    {
        return [
            [
                [
                    DivisionConverterFactory::create('Fizz', new NaturalNumber(3)),
                    DivisionConverterFactory::create('Buzz', new NaturalNumber(5)),
                ],
                new NaturalNumber(15),
                'FizzBuzz',
            ],
            [
                [
                    DivisionConverterFactory::create('Cookie', new NaturalNumber(2)),
                    DivisionConverterFactory::create('Chocolate', new NaturalNumber(7)),
                ],
                new NaturalNumber(15),
                '',
            ],
            [
                [
                    AdditionConverterFactory::create('Fruits', new NaturalNumber(2),  new NaturalNumber(5)),
                    AdditionConverterFactory::create('Beef', new NaturalNumber(7),  new NaturalNumber(11)),
                ],
                new NaturalNumber(3),
                'Fruits',
            ],
        ];
    }
}

まとめ

以上がプログラミング5年目のFizzBuzzになります。

ちょうぜつソフトウェア設計入門』を読んで勉強していたため、書籍内のサンプルコードをアレンジしたようなコードになってしまいました。

この4年間で値オブジェクトやファーストクラスコレクションなどのテクニックを覚えて使えるまで成長することが出来たと思います。

ただ、実際の業務では設計力不足を感じる場面が多いので、これからも勉強を続けていきたいです。

https://github.com/naoyuki42/oop-fizzbuzz

Discussion