変更に強いFizz&Buzz
一般的な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