OO厨(?)の俺がFizzBuzz書いてみた

お題:

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

どうしてプログラマに・・・プログラムが書けないのか?

実装の前に考えた

  • 「1から100までの数を」
    • 将来的には1から100とは限らないな。そこは設定出来るようにしよう
    • そもそも数だけか?文字列とか他のオブジェクトに変わる事は?
    • とりあえず元データを生成する部分を別クラスにしよう!
  • 「プリントする」何を何処に?
    • 数字と文字列があるらしい→任意の文字列をプリント出来る
    • 標準出力とは限らないな……
    • 改行するのか、間に空白を入れるのか……その辺は将来変わるかも
    • とりあえず文字列を出力する部分を別クラスにしよう!
  • 「○○の倍数のときは数の代わりに「△△」と」
    • 同じような条件が多いな
    • ○○と△△を変えられるようにすれば一つのロジックで実装出来そうだな
  • 条件によって「数」「Fizz」「Buzz」「FizzBuzz」のどれか一つか……
    • 個々の判定&出力ロジックを別オブジェクトにして、Chains of Responsibilityにするのが良そうだな
  • 将来的にナベアツ問題にも対応出来るようにしたい
    • FizzBuzz固有部分は分離しよう

実装した(基礎部分)

元データを順番に生成するやつとそれを一つずつ処理する奴と、その二つをつなげて動かす奴が必要だ。


生成する奴(インタフェース)

package sample.sequence;

public interface SequenceGenerator<T> {
    /** @return 次のオブジェクトを取り出せる時はtrueを戻す */
    boolean hasNext();
    /** @return 次のオブジェクトを戻す */
    T next();
}

一つずつ処理する奴(インタフェース)

package sample.sequence;

public interface SequenceProcessor<T> {
    void process(T item);
}

つなげて動かす奴

package sample.sequence;

public class SequenceEngine<T> {
    private SequenceGenerator<T> generator;
    private SequenceProcessor<T> processor;

    public SequenceEngine(SequenceGenerator<T> generator, SequenceProcessor<T> processor) {
        super();
        if (generator == null) {
            throw new IllegalArgumentException("generator is null.");
        }
        if (processor == null) {
            throw new IllegalArgumentException("processor is null.");
        }
        this.generator = generator;
        this.processor = processor;
    }

    public void run() {
        while (this.generator.hasNext()) {
            this.processor.process(this.generator.next());
        }
    }
}


SequenceProcessorをChains of Responsibility化するのに抽象スーパークラスを作る

package sample.sequence;

public abstract class AbstractSequenceProcessor<T> implements SequenceProcessor<T>{
    private SequenceProcessor<T> next;

    public void setNext(SequenceProcessor<T> next) {
        this.next = next;
    }

    public void process(T item) {
        if (item == null) {
            throw new IllegalArgumentException("item is null.");
        }
        
        if (! doProcess(item) && this.next != null) {
            this.next.process(item);
        }
    }

    /** 自分が処理する場合trueを戻す */
    protected abstract boolean doProcess(T item);   
}

文字列をプリントするクラスを作る。

package sample.sequence;

/**
 * 文字列をプリントするインタフェース
 */
public interface WordPrinter {
     void printWord(String word);
}
package sample.sequence;

import java.io.PrintStream;

/**
 * 文字列をPrintStreamを使ってプリントするクラス
 */
public class PrintStreamWordPrinter implements WordPrinter {

    private PrintStream printStream;
    public PrintStreamWordPrinter(PrintStream printStream) {
        super();
        if (printStream == null) {
            throw new IllegalArgumentException("printStream is null.");
        }
        this.printStream = printStream;
    }
    public void printWord(String word) {
        this.printStream.println(word);
    }

}

1から100までの数を順番に生成するクラス。

package sample.sequence;

public class NumberSequenceGenerator implements SequenceGenerator<Integer> {

    private int max;
    private int current;
    
    /**
     * minからmaxまでの数字を順に出力するSequenceGeneratorを生成する
     * @param min 最小値
     * @param max 最大値
     */
    public NumberSequenceGenerator(int min, int max) {
        super();
        if (min > max) {
            throw new IllegalArgumentException("'max' should be greator than 'min.'");
        }
        this.max = max;
        this.current = min;
    }
    
    /** @see SequenceGenerator#hasNext() */
    public boolean hasNext() {
        return this.current <= this.max;
    }
    /** @see SequenceGenerator#next() */
    public Integer next() {
        if (!hasNext()) {
            throw new IndexOutOfBoundsException();
        }
        return this.current++;
    }
}

実装した(FizzBuzz固有部分)

割り切れる時に文字列を出力するSequenceProcessor

package sample.sequence.fizzbuzz;

import sample.sequence.AbstractSequenceProcessor;
import sample.sequence.WordPrinter;
/**
 * 渡された数字が特定の整数で割り切れた時に特定の文字列をプリントするクラス
 */
public class PrintWhenDivisibleProcessor extends AbstractSequenceProcessor<Integer> {
    private WordPrinter wordPrinter;

    private String word;
    private int divider;
    /**
     * @param word 割り切れる時にプリントする文字列
     * @param divider 割り切れるかの判定をおこなう数
     * @param wordPrinter 文字列をプリントする為のオブジェクト
     */
    public PrintWhenDivisibleProcessor(String word, int divider, WordPrinter wordPrinter) {
        super();
        if (wordPrinter == null) {
            throw new IllegalArgumentException("wordPrinter is null.");
        }
        if (divider == 0) {
            throw new IllegalArgumentException("divider should not be zero.");
        }
        this.wordPrinter = wordPrinter;
        this.word = word;
        this.divider = divider;
    }

    @Override
    protected boolean doProcess(Integer item) {
        if (item % this.divider == 0) {
            this.wordPrinter.printWord(this.word);
            return true;
        }
        return false;
    }
}

渡された数字をそのままプリントするSequenceProcessor

package sample.sequence.fizzbuzz;

import sample.sequence.AbstractSequenceProcessor;
import sample.sequence.WordPrinter;

/**
 * 渡された数字をプリントするクラス
 */
public class PrintItemProcessor extends AbstractSequenceProcessor<Integer> {
    private WordPrinter wordPrinter;
    
    /**
     * @param wordPrinter 文字列をプリントする為のオブジェクト
     */
    public PrintItemProcessor(WordPrinter wordPrinter) {
        super();
        if (wordPrinter == null) {
            throw new IllegalArgumentException("wordPrinter is null.");
        }
        this.wordPrinter = wordPrinter;
    }

    @Override
    protected boolean doProcess(Integer item) {
        this.wordPrinter.printWord(item.toString());
        return true;
    }
}

テスト

と言ってもクラスのユニットテストじゃなくてFizzBuzzの出力確認だけだけのもの。

package sample.sequence.fizzbuzz;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import junit.framework.TestCase;

import sample.sequence.NumberSequenceGenerator;
import sample.sequence.PrintStreamWordPrinter;
import sample.sequence.SequenceEngine;

public class FizzBuzzTest extends TestCase {

    private static final String FIZZBUZZ_ANS = "1?n2?nFizz?n4?nBuzz?nFizz?n" +
    		"7?n8?nFizz?nBuzz?n11?nFizz?n13?n14?nFizzBuzz?n16?n17?nFizz?n19?n" +
    		"Buzz?nFizz?n22?n23?nFizz?nBuzz?n26?nFizz?n28?n29?nFizzBuzz?n31?n" +
    		"32?nFizz?n34?nBuzz?nFizz?n37?n38?nFizz?nBuzz?n41?nFizz?n43?n44?n" +
    		"FizzBuzz?n46?n47?nFizz?n49?nBuzz?nFizz?n52?n53?nFizz?nBuzz?n56?n" +
    		"Fizz?n58?n59?nFizzBuzz?n61?n62?nFizz?n64?nBuzz?nFizz?n67?n68?n" +
    		"Fizz?nBuzz?n71?nFizz?n73?n74?nFizzBuzz?n76?n77?nFizz?n79?nBuzz?n" +
    		"Fizz?n82?n83?nFizz?nBuzz?n86?nFizz?n88?n89?nFizzBuzz?n91?n92?n" +
    		"Fizz?n94?nBuzz?nFizz?n97?n98?nFizz?nBuzz?n";

    @Override
    protected void setUp() throws Exception {
        super.setUp();
    }
    public void testFizzbuzz() {
        // 出力内容を確認する為にByteArrayOutputStreamを使用
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        // FizzBuzz問題を解くオブジェクトを作成して実行
        SequenceEngine<Integer> fizzbuzzEngine = createFizzbuzzEngine(out);
        fizzbuzzEngine.run();
        
        // 出力された文字列をチェック
        String results = new String(out.toByteArray());
        String expected = FIZZBUZZ_ANS;
        
        assertEquals(expected, results);
    }
    private SequenceEngine<Integer> createFizzbuzzEngine(OutputStream out) {
        // OutputStreamに書き出すwordPrinterを作成
        PrintStream ps = new PrintStream(out);
        PrintStreamWordPrinter wordPrinter = new PrintStreamWordPrinter(ps);
        
        // Fizz/Buzz/FizzBuzz/数字を出力するSequenceProcessorを作成
        PrintWhenDivisibleProcessor fizzProcessor = 
            new PrintWhenDivisibleProcessor("Fizz", 3, wordPrinter);
        PrintWhenDivisibleProcessor buzzProcessor = 
            new PrintWhenDivisibleProcessor("Buzz", 5, wordPrinter);
        PrintWhenDivisibleProcessor fizzbuzzProcessor = 
            new PrintWhenDivisibleProcessor("FizzBuzz", 15, wordPrinter);
        PrintItemProcessor otherProcessor = 
            new PrintItemProcessor(wordPrinter);
        // SequenceProcessorのチェーンを生成
        fizzbuzzProcessor.setNext(buzzProcessor);
        buzzProcessor.setNext(fizzProcessor);
        fizzProcessor.setNext(otherProcessor);
        
        // 1から100までの数字を生成するSequenceGeneratorを作成
        NumberSequenceGenerator generator =
            new NumberSequenceGenerator(1, 100);
        
        // 実際にFizzBuzz問題を解くオブジェクトを作成
       SequenceEngine<Integer> fizzbuzzEngine =
            new SequenceEngine<Integer>(generator, fizzbuzzProcessor);
        return fizzbuzzEngine;
    }

}

DIコンテナでの設定例

fizzbuzzEngineの組み立てをDIコンテナ(S2)で実施


fizzbuzz.dicon

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN"
	"http://www.seasar.org/dtd/components21.dtd">
<components>
	<!-- 標準出力に出すだけならこちらで -->
	<!-- 
	<component name="printStream">
		@java.lang.System@out
	</component>
	 -->
	<component name="out" class="java.io.ByteArrayOutputStream" />
	<component name="printStream" class="java.io.PrintStream">
		<arg>out</arg>
	</component>
	<component name="wordPrinter"
	    class="sample.sequence.PrintStreamWordPrinter">
		<arg>printStream</arg>
	</component>
	<component name="otherProcessor"
	    class="sample.sequence.fizzbuzz.PrintItemProcessor">
		<arg>wordPrinter</arg>
		<property name="next">null</property>
	</component>
	<component name="fizzProcessor"
	    class="sample.sequence.fizzbuzz.PrintWhenDivisibleProcessor">
		<arg>"Fizz"</arg>
		<arg>3</arg>
		<arg>wordPrinter</arg>
		<property name="next">otherProcessor</property>
	</component>
	<component name="buzzProcessor"
	    class="sample.sequence.fizzbuzz.PrintWhenDivisibleProcessor">
		<arg>"Buzz"</arg>
		<arg>5</arg>
		<arg>wordPrinter</arg>
		<property name="next">fizzProcessor</property>
	</component>
	<component name="fizzbuzzProcessor"
	    class="sample.sequence.fizzbuzz.PrintWhenDivisibleProcessor">
		<arg>"FizzBuzz"</arg>
		<arg>3*5</arg>
		<arg>wordPrinter</arg>
		<property name="next">buzzProcessor</property>
	</component>
	<component name="sequenceGenerator"
	    class="sample.sequence.NumberSequenceGenerator">
		<arg>1</arg>
		<arg>100</arg>
	</component>
	<component class="sample.sequence.SequenceEngine">
		<arg>sequenceGenerator</arg>
		<arg>fizzbuzzProcessor</arg>
	</component>
</components>

そのテスト

package sample.sequence.fizzbuzz;

import java.io.ByteArrayOutputStream;
import org.seasar.extension.unit.S2TestCase;
import sample.sequence.SequenceEngine;

public class FizzBuzzS2Test extends S2TestCase {

    private static final String FIZZBUZZ_ANS = "1?n2?nFizz?n4?nBuzz?nFizz?n" +
            "7?n8?nFizz?nBuzz?n11?nFizz?n13?n14?nFizzBuzz?n16?n17?nFizz?n19?n" +
            "Buzz?nFizz?n22?n23?nFizz?nBuzz?n26?nFizz?n28?n29?nFizzBuzz?n31?n" +
            "32?nFizz?n34?nBuzz?nFizz?n37?n38?nFizz?nBuzz?n41?nFizz?n43?n44?n" +
            "FizzBuzz?n46?n47?nFizz?n49?nBuzz?nFizz?n52?n53?nFizz?nBuzz?n56?n" +
            "Fizz?n58?n59?nFizzBuzz?n61?n62?nFizz?n64?nBuzz?nFizz?n67?n68?n" +
            "Fizz?nBuzz?n71?nFizz?n73?n74?nFizzBuzz?n76?n77?nFizz?n79?nBuzz?n" +
            "Fizz?n82?n83?nFizz?nBuzz?n86?nFizz?n88?n89?nFizzBuzz?n91?n92?n" +
            "Fizz?n94?nBuzz?nFizz?n97?n98?nFizz?nBuzz?n";
    private ByteArrayOutputStream out;
    private SequenceEngine<Integer> fizzbuzzEngine;
    
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        include("fizzbuzz.dicon");
    }
    public void testDicon() {
        assertNotNull(out);
        assertNotNull(fizzbuzzEngine);
        fizzbuzzEngine.run();
        String results = new String(out.toByteArray());
        String expected = FIZZBUZZ_ANS;
        
        assertEquals(expected, results);
  
    }
}

結論

こんなコードを書く奴が居たら、『アジャイルラクティス』6章 27「トレードオフを積極的に考慮する」、29「シンプルにすること」あたりを読まそう。