みんな、あたらしい言語を覚えたらFizzBuzzを書きたがりますよね。
私も最終鬼畜FizzBuzz大全とかイキがってた時代がありました。現在はまさに約束された誰だって婆だってFizzBuzzが書ける2016年ですが、私は未だに逆FizzBuzzの解法に自信が持てません。
さて、新時代の言語と言へばPHP 7です。未だにオブジェクト指向がしっくりこないstaticおにいさんなのですが、新しい言語で初めてFizzBuzzを書くともなれば張りきってもしまひます。仕事ではがんばってPHP 7導入の準備を進めてるものの、まだ5系に留まってるので、うづうづしてる気持ちもあります。
そんなこともあって、どうせならエンタープライズに耐へうる品質で書きたい!
でもデザインパターンがすごく苦手でおぼえられないので、デザインパターン | TECHSCORE(テックスコア)にお世話になってにらめっこしながら、それっぽいクラス名とかメソッド名をつけていくことにします。英語とデザインパターンは本とか読んでも全然覚えられないんです。「英語と恋はうまくならない」ってモモーイも言ってた。
最初に呼び出し側のコードから。take()
は自作の補助函数です。これは配列とかforeach
でループ可能な何かの先頭$n
要素を取り出してくるだけのやつです。
<?php
use FizzBuzz;
$map = [
3 => '\FizzBuzz\FizzValue',
5 => '\FizzBuzz\BuzzValue',
];
$factory = new FizzBuzz\FizzBuzzFactory(new FizzBuzz\Mapper($map));
foreach (take($factory->generate(), 100) as $v) {
echo $v, PHP_EOL;
}
exit;
/**
* @param array|\Traversable $iter
* @param int $n
* @return \Generator
*/
function take($iter, $n)
{
$i = 0;
foreach ($iter as $x) {
if ($n <= $i++) { return; }
yield $x;
}
}
FizzBuzz\FizzBuzzFactory
はFizzBuzzStrategy
に従ってFizzBuzzしてくれるやつです。StrategyパターンとAbstractFactoryパターンの合せ技(キリッ のつもりですが、まちがってるかもしれません。FizzBuzz\Mapper
はFizzBuzz\FizzBuzzStrategy
を実装したクラスです。
FizzBuzz\Mapper
には連想配列を渡して、「3の倍数なら\FizzBuzz\FizzValue
」「5の倍数なら\FizzBuzz\BuzzValue
」と、動的に対応するクラス名の文字列を指定してやることができる仕組みです。
そうそう、エンタープライズの世界なので、ただの文字列じゃなくて値を表すオブジェクトを対象にすることにします。ここではabstract FizzBuzz\Value
って抽象クラスがそれです。それを継承したクラスとしてFizzValue
、BuzzValue
、NumberValue
を用意しました。これらはそれぞれ__toString()
メソッドを持ち、"Fizz"
, "Buzz"
、そして数字を十進数表現の文字列(たとへば"11"
)に文字列化することができます。
おやおやFizzとBuzzはわかったよ、じゃあ"FizzBuzz"
はどうやって出すんだね。みなさまご存じの通り、「3の倍数」であることと「5の倍数」であることは同時に成り立ちます。そして今回は縛りとして、abstract
なクラス以外は継承できないようにfinal class
にしてあります。そもそもPHPに多重継承はないし… そんなときのためのComposableValue
クラスです。こいつがあれば、FuzzBuzz\Value
を実装したクラスがいくつあろうと、状態を組み合せられます。やったぞ!
そろそろ説明おにいさんも飽きてきたのでコード見せます。
<?php
namespace FizzBuzz
{
// Strategy
interface FizzBuzzStrategy
{
public function fizzbuzzize(int $n) :Value;
}
final class Mapper implements FizzBuzzStrategy
{
/** @var array */
private $fizzbuzz_map;
/**
* @param array $fizzbuzz_map
*/
public function __construct(array $fizzbuzz_map)
{
$this->fizzbuzz_map = $fizzbuzz_map;
}
/**
* @param int $n
* @return Value
*/
public function fizzbuzzize(int $n) :Value
{
$vs = [];
foreach ($this->fizzbuzz_map as $m => $v) {
if ($n % $m == 0) {
$vs[] = ($v instanceof \Closure) ? $v($n) : new $v($n);
}
}
return (count($vs) > 0) ? new ComposableValue($n, ...$vs) : new NumberValue($n);
}
}
// Value
abstract class Value
{
/** @var int */
protected $n;
public function __construct(int $n)
{
$this->n = $n;
}
abstract function __toString();
}
final class NumberValue extends Value
{
public function __toString()
{
return sprintf("%d", $this->n);
}
}
trait SymbolToString
{
public function __toString()
{
return static::SYMBOL;
}
}
final class FizzValue extends Value
{
use SymbolToString;
const SYMBOL = 'Fizz';
}
final class BuzzValue extends Value
{
use SymbolToString;
const SYMBOL = 'Buzz';
}
final class ComposableValue extends Value
{
/** @var Value[] */
private $values = [];
public function __construct(int $n, Value ...$values)
{
parent::__construct($n);
$this->values = array_values($values);
}
public function add(Value $value)
{
$this->values[] = $value;
}
public function __toString()
{
$s = '';
foreach ($this->values as $v) {
$s .= $v;
}
return $s;
}
}
// Factory
final class FizzBuzzFactory
{
/** @var FizzBuzzStrategy */
private $strategy;
public function __construct(FizzBuzzStrategy $strategy)
{
$this->strategy = $strategy;
}
/**
* @param int $from
* @return \Generator
*/
public function generate(int $from = 1)
{
$n = $from;
while (true) {
yield $this->strategy->fizzbuzzize($n++);
}
}
}
}
(TODO: あとでリポジトリつくってGitHubに置く)
GitHubに置きました https://github.com/BaguettePHP/FizzBuzz
composer g require baguette/fizzbuzz
Composerでインストールしよう。
まとめ
だるいので、ここで使ってるPHPの言語機能を列挙するだけにしておきます ヾ(〃><)ノ゙
おまけ
なんでわざわざFizzBuzz\Mapper
なんてクラスを分けたのかって? それに、上のコードには無名クラスなんてどこにもないじゃないか。うんうん。
それはね、頭のとんがった上司に「チミィ、このプログラムに7の倍数だったときはNass
って出力する機能を追加してくれたまへよ」って言はれたときのためだよ。
$map = [
3 => '\FizzBuzz\FizzValue',
5 => '\FizzBuzz\BuzzValue',
7 => function (int $n) {
return new class($n) extends FizzBuzz\Value { use FizzBuzz\SymbolToString; const SYMBOL = 'Nass'; };
}
];