12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FizzBuzzAdvent Calendar 2017

Day 11

オブジェクト指向でFizzBuzz(Java)

Last updated at Posted at 2017-12-10

はじめに

この記事は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. オブジェクトの洗い出し
  2. モデルの検討
  3. クラス名前の選定

1. オブジェクトの洗い出し

まずは問題分で出現するオブジェクトの洗い出しを行います。

  1. 「1~100の数字を数えて条件に応じて結果を出力する」オブジェクト
  2. 「3の倍数をチェックし、Fizzを返す」オブジェクト
  3. 「5の倍数をチェックし、Buzzを返す」オブジェクト
  4. 「3の倍数かつ5の倍数をチェックし、FizzBuzzを返す」オブジェクト

役割でまとめただけなので、特に問題ありませんね。

2.モデルの検討

上記で出したオブジェクトから必要なモデルを検討します。

まずは「1~100の数字を数えて条件に応じて結果を出力する」 ですが、
これは「数値の範囲と条件をもらい、条件に応じて文字列を出力する」という入出力の機能をもっています。

次に2.~4.ですが、具体的な数値や文字列が違うだけで同じ機能をもっていると言えます。
機能としては 「指定された数値で割り切れるかチェックする(複数の場合はandでチェックする)」機能と「条件と対応する文字列を保持する」機能の2つに分解できそうです。

まとめると、モデルとしては以下の3つがあればよさそうです。

  • 数値の範囲と条件をもらい、条件に応じて文字列を出力する
  • 指定された数値で割り切れるかチェックする(複数の場合はandでチェックする)
  • 条件と対応する文字列を保持する

3.クラス名前の選定

上で出した3つのモデルに名前をつけていきます。
名前はそのままクラス名として使用します。

名前 機能
Operator 数値の範囲と条件をもらい、条件に応じて文字列を出力する
Specification 指定された数値で割り切れるかチェックする
Operation 条件と対応する文字列を保持する

Specificationのようなモデルはドメイン駆動開発(DDD)では仕様パターンというパターンで紹介されています。
これで必要なクラスが出そろったので実装していきます。

実装

設計したモデルを基にJavaのコードに落としていきます。(使用される側から載せていくので上のモデル順とは異なります。)

Streamを使用しているのでJava8以上の環境が必要です。

Specification.java
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の仕様パターンとして実装
  • 条件を表現する部分はJavaPredicateを使用
  • 条件はコンストラクタで最低1つ受け取り、複数の場合はandでチェーンするように実装
  • ファーストクラスコレクションとして実装(イミュータブル)
Operation.java
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;
    }

}

※ただのデータ保持用のオブジェクトでドメイン貧血症になってしまっています:(

Operator.java
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を動かすコードも書いてみます。

FizzBuzz.java
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));
    }

}
DivisiblePredicateFactory.java
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");
12
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?