毎週土曜夜はワンオペ育児をしている岩原です。こんにちは。
皆さん、FizzBuzz問題って解いたことありますか?
Fizz Buzz - Wikipedia
今回は、そのFizzBuzz問題をカルテット的考え方に基づいて解いてみようと思います。

まずは単純に

何も考えずに書くとこうなるかと思います。

<?php

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

for ($i=1; $i <= 100; $i++) { 
    $ret = fizzBuzz($i);
    echo $ret . ' ';
    if ($i % 10 == 0) {
        echo "\n";
    }
}

バリエーションを考える

PHPerKaigiで「設計力を上げる!バリエーションの見極め術」を発表しました | QUARTETCOM TECH BLOGということで、まずはバリエーションの見極めをします。
と言っても、今回のバリエーションは以下の部分しかありませんね。

    if ($val % 3 === 0 && $val % 5 === 0){
        return "FizzBuzz";
    }elseif ($val % 3 === 0) {
        return "Fizz";
    }elseif ($val % 5 === 0){
        return "Buzz";
    }else{
        return "$val";
    }

ここのif-elseは今後も増えていきそうですね。
その点に注意してカルテット流にするとこんな感じになります。

<?php

interface SpeakerInterface{
    public function say(int $val) : string;

    public function supports(int $val) : bool;
}

class FizzSpeaker implements SpeakerInterface{

    public function say(int $val) : string{
        return "Fizz";
    }

    public function supports(int $val): bool
    {
        return $val % 3 === 0;
    }
}

class BuzzSpeaker implements SpeakerInterface{

    public function say(int $val) : string{
        return "Buzz";
    }
    public function supports(int $val): bool
    {
        return $val % 5 === 0;
    }
}

class FizzBuzzSpeaker implements SpeakerInterface{

    public function say(int $val) : string{
        return "FizzBuzz";
    }

    public function supports(int $val): bool
    {
        return $val % 3 === 0 && $val % 5 === 0;
    }
}

class NumberSpeaker implements SpeakerInterface{
    public function say(int $val) : string{
        return "$val";
    }

    public function supports(int $val): bool
    {
        return true;
    }
}

class SpeakerResolver{
    private $speakerList = [];

    public function add(SpeakerInterface $speaker)
    {
        $this->speakerList[] = $speaker;
    }

    public function resolve(int $val) : SpeakerInterface
    {
        foreach ($this->speakerList as $speaker) {
            if($speaker->supports($val)){
                return $speaker;
            }
        }
        throw new \Exception("対応していない値です!");
        
    }
}

function fizzBuzz(SpeakerResolver $resolver, int $val) : string {
    $speaker = $resolver->resolve($val);
    return $speaker->say($val);
}

$resolver = new SpeakerResolver();
$resolver->add(new FizzBuzzSpeaker());
$resolver->add(new FizzSpeaker());
$resolver->add(new BuzzSpeaker());
$resolver->add(new NumberSpeaker());

for ($i=1; $i <= 100; $i++) { 
    $ret = fizzBuzz($resolver, $i);
    echo $ret . ' ';
    if ($i % 10 == 0) {
        echo "\n";
    }
}

単なるFizzBuzzになんて長さだ!と思うかもしれません。自分でもそう思います。
では1つずつ解説してきます。

解説

SpeakerInterface

今回の肝となるInterfaceです。
弊社ではオブジェクト指向の中でもとりわけポリモーフィズムを重視しています。
SpeakerInterfaceにsayメソッドとsupportsメソッドを定義し、
それぞれ「何を話すのか」と「どんな条件なのか」を実装させます。

FizzSpeaker、BuzzSpeaker、FizzBuzzSpeaker、NumberSpeaker

SpeakerInterfaceを実装したクラスになります。

それぞれ「何を話すのか」と「どんな条件なのか」を実装させます。

を実際に実装したクラスになります。 SpeakerInterfaceを実装したクラスを増やしていくことで、バリエーションの増加への対応が楽になります。

SpeakerResolver

各Speakerたちをまとめて、どのSpeakerクラスのsayメソッドを呼び出せばよいかを管理するクラスになります。
addされた順に判定していきます。 弊社ではよく使うパターンだったりします。 PhpStormでコードテンプレートを使って*Resolverクラスを楽に作る | QUARTETCOM TECH BLOG

実際にバリエーションを増やしてみる

バリエーションとして、「7の倍数の場合はHoge!と出力する」を追加してみます。 差分はこちら。

     }
 }

+class HogeSpeaker implements SpeakerInterface{
+    public function say(int $val) : string{
+        return "Hoge!";
+    }
+
+    public function supports(int $val): bool
+    {
+        return $val % 7 === 0;
+    }
+}
+
 class SpeakerResolver{
     private $speakerList = [];

 $resolver->add(new FizzBuzzSpeaker());
 $resolver->add(new FizzSpeaker());
 $resolver->add(new BuzzSpeaker());
+$resolver->add(new HogeSpeaker());
 $resolver->add(new NumberSpeaker());

 for ($i=0; $i < 100; $i++) {

こんな感じになります。 変更差分は大きく感じますが、非常にわかりやすい差分になってるかと思います。

ちなみに、単純実装の場合はこんな感じですね。 なんとなくバグを埋め込みそうな変更ですね… 😩

         return "Fizz";
     }elseif ($val % 5 === 0){
         return "Buzz";
+    }elseif($val % 7 === 0){
+        return "Hoge!";
     }else{
         return "$val";
     }

発展型

ここでは省略しますが、さらにオブジェクト指向的な考えとして、条件をオブジェクト化するというのもあります。
各Speakerクラスのsupportメソッドの内容をクラスの外から渡せるようにする、というイメージです。
ラムダや関数オブジェクトを渡すというのもありですね。
外から条件を渡せるようになると、条件の組み換えをクラスの外からできるようになるので、かなり汎用性を持つことになります。

まとめ

カルテット流FizzBuzzの回答例はどうでしたか?
FizzBuzzだとあまり効果が分かりづらいかと思いますが、DIやテストコードと組み合わせると効果がわかるようになるかと思います。
ココだけの話、バックエンドエンジニアの実技試験は、「オブジェクト指向でどれだけ書けるか」というのも見るのですが、
今回提示したぐらい書けると良い印象になるかと思います(保証はできませんが… 😔)
もちろん、FizzBuzz問題ではありませんのであしからず。