プログラミング5年目の「FizzBuzz」
目次
経緯
まずは自己紹介ですが、私は大学を卒業し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で割るという計算方法やFizz
やBuzz
という計算結果の出力という概念を次のように抽象化し拡張します。
- ある数字に対して、次の行為を複数回適用し、その結果をまとめて出力する
- ある数字に対して計算を行い、ある条件に合致した場合は何かの文字列を出力する
- ただし、計算方法と条件、条件に合致した場合の出力は同じものである必要はない
クラス図について
コードの解説
上記のクラス図を元にコードを解説していきます。
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
と命名しています。
具体的な計算手段と条件に合致した時の結果についての処理はそれぞれCalculator
とResult
に委譲しています。
<?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
クラスを生成するのが複雑になりました。
そのため、Division
やAddition
などの計算手段に対応した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年間で値オブジェクトやファーストクラスコレクションなどのテクニックを覚えて使えるまで成長することが出来たと思います。
ただ、実際の業務では設計力不足を感じる場面が多いので、これからも勉強を続けていきたいです。
Discussion