🙄

変更に強いFizz&Buzz

2025/04/08に公開

一般的なFizz&Buzz

具体的に、変更に強いコードをFizz&Buzzで表現してみる。

function fizzBuzzOne(num) {
  if (num % 15 === 0) {
    return "FizzBuzz";
  } else if (num % 3 === 0) {
    return "Fizz";
  } else if (num % 5 === 0) {
    return "Buzz";
  } else {
    return num;
  }
}

まずこれが、一般的なFizzBuzz関数である。

具体的な関数の内容は省かせてもらう。

一見よさそうに見えるが、FizzBuzzの文字が変更される可能性がある。
プラスアルファのルールが追加される可能性もある。
そうなった場合、関数を直接変更しなければならなくなり、変更に強いコードとは言えない。

変更に強い設計の考え方

ここから本質と仕様に分けていく。

本質は、
整数を受け取ったら、文字列を返す。
任意のルールは複数定義できる。
変換ルールは、特定の条件を満たしているときに実行される。
変換結果は蓄積される。

本質を見つけ出す

まず、ルールが満たされているときと、ルールを適応する抽象を作る。
これは、マッチする処理と、ルールを適応する処理である。

interface ReplaceRuleInterface {
    public function match(string $carry, int $number): bool;
    public function apply(string $carry, int $number): string;
}

ルールを適応する処理を実装

その後、

複数のルールを適応する処理を実装する。

class NumberConverter
{
    /**
     * コンストラクタで複数のルールを受け取る
     * 
     * @param ConvertRuleInterface[] $rules
     */
    public function __construct(protected array $rules)
    { }
    
    public function convert(int $n): string
    {
        $carry = "";

        foreach ($this->rules as $rule) {
            if ($rule->match($carry, $n)) {
                $carry = $rule->apply($carry, $n);
            }
        }

        return $carry;
    }
}

ここからは実際のルールの実装

ルールをある文適応させ、ルールがあるなら、それをforeachであるまで繰り返す。
結果、carryを返す。

ここでは、ルールを作っているのだが、ポリモーフィズムで、matchとapplyを利用している。
NumberConverterでのmatchとapplyは詳細を知らなくていい。

CyclicNumberRuleにルールとなる数字と、文字を渡すことで、数字が一致するとき、applyで文字が追加される。

class CyclicNumberRule implements ReplaceRuleInterface
{
    // コンストラクタで base と replacement を受け取る
    public function __construct(
        protected int $base,
        protected string $replacement
    ) {}

    // n が base で割り切れるかをチェック
    public function match(string $carry, int $n): bool
    {
        return $n % $this->base == 0;  // n が base の倍数である場合
    }

    // carry に replacement を追加して返す
    public function apply(string $carry, int $n): string
    {
        return $carry . $this->replacement;  // carry に replacement を追加
    }
}

こちらは、carryの文字が空だったら、数字を返す処理である。

class PassthroughRule implements ReplaceRuleInterface
{
    // match メソッド:carry が空文字の場合に適用される
    public function match(string $carry, int $n): bool
    {
        return $carry === "";  // carry が空文字の場合に true を返す
    }

    // apply メソッド:carry が空文字なら、n を文字列にして返す
    public function apply(string $carry, int $n): string
    {
        return (string)$n;  // 数字 n を文字列にして返す
    }
}

結局、NumberConverterとCyclicNumberRule,PassthroughRuleがReplaceRuleInterfaceに依存している形になっている。

この実装により、オープン・クローズドの法則が適応された、拡張に対しては開いていて、修正に対して閉じている設計になった。

本質の部分を実装し、順に抽象度の高いものから実装していく

本質を見抜くことは難しいが、訓練をしてつよくなりたい。

引用:ちょうぜつソフトウェア設計入門――PHPで理解するオブジェクト指向の活用 著者:田中ひさてる

Discussion