読者です 読者をやめる 読者になる 読者になる

CodeIQ Blog

自分の実力を知りたいITエンジニア向けの、実務スキル評価サービス「CodeIQ(コードアイキュー)」の公式ブログです。

『PHPでオブジェクト指向的FizzBuzz』問題の解説記事~PHPが書けてオブジェクト指向がわかるとイケてるエンジニアになれる!? #php #オブジェクト指向

問題解説

CodeIQ中の人、millionsmileです。

PHPメンターズの後藤秀宣さん出題の『オブジェクト指向的FizzBuzz』問題の解説記事です!

PHPは、開発言語別の求人数ランキングで2位であります(出典)。さらには、PHPが書けてオブジェクト指向がわかるエンジニアへの企業ニーズは高いものの、実際は、まだまだ層が薄いということもあり、今回の出題へ、となりました。

ぜひ解説記事を読んで、イケてるオブジェクト指向がわかるPHPエンジニアをめざしてみてはどうでしょう。

以下、問題文です。

FizzBuzz問題を解くアプリケーションを実装しているとします。

  ★FizzBuzz問題とは?
     1, 2, 3, ・・・という入力に対して3で割り切れる場合は「fizz」、5で割り切れる場合は「buzz」
     3でも5でも割り切れる場合は「fizzbuzz」、それ以外は数値をそのまま出力する

PHPコードは次の3ファイルで構成されます。
 ・fizzbuzz.php・・・実行スクリプト(問題にコードあり。ダウンロードファイルに含まれています)
 ・FizzBuzzApplication.php・・・アプリケーションクラス(作成してください)
 ・FizzBuzzSpecification.php・・・FizzBuzz仕様クラス(作成してください)

以下に示す実行スクリプトと各クラスの仕様を元に、コードを完成させてください。

■fizzbuzz.php
 実行スクリプトfizzbuzz.phpは、次の内容です。
 ダウンロードファイルに含まれていますので、変更せずそのまま利用してください。
=================================================
<?php
namespace CodeIQ;
require_once 'FizzBuzzSpecification.php';
require_once 'FizzBuzzApplication.php';
$app = new FizzBuzzApplication();
$app->addSpecAndMessage(new FizzBuzzSpecification(15), 'fizzbuzz');
$app->addSpecAndMessage(new FizzBuzzSpecification(3), 'fizz');
$app->addSpecAndMessage(new FizzBuzzSpecification(5), 'buzz');
$data = range(1,30);
$app->run($data);
=================================================

■FizzBuzzApplicationクラス
 FizzBuzzApplicationクラスは、今回のFizzBuzz問題全体を表します。

 ・addSpecAndMessage()メソッド
  引数で受け取った仕様と、仕様にマッチした場合のメッセージを対で格納します。
  仕様とメッセージの対は複数登録されます。
 ・run()メソッド
  問題のデータ(1次元の数値データの配列)を引数で受け取り、全データに対してFizzBuzz問題を実行します。

 ※必要に応じてプロパティや補助メソッドを実装してください。

■FizzBuzzSpecificationクラス
 FizzBuzz問題における、1つの条件を表します。たとえば「データが3で割り切れるかどうか」という条件に該当します。

 ・コンストラクタ
  条件のパラメータを受け取ります
 ・isSatisfiedBy()メソッド
  オブジェクトが表す条件にマッチするかどうかを判定します

 ※必要に応じてプロパティや補助メソッドを実装してください。

コード完成後fizzbuzz.phpを実行すると、結果が次のようになります。
(左側の二桁の数字は行番号ですので、アプリケーションの出力とは関係ありません)
=================================================
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
16
17
fizz
19
buzz
fizz
22
23
fizz
buzz
26
fizz
28
29
fizzbuzz
=================================================

■その他の要件
 コーディング規約はPSRに準拠してください。
 http://www.php-fig.org/

■解答方法
 php_fizzbuzz_answer.zip:
 https://dl.dropboxusercontent.com/u/110505645/CodeIQ/2013/07/php_fizzbuzz_answer.zip
 ZIPを解凍してお使いください。以下の2つのファイルが入っています。
 ・php_fizzbuzz_answer.txt
  解答用テキストファイル
 ・fizzbuzz.php
  問題文で使われているコード

 解答は、解答用テキストファイル「php_fizzbuzz_answer.txt」に書かれていることに従って解答ファイルを作成し、
 完成したらファイルアップロードで提出してください。


以下、出題者の後藤さんの解説記事です。
=====================================

問題文を分割する

今回はPHPでオブジェクト指向なコードを書く基礎スキルを問う問題でした。オブジェクト指向というと「どういうクラスを作るのか?」というモデリング(設計)に感心がいきがちですが、モデリングにおいては、かなり条件を限定しないと100%の正解というものはありません。今回の問題ではすでにモデリングが完了していた状態であり、問題からモデルを読み取ってPHPコードに落としこむというものでした。今回の問題のモデルはどうなっていたのかを考えてみますが、その前に、一度FizzBuzz問題の文章を振り返ってみます。

 1, 2, 3, ・・・という入力に対して3で割り切れる場合は「fizz」、5で割り切れる場合は「buzz」
 3でも5でも割り切れる場合は「fizzbuzz」、それ以外は数値をそのまま出力する

問題文は、次のように分割できます。

 (1)1, 2, 3, ・・・という入力に対して〜〜〜〜〜出力する
 (2)3で割り切れる場合は「fizz」
  5で割り切れる場合は「buzz」
  3でも5でも割り切れる場合は「fizzbuzz」
  それ以外は数値をそのまま

(1)は、FizzBuzz問題を解くアプリケーションの入出力を表しています。アプリケーション内部で(2)で表す処理を構成して実行していることになります。
(2)は、数値の配列の各要素に対して、どういった処理を行うのかを表しています。3つの条件と、各条件に対して表示するメッセージがあることが分かります。
ここまでを図にしたのが図1です。

[図1]
f:id:codeiq:20130807160031p:plain

図1は、現在ある問題がどのように(どのようなオブジェクトで)構成されているのか、そのままをモデルにしたものです。
問題をソフトウェアで解決するには、問題そのものをソフトウェアで表現しなくてはなりません。ここでいきなりデザインパターンのような技法を持ち込むと、モデルを歪めてしまう可能性があります。何らかの指針をベースとしてクラスの表現に置き換えていくのが望ましいでしょう。筆者は『エリック・エヴァンスのドメイン駆動設計』の考え方を元に、次のような指針を用いています。

 ・モデル駆動設計を重視する(問題空間のモデルをソフトウェアにそのまま表現すること)
 ・可変性のためのテクニックは、必要が生じた場合に導入する

優先順位としてモデル駆動設計の方を重視しますので、図1のモデルをできるだけそのままの形でソフトウェアに落としこんでいきます。
したがって、

 (1)「アプリケーション」オブジェクト
 (2)「3で割り切れる場合」オブジェクト
 (3)「5で割り切れる場合」オブジェクト
 (4)「3でも5でも割り切れる場合」オブジェクト

この4つをクラスで表現できればよさそうです。
ここで、(2)〜(4)は似たような機能を持っていることに着目します。それぞれ数値が満たすべき何らかの条件をあらわしています。ドメイン駆動設計では、このような条件を「仕様」オブジェクトであらわすパターンが紹介されています。割り切る数字は3つのオブジェクトの可変部分であるとみなせるので、数字部分を切り離すと「割り切れるかどうか仕様」クラスが導かれます。
結果として

 (1)アプリケーションクラス
 (2)割り切れるかどうか仕様クラス

この2つのクラスがあればよさそうです。

導き出したクラスを使って、今回の問題を表現しなおしてみましょう。やや回りくどい表現になってしまいますが、

 1, 2, 3, ・・・という入力をアプリケーションクラスのインスタンスで受け取り、
 各数字に対して
 割り切れるかどうか仕様クラス「15」のインスタンスで条件を満たす場合は「fizzbuzz」
 割り切れるかどうか仕様クラス「3」のインスタンスで条件を満たす場合は「fizz」
 割り切れるかどうか仕様クラス「5」のインスタンスで条件を満たす場合は「buzz」
 それ以外は数値をそのまま出力する

となります。(条件の実行順についての考慮を入れてあります)
実際はここまで設計を行った後にプログラムコードを記述していくわけです。fizzbuzz.phpスクリプトコードを見てみましょう。

[fizzbuzz.php]

<?php
namespace CodeIQ;

require_once 'FizzBuzzSpecification.php';
require_once 'FizzBuzzApplication.php';

$app = new FizzBuzzApplication();
$app->addSpecAndMessage(new FizzBuzzSpecification(15), 'fizzbuzz');
$app->addSpecAndMessage(new FizzBuzzSpecification(3), 'fizz');
$app->addSpecAndMessage(new FizzBuzzSpecification(5), 'buzz');

$data = range(1,30);

$app->run($data);

7行目から10行目の4行をみてみると、先ほどの「表現しなおした問題」で書かれたものとよく似ていると思いませんか?
この4行のコードは「アプリケーションを構成」しているコードです。最初にモデリングした図1の状態を組み立てている部分です。
fizzbuzz.phpスクリプトには「問題の構成」が分かりやすくあらわれています。問題の構成が変わった(6で割り切れる場合の条件が追加されたetc)場合、どこを編集すれば良いか想像がつきやすいですね。

現実のソフトウェアでは、問題解決のためのオブジェクト構造を組み立てるのに必要な情報はある程度複雑になり、組み立て自体も難しい処理になりますので、構成をコンフィギュレーションとして記述し、それをDIコンテナで組み立てるのが一般的です。今回の問題では単純化のために、コンフィギュレーションを起動スクリプト内で行なっているということになります。

アプリケーションオブジェクトが組み立て終わったら、入力データを用意してアプリケーションオブジェクトに渡し、処理を実行していますね。


さて、ここまでは出題時に用意したものの解説でした。
アプリケーションクラス、仕様クラスの中身を実装すれば完成となります。解説したモデルが頭に描けていれば、2つのクラスの実装は自ずと見えてくるでしょう。

仕様クラス

仕様クラスは、可変部分としている「割り切る数字(除数)」をコンストラクタで受け取りますので、これをメンバー変数に保存しておきます。
自身があらわす仕様を満たすかどうかチェックするisSatisfiedBy()メソッドで、この数字を使って割り切れるかどうかをチェックし、結果を返します。

<?php
namespace CodeIQ;

class FizzBuzzSpecification
{
    protected $divisor;

    public function __construct($divisor)
    {
        $this->divisor = $divisor;
    }

    public function isSatisfiedBy($number)
    {
        return ($number % $this->divisor == 0);
    }
}

解答頂いた中では、仕様クラスにメッセージも持たせるように変更された方、仕様とメッセージを対として格納するクラスを別途用意された方が複数いらっしゃいました。面白いですね。
また、1名の方は仕様クラスを抽象化して別の種類の仕様も扱えるようにすることにも言及されていました。

アプリケーションクラス

アプリケーションクラスは、問題を構成するためのaddSpecAndMessage()メソッドと、問題を実行するrun()メソッドを持ちます。
addSpecAndMessage()メソッドでは、渡された仕様オブジェクトとメッセージの組み合わせを、配列に保存しています。
run()メソッドでは、入力として受け取った配列の各要素に対してcheckSpecAndGetMessage()という補助メソッドを実行しています。
checkSpecAndGetMessage()では、仕様を順にチェックして満たしている仕様があれば、対応するメッセージを返しています。

<?php
namespace CodeIQ;

class FizzBuzzApplication
{
    private $specAndMessages;

    public function __construct()
    {
        $this->specAndMessages = [];
    }

    public function addSpecAndMessage(FizzBuzzSpecification $spec, $message)
    {
        $this->specAndMessages[] = ['spec'=>$spec, 'message'=>$message];
    }

    public function run($data)
    {
        array_walk($data, function($number) {
            echo $this->checkSpecAndGetMessage($number).PHP_EOL;
        });
    }

    public function checkSpecAndGetMessage($number)
    {
        foreach ($this->specAndMessages as $specAndMessage) {
            if ($specAndMessage['spec']->isSatisfiedBy($number)) {

                return $specAndMessage['message'];
            }
        }

        return $number;
    }
}

run()メソッドやcheckSpecAndGetMessage()メソッドの実装部分については、実装者が多少工夫を見せられる部分ですね。最低限クリアしておきたいのは、保持する仕様オブジェクトの数の可変性に対応したコードになっていることでしょう。
これで実装は完了ですので、ここまで動作するものができていれば正解です。

解答を頂いたみなさん、この基本部分の機能は動作していました。また、複数の方が「仕様を追加する順序」に依存しないよう、内部でソートを行うように工夫されていました。
問題の処理と仕様のチェックはほとんどの方がループ処理でしたが、translator/fallbackを使って仕様のチェックと出力への変換をチェーンしていくパターンの方が1名、visitorに分離された方が1名いらっしゃいました。
この部分の処理の分け方(メソッドを分ける、クラスを分ける)については、次に解説するテストの観点も持つと判断しやすくなります。

テストを実装する

せっかくなのでテストコードも記述してみましょう。

今回、条件にマッチするかどうかをあらわす仕様クラスと、仕様を使って数値配列を処理するアプリケーションクラスの2つに問題を分割しました。それぞれのクラスの持つ責務に着目してテストを記述します。
まずは仕様クラスですが、こちらは割り切れるかどうかの判定を行うのみですので、テストも単純です。PHPUnitのデータプロバイダを使ってコンストラクタに渡す引数とチェックする数字、チェック結果のパターンを作ってテストしています。

<?php
namespace CodeIQ\tests;

require_once 'PHPUnit/Framework/TestCase.php';
require_once '../FizzBuzzSpecification.php';

use CodeIQ\FizzBuzzSpecification;

class FizzBuzzSpecificationTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @test
     * @dataProvider 指定された数で割り切れるかどうかの判定データ
     */
    public function 指定された数で割り切れるかどうかの判定($divisor, $number, $expected)
    {
        $spec = new FizzBuzzSpecification($divisor);
        $this->assertEquals($expected, $spec->isSatisfiedBy($number));
    }

    public function 指定された数で割り切れるかどうかの判定データ()
    {
        return [
            [1, 1, true],
            [1, 2, true],
            [2, 1, false],
            [2, 2, true],
            [2, 3, false],
            [3, 1, false],
            [3, 2, false],
            [3, 3, true],
            [3, 4, false],
        ];
    }
}

次にアプリケーションクラスです。こちらは仕様クラスの中身には踏み込まず、アプリケーションクラスの仕事だけをテストしたいので、モックオブジェクトを上手く使ってテストします。
モッキングフレームワークにPhakeを利用するよう設定しておきます。
以下のテストコードでは、次の4種類のテストを行なっています。

 ・run()で入力データの1つ1つに対してcheckSpecAndGetMessage()が呼び出されていること
 ・checkSpecAndGetMessage()で仕様オブジェクトを使った検査が行われること
 ・複数の仕様がある場合に先頭から順に検査が行われマッチした以降は検査が行われないこと
 ・仕様にマッチしなかった場合は数値がそのまま返されること

このテストでは仕様オブジェクトはすべてモックオブジェクトで扱っていますので、実装には依存しません。
アプリケーションオブジェクトは基本的に実物をnewしますが、アプリケーションオブジェクト自身のメソッド呼び出しを検証したいテストでのみパーシャルモックにしています。

<?php
namespace CodeIQ\tests;

require_once '../vendor/autoload.php';
require_once '../FizzBuzzApplication.php';
require_once '../FizzBuzzSpecification.php';

use CodeIQ\FizzBuzzApplication;
use CodeIQ\FizzBuzzSpecification;

class FizzBuzzApplicationTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @test
     */
    public function 各入力に対してrun実行でcheckSpecAndGetMessage呼び出し()
    {
        $app = \Phake::partialMock('CodeIQ\FizzBuzzApplication');
        \Phake::when($app)->checkSpecAndGetMessage(\Phake::anyParameters())->thenReturn('');
        $data = [1,2,3];

        $app->run($data);

        \Phake::verify($app, \Phake::times(3))->checkSpecAndGetMessage(\Phake::anyParameters());
    }

    /**
     * @test
     */
    public function 仕様呼び出し()
    {
        $app = new FizzBuzzApplication();
        $number = 3;

        $spec = \Phake::mock('CodeIQ\FizzBuzzSpecification');
        \Phake::when($spec)->isSatisfiedBy($number)->thenReturn(true);
        $app->addSpecAndMessage($spec, 'spec1');

        $app->checkSpecAndGetMessage($number);

        \Phake::verify($spec)->isSatisfiedBy($number);
    }

    /**
     * @test
     */
    public function 複数仕様の場合にマッチした最初の仕様の結果が返る()
    {
        $app = new FizzBuzzApplication();
        $number = 3;

        $spec1 = \Phake::mock('CodeIQ\FizzBuzzSpecification');
        \Phake::when($spec1)->isSatisfiedBy($number)->thenReturn(false);
        $spec2 = \Phake::mock('CodeIQ\FizzBuzzSpecification');
        \Phake::when($spec2)->isSatisfiedBy($number)->thenReturn(true);
        $spec3 = \Phake::mock('CodeIQ\FizzBuzzSpecification');
        \Phake::when($spec3)->isSatisfiedBy($number)->thenReturn(true);

        $app->addSpecAndMessage($spec1, 'spec1');
        $app->addSpecAndMessage($spec2, 'spec2');
        $app->addSpecAndMessage($spec3, 'spec3');

        $ret = $app->checkSpecAndGetMessage($number);

        $this->assertEquals('spec2', $ret);
        \Phake::verify($spec1, \Phake::times(1))->isSatisfiedBy($number);
        \Phake::verify($spec2, \Phake::times(1))->isSatisfiedBy($number);
        \Phake::verify($spec3, \Phake::times(0))->isSatisfiedBy($number);
    }

    /**
     * @test
     */
    public function 仕様にマッチしない場合は値がそのままが返る()
    {
        $app = new FizzBuzzApplication();
        $number = 3;

        $spec1 = \Phake::mock('CodeIQ\FizzBuzzSpecification');
        \Phake::when($spec1)->isSatisfiedBy($number)->thenReturn(false);
        $spec2 = \Phake::mock('CodeIQ\FizzBuzzSpecification');
        \Phake::when($spec2)->isSatisfiedBy($number)->thenReturn(false);

        $app->addSpecAndMessage($spec1, 'spec1');
        $app->addSpecAndMessage($spec2, 'spec2');

        $ret = $app->checkSpecAndGetMessage($number);

        $this->assertEquals($number, $ret);
    }
}

テストの記述は問題には含めていませんので必須ではありませんが、解答に含めて頂いた方もいらっしゃいます。
受け入れテストで正しい解答になっていることをテストされていた方が1名、モックオブジェクトを使ってユニットテストを書かれた方が2名いらっしゃいました。
このアプリケーションクラスのように、オブジェクトの相互作用があるクラスのユニットテストを記述できることは、オブジェクト指向ソフトウェアを開発していく上で重要な基礎体力となります。是非ご自分のコードでそれぞれのクラスのユニットテストまで記述してみることをおすすめします。

最後に

FizzBuzz問題を通してオブジェクト指向で問題をとらえソフトウェアに表現する過程の一部を回答していただきました。
問題をどのようにモデリングするのかには、100%の正解はありません。
また、問題で示した実装も100%正解というわけでもありません。責務の観点でもう少し分割することも考えられるでしょうし、入出力の対称性等に着目した修正も考えられます。
「素数なら」という条件が追加されたら?
「結果をJSONに出力したい」という要望が追加されたら?
要求に合わせてモデルを成長させ、それとシンクロしてソフトウェアも成長させる。
この流れが会得できれば、あなたも立派なオブジェクト指向エンジニアです。

参考書籍

・エリック・エヴァンスのドメイン駆動設計
http://www.amazon.co.jp/dp/4798121967/

・オブジェクトデザイン
http://www.amazon.co.jp/dp/4798109037/

・実践テスト駆動開発
http://www.amazon.co.jp/dp/4798124583/

PHPでのオブジェクト指向開発ノウハウなどについて興味のある方は、PHPメンターズブログも是非チェックしてください。
・ブログ http://phpmentors.jp/
・twitter https://twitter.com/phpmentors
・Facebook https://www.facebook.com/pages/PHPメンターズ/350233588424604
=================================================

さすがはPHPでオブジェクト指向ノウハウについて、記事を書いたり、トレーニングしたりしている後藤さんだけあって、わかりやすい説明ですね。オブジェクト指向的にどう考えるのかというプロセスを書いてくれるのは勉強になりますね。

https://codeiq.jp/ace/goto_hidenori/q374
f:id:codeiq:20130807162640j:plain

これからも面白い問題や役立つ問題などいろいろ出していきますので、
CodeIQをよろしくお願いします!

エンジニアのための新しい転職活動!CodeIQのウチに来ない?の特集ページを見る