はじめに
この記事はFizzBuzz Advent Calender 2017の11日目の記事です。
みんな大好きFizzBuzz。
弊社でも入社前の研修の一問目に使用されています。
どんな言語だろうと基本ワンライナーでかける(はず)なので特に問題ありませんが、ふとオブジェクト指向(OOP)で書いてみたらどうなるのかと気になったのでJava
で書いてみました。
FizzBuzzのおさらい
改めて簡単にFizzBuzz
をおさらいしてみます。
問題文
1 から 100 までの数字を数えていき、
- 3 の倍数なら Fizz
- 5 の倍数なら Buzz
- 3 の倍数かつ 5 の倍数なら FizzBuzz
- それ以外は、その数字
を出力する。
普通の回答
いたって普通のJava
での回答です。
public class FizzBuzz {
public static void main(String[] args) {
for (int i = 1; i <= 100; i++) {
if (i % 3 == 0 && i % 5 == 0) {
System.out.println("FizzBuzz");
} else if (i % 3 == 0) {
System.out.println("Fizz");
} else if (i % 5 == 0) {
System.out.println("Buzz");
} else {
System.out.println(i);
}
}
}
}
これをオブジェクト指向なコードにするためにまずはクラス設計をしていきます。
クラス設計
クラス設計を以下の手順で行っていきます。
- オブジェクトの洗い出し
- モデルの検討
- クラス名前の選定
1. オブジェクトの洗い出し
まずは問題分で出現するオブジェクトの洗い出しを行います。
- 「1~100の数字を数えて条件に応じて結果を出力する」オブジェクト
- 「3の倍数をチェックし、Fizzを返す」オブジェクト
- 「5の倍数をチェックし、Buzzを返す」オブジェクト
- 「3の倍数かつ5の倍数をチェックし、FizzBuzzを返す」オブジェクト
役割でまとめただけなので、特に問題ありませんね。
2.モデルの検討
上記で出したオブジェクトから必要なモデルを検討します。
まずは「1~100の数字を数えて条件に応じて結果を出力する」 ですが、
これは「数値の範囲と条件をもらい、条件に応じて文字列を出力する」という入出力の機能をもっています。
次に2.~4.ですが、具体的な数値や文字列が違うだけで同じ機能をもっていると言えます。
機能としては 「指定された数値で割り切れるかチェックする(複数の場合はandでチェックする)」機能と「条件と対応する文字列を保持する」機能の2つに分解できそうです。
まとめると、モデルとしては以下の3つがあればよさそうです。
- 数値の範囲と条件をもらい、条件に応じて文字列を出力する
- 指定された数値で割り切れるかチェックする(複数の場合はandでチェックする)
- 条件と対応する文字列を保持する
3.クラス名前の選定
上で出した3つのモデルに名前をつけていきます。
名前はそのままクラス名として使用します。
名前 | 機能 |
---|---|
Operator | 数値の範囲と条件をもらい、条件に応じて文字列を出力する |
Specification | 指定された数値で割り切れるかチェックする |
Operation | 条件と対応する文字列を保持する |
Specification
のようなモデルはドメイン駆動開発(DDD)では仕様パターンというパターンで紹介されています。
これで必要なクラスが出そろったので実装していきます。
実装
設計したモデルを基にJava
のコードに落としていきます。(使用される側から載せていくので上のモデル順とは異なります。)
※ Stream
を使用しているのでJava8
以上の環境が必要です。
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class Specification {
private List<Predicate<Integer>> predicateList = new ArrayList<>();
public Specification(Predicate<Integer> predicate) {
this.predicateList.add(predicate);
}
private Specification(List<Predicate<Integer>> predicateList) {
this.predicateList = predicateList;
}
public Specification and(Predicate<Integer> predicate) {
List<Predicate<Integer>> results = new ArrayList<>(this.predicateList);
results.add(predicate);
return new Specification(results);
}
public boolean isSatisfiedBy(Integer number) {
return this.predicateList.stream().allMatch(p -> p.test(number));
}
}
Specification.java
の実装上の要点としては以下の4つです。
- DDDの仕様パターンとして実装
- 条件を表現する部分は
Java
のPredicate
を使用 - 条件はコンストラクタで最低1つ受け取り、複数の場合は
and
でチェーンするように実装 - ファーストクラスコレクションとして実装(イミュータブル)
public class Operation {
private Specification specification;
private String message;
Operation(Specification specification, String message) {
this.specification = specification;
this.message = message;
}
public Specification getSpecification() {
return this.specification;
}
public String getMessage() {
return message;
}
}
※ただのデータ保持用のオブジェクトでドメイン貧血症になってしまっています:(
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
public class Operator {
private List<Operation> operationList = new ArrayList<>();
public void addOperation(Operation operation) {
this.operationList.add(operation);
}
public void run(IntStream range) {
range.forEach(number -> {
String message = this.operationList.stream()
.filter(operation -> operation.getSpecification().isSatisfiedBy(number))
.map(Operation::getMessage)
.findFirst()
.orElse(String.valueOf(number));
System.out.println(message);
});
}
}
条件をチェックする箇所はStream
を使ってワンライナーで書いています。
.findFirst().orElse()
の箇所はもう少しきれいな書き方がないか詰める必要がありますね。
上のクラスを使ってFizzBuzz
を動かすコードも書いてみます。
import java.util.function.Predicate;
import java.util.stream.IntStream;
public class FizzBuzz {
public static void main(String[] args) {
Operator operator = new Operator();
Predicate<Integer> divisibleBy3 = DivisiblePredicateFactory.divisibleBy(3);
Predicate<Integer> divisibleBy5 = DivisiblePredicateFactory.divisibleBy(5);
Operation fizzbuzz = new Operation(new Specification(divisibleBy3).and(divisibleBy5), "FizzBuzz");
Operation fizz = new Operation(new Specification(divisibleBy3), "Fizz");
Operation buzz = new Operation(new Specification(divisibleBy5), "Buzz");
operator.addOperation(fizzbuzz);
operator.addOperation(fizz);
operator.addOperation(buzz);
operator.run(IntStream.rangeClosed(1, 100));
}
}
import java.util.function.Predicate;
public class DivisiblePredicateFactory {
public static Predicate<Integer> divisibleBy(Integer divisor) {
return n -> n % divisor == 0;
}
}
FizzBuzz.java
を見ると各クラスの役割がはっきりとし、問題文とリンクした書き方が出来ているのが分かると思います。
もし「7の倍数なら Bar」のような追加仕様があったとしてもどこを編集すれば良いか簡単に想像がつきます。
これがオブジェクト指向プログラミングの良さですよね。
まとめ
今回はFizzBuzz
をオブジェクト指向で書いてみました。
このような簡単な問題でも真剣にオブジェクト指向を考えると結構面白かったです。
今度はテストも書いてみようと思います。
ここで載せているコードはGithubにまとめてあるのでよかったら手元で実行してみて、また改良してみてください。
(そしてプルリクください。)
https://github.com/totto357/OOP-FizzBuzz
明日の12日目は @aimofさんの 「実行文を除き、ワンライン関数定義のみで構成されたFizzBuzz 」です!
おまけ
却下したOperationクラスのBuilderパターンでの実装です。
「~~の時、xxと表示する」というのがコード上で表現できて良さそうだったんですが、Java
っぽくなかったので却下しました。
けど、せっかくなので残しておきます。
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class OperationWithBuilder {
private Specification specification;
private String message;
private Operation(Builder builder) {
this.specification = builder.getSpecification();
this.message = builder.getMessage();
}
public static Builder when(Predicate<Integer> predicate) {
return new Builder(predicate);
}
@Getter
static class Builder {
private Specification specification;
private String message;
public Builder(Predicate<Integer> predicate) {
this.specification = new Specification(predicate);
}
public Builder and(Predicate<Integer> predicate) {
this.specification = this.specification.and(predicate);
return this;
}
public Operation print(String message) {
this.message = message;
return new Operation(this);
}
}
}
使うとき
Predicate<Integer> divisibleBy3 = DivisiblePredicateFactory.divisibleBy(3);
Predicate<Integer> divisibleBy5 = DivisiblePredicateFactory.divisibleBy(5);
Operation fizz = Operation.when(divisibleBy3).print("Fizz");
Operation fizzbuzz = Operation.when(divisibleBy3).and(divisibleBy5).print("FizzBuzz");