469
531

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.

オブジェクト指向エクササイズをやってみる

Last updated at Posted at 2016-03-19

なんか面白そうなのが流れてきたので、やってみた。

でき上がったコードは GitHub に上げています。

まずはソースを読む

ソースは こちら に上げられている。

クラス図にすると、以下のような感じ(分かりやすいように日本語にしてる)。

oop-exercise.jpg

いい感じで気持ち悪いですね!

「飲み物」クラスと「自動販売機」クラスがある。
どうやら、「自動販売機」にお金を投入して「飲み物」を購入するプログラムを想定しているらしい。

これらを使うクライアントは、次のような感じだろうか。

import vending.Drink;
import vending.VendingMachine;

public class Main {
    
    public static void main(String[] args) {
        VendingMachine vm = new VendingMachine();
        
        Drink drink = vm.buy(500, Drink.COKE);
        int charge = vm.refund();
        
        if (drink != null && drink.getKind() == Drink.COKE) {
            System.out.println("コーラを購入しました。");
            System.out.println("おつりは " + charge + " 円です。");
        } else {
            throw new RuntimeException("コーラ買えんかった(´゚д゚`)");
        }
    }
}
実行結果
コーラを購入しました。
おつりは 400 円です。

オブジェクト指向エクササイズのルールを適用していく

オブジェクト指向エクササイズのルールは、以下の9つ。

  1. 1つのメソッドにつきインデントは1段階までにすること
  2. else 句を使用しないこと
  3. すべてのプリミティブ型と文字列型をラップすること
  4. 1行につきドットは1つまでにすること
  5. 名前を省略しないこと
  6. すべてのエンティティを小さくすること
  7. 1つのクラスにつきインスタンス変数は2つまでにすること
  8. ファーストクラスコレクションを使用すること
  9. Getter, Setter, プロパティを使用しないこと

第21回 DDD(ドメイン駆動設計)勉強会in仙台 - connpass より拝借。

順番は、適用しやすそうなものから順番に適用していってみるので、上から順番とは限らない。

1つのメソッドにつきインデントは1段階までにすること

幸か不幸か? 今回のコードはインデントが2つ以上の場所はないので、ここはスキップ。

ただし、今後変更していくコードの中で2段階以上のインデントが発生しないように注意する必要はある。

名前を省略しないこと

投入金額が i という難解な変数名になっている。

辞書によると、支払金額は payment らしいので、 payment にする。

    public Drink buy(int payment, int kindOfDrink) {
        // 100円と500円だけ受け付ける
        if ((payment != 100) && (payment != 500)) {
            charge += payment;
            return null;
        }

        ...

すべてのプリミティブ型と文字列型をラップすること

    int quantityOfCoke = 5; // コーラの在庫数
    int quantityOfDietCoke = 5; // ダイエットコーラの在庫数
    int quantityOfTea = 5; // お茶の在庫数
    int numberOf100Yen = 10; // 100円玉の在庫
    int charge = 0; // お釣り

これとか

    public Drink buy(int payment, int kindOfDrink) {

こいつとかのことですね。

もうちょっと具体的に書き出すと、

  • 在庫
  • 硬貨
  • 飲み物種別

のこと。

これらをラップしたクラスを考える。

oop-exercise.jpg

実装は以下のような感じ。

Stock.java
package vending.after;

public class Stock {
    
    private int quantity;
    
    public Stock(int quantity) {
        this.quantity = quantity;
    }

    public int getQuantity() {
        return quantity;
    }

    public void decrement() {
        this.quantity--;
    }
}
Coin.java
package vending.after;

public enum Coin {
    ONE_HUNDRED(100),
    FIVE_HUNDRED(500);
    
    private final int amount;

    private Coin(int amount) {
        this.amount = amount;
    }

    public int getAmount() {
        return amount;
    }
}
DrinkType.java
package vending.after;

public enum DrinkType {
    COKE,
    DIET_COKE,
    TEA;
}

「自動販売機」クラスは、以下のようになった。

VendingMachine.java
public class VendingMachine {

    Stock stockOfCoke = new Stock(5); // コーラの在庫数
    Stock stockOfDietCoke = new Stock(5); // ダイエットコーラの在庫数
    Stock stockOfTea = new Stock(5); // お茶の在庫数
    Stack<Coin> numberOf100Yen = new Stack<>(); // 100円玉の在庫
    List<Coin> charge = new ArrayList<>(); // お釣り

    public Drink buy(Coin payment, DrinkType kindOfDrink) {
        // 100円と500円だけ受け付ける
        if ((payment != Coin.ONE_HUNDRED) && (payment != Coin.FIVE_HUNDRED)) {
            change.add(payment);
            return null;
        }

        if ((kindOfDrink == DrinkType.COKE) && (stockOfCoke.getQuantity() == 0)) {
            change.add(payment);
            return null;
        } else if ((kindOfDrink == DrinkType.DIET_COKE) && (stockOfDietCoke.getQuantity() == 0)) {
            change.add(payment);
            return null;
        } else if ((kindOfDrink == DrinkType.TEA) && (stockOfTea.getQuantity() == 0)) {
            change.add(payment);
            return null;
        }

        // 釣り銭不足
        if (payment == Coin.FIVE_HUNDRED && numberOf100Yen.size() < 4) {
            change.add(payment);
            return null;
        }

        if (payment == Coin.ONE_HUNDRED) {
            // 100円玉を釣り銭に使える
            numberOf100Yen.add(payment);
        } else if (payment == Coin.FIVE_HUNDRED) {
            // 400円のお釣り
            change.addAll(this.calculateChange());
        }

        if (kindOfDrink == DrinkType.COKE) {
            stockOfCoke.decrement();
        } else if (kindOfDrink == DrinkType.DIET_COKE) {
            stockOfDietCoke.decrement();
        } else {
            stockOfTea.decrement();
        }

        return new Drink(kindOfDrink);
    }
    
    private List<Coin> calculateChange() {
        return IntStream.range(0, 4)
                .mapToObj(i -> numberOf100Yen.pop())
                .collect(toList());
    }

    public List<Coin> refund() {
        List<Coin> result = new ArrayList<>(change);
        change.clear();
        return result;
    }
}

とりあえずプリミティブ型は無くなった。

ファーストクラスコレクションを使用すること

Stack<Coin> とか List<Coin> があるので、これらをラップしたクラスを作る。

oop-exercise.jpg

Change.java
package vending.after;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class Change implements Cloneable {
    private List<Coin> coins;
    
    public Change() {
        this.coins = new ArrayList<>();
    }

    public Change(List<Coin> coins) {
        this.coins = new ArrayList<>(coins);
    }

    public void add(Coin coin) {
        this.coins.add(coin);
    }

    public void add(Change change) {
        this.coins.addAll(change.coins);
    }

    public int getAmount() {
        return this.coins.stream().collect(Collectors.summingInt(Coin::getAmount));
    }

    public void clear() {
        this.coins.clear();
    }
    
    @Override
    public Change clone() {
        return new Change(this.coins);
    }
}
StockOf100Yen.java
package vending.after;

import java.util.Stack;

public class StockOf100Yen {
    private Stack<Coin> numberOf100Yen = new Stack<>();

    public void add(Coin coin) {
        this.numberOf100Yen.add(coin);
    }

    public int size() {
        return this.numberOf100Yen.size();
    }

    public Coin pop() {
        return this.numberOf100Yen.pop();
    }
}

「自動販売機」クラスは、以下のようになった。

VendingMachine.java
public class VendingMachine {

    Stock stockOfCoke = new Stock(5); // コーラの在庫数
    Stock stockOfDietCoke = new Stock(5); // ダイエットコーラの在庫数
    Stock stockOfTea = new Stock(5); // お茶の在庫数
    StockOf100Yen stockOf100Yen = new StockOf100Yen();
    Change change = new Change();

    public Drink buy(Coin payment, DrinkType kindOfDrink) {
        // 100円と500円だけ受け付ける
        if ((payment != Coin.ONE_HUNDRED) && (payment != Coin.FIVE_HUNDRED)) {
            change.add(payment);
            return null;
        }

        if ((kindOfDrink == DrinkType.COKE) && (stockOfCoke.getQuantity() == 0)) {
            change.add(payment);
            return null;
        } else if ((kindOfDrink == DrinkType.DIET_COKE) && (stockOfDietCoke.getQuantity() == 0)) {
            change.add(payment);
            return null;
        } else if ((kindOfDrink == DrinkType.TEA) && (stockOfTea.getQuantity() == 0)) {
            change.add(payment);
            return null;
        }

        // 釣り銭不足
        if (payment == Coin.FIVE_HUNDRED && stockOf100Yen.size() < 4) {
            change.add(payment);
            return null;
        }

        if (payment == Coin.ONE_HUNDRED) {
            // 100円玉を釣り銭に使える
            stockOf100Yen.add(payment);
        } else if (payment == Coin.FIVE_HUNDRED) {
            // 400円のお釣り
            change.add(this.calculateChange());
        }

        if (kindOfDrink == DrinkType.COKE) {
            stockOfCoke.decrement();
        } else if (kindOfDrink == DrinkType.DIET_COKE) {
            stockOfDietCoke.decrement();
        } else {
            stockOfTea.decrement();
        }

        return new Drink(kindOfDrink);
    }
    
    private Change calculateChange() {
        List<Coin> coins = IntStream.range(0, 4)
                            .mapToObj(i -> stockOf100Yen.pop())
                            .collect(toList());
        
        return new Change(coins);
    }

    public Change refund() {
        Change result = change.clone();
        change.clear();
        return result;
    }
}

List が一瞬現れてるけど、まぁ一瞬なので勘弁ということで。

Getter, Setter, プロパティを使用しないこと

  • オブジェクトが持つ値を無加工で取得しているところ。
  • 呼び出し側で、取得した値を使って何らかのロジック(if 分岐や値の加工)を実行しているところ。

この観点で修正していく。

「在庫」の数量を取得している

変更前

VendingMachine.java
        if ((kindOfDrink == DrinkType.COKE) && (stockOfCoke.getQuantity() == 0)) {
            change.add(payment);
            return null;
        } else if ((kindOfDrink == DrinkType.DIET_COKE) && (stockOfDietCoke.getQuantity() == 0)) {
            change.add(payment);
            return null;
        } else if ((kindOfDrink == DrinkType.TEA) && (stockOfTea.getQuantity() == 0)) {
            change.add(payment);
            return null;
        }

「在庫」クラスに、空かどうかを判定するメソッドを作る。

変更後

Stock.java
public class Stock {
    
    private int quantity;
    
    ...
    
    public boolean isEmpty() {
        return this.quantity == 0;
    }
}
VendingMachine.java
        if ((kindOfDrink == DrinkType.COKE) && stockOfCoke.isEmpty()) {
            change.add(payment);
            return null;
        } else if ((kindOfDrink == DrinkType.DIET_COKE) && stockOfDietCoke.isEmpty()) {
            change.add(payment);
            return null;
        } else if ((kindOfDrink == DrinkType.TEA) && stockOfTea.isEmpty()) {
            change.add(payment);
            return null;
        }

「100円硬貨の在庫」から数量を取得している

変更前

VendingMachine.java
        // 釣り銭不足
        if (payment == Coin.FIVE_HUNDRED && stockOf100Yen.size() < 4) {
            change.add(payment);
            return null;
        }

「100円硬貨の在庫」クラスに、「釣り銭切れ」かどうかを判定するメソッドを作る。

変更後

StockOf100Yen.java
public class StockOf100Yen {
    private Stack<Coin> numberOf100Yen = new Stack<>();

    ...

    public boolean doesNotHaveChange() {
        return this.numberOf100Yen.size() < 4;
    }}
VendingMachine.java
        if (payment == Coin.FIVE_HUNDRED && stockOf100Yen.doesNotHaveChange()) {
            change.add(payment);
            return null;
        }

「100円硬貨の在庫」から硬貨を取得して「お釣り」を作っている

変更前

VendingMachine.java
    private Change calculateChange() {
        List<Coin> coins = IntStream.range(0, 4)
                            .mapToObj(i -> stockOf100Yen.pop())
                            .collect(toList());
        
        return new Change(coins);
    }

「100円硬貨の在庫」クラスに「お釣り」を作るメソッドを作る。

変更後

StockOf100Yen.java
public class StockOf100Yen {
    private Stack<Coin> numberOf100Yen = new Stack<>();

    ...
    
    public Change takeOutChange() {
        List<Coin> coins = IntStream.range(0, 4)
                            .mapToObj(i -> this.numberOf100Yen.pop())
                            .collect(toList());
        
        return new Change(coins);
    }
}
VendingMachine.java
        } else if (payment == Coin.FIVE_HUNDRED) {
            // 400円のお釣り
            change.add(stockOf100Yen.takeOutChange());
        }

「硬貨」クラスに金額を取得するためのメソッドがある

変更前

Coin.java
    public int getAmount() {
        return amount;
    }

よく見たら、 int を返している点も良くない。
int をラップした「お金」クラスを作って、それに変換するメソッドにしてしまう。

変更後

Money.java
package vending.after;

public class Money {
    
    private final int amount;
    
    public Money(int amount) {
        this.amount = amount;
    }

    public Money add(Money money) {
        return new Money(this.amount + money.amount);
    }

    @Override
    public String toString() {
        return String.valueOf(this.amount);
    }
}
Coin.java
package vending.after;

public enum Coin {
    ONE_HUNDRED(100),
    FIVE_HUNDRED(500);
    
    private final int amount;

    private Coin(int amount) {
        this.amount = amount;
    }
    
    public Money toMoney() {
        return new Money(this.amount);
    }
}

「飲み物」クラスが「飲み物種別」を返している

変更前

Drink.java
    public DrinkType getDrinkType() {
        return drinkType;
    }

「飲み物種別」を判定するメソッドにしてみる。

変更後

Drink.java
    public boolean isCoke() {
        return this.kind == DrinkType.COKE;
    }
    
    public boolean isDietCoke() {
        return this.kind == DrinkType.DIET_COKE;
    }
    
    public boolean isTea() {
        return this.kind == DrinkType.TEA;
    }

1つのクラスにつきインスタンス変数は2つまでにすること

変更前

VendingMachine.java
public class VendingMachine {

    Stock stockOfCoke = new Stock(5); // コーラの在庫数
    Stock stockOfDietCoke = new Stock(5); // ダイエットコーラの在庫数
    Stock stockOfTea = new Stock(5); // お茶の在庫数
    StockOf100Yen stockOf100Yen = new StockOf100Yen();
    Change change = new Change();

「自動販売機」クラスに5つものインスタンス変数が存在してしまっている。
これを、役割ごとに分けないといけない。

よく見ると、上3つは「在庫」に関係している変数で、下2つは「お釣り」に関係している変数が集まっていることに気づく。
これらを、それぞれまとめ上げるクラスを考えたい。

イメージとしては、上3つは自動販売機内にある飲み物を収めている場所で、下2つはお金を投入する場所にあるヤツ(業者の人がお金を回収するときに取り外しているヤツ)を指すような名前にしたい。

しかし、残念ながら、これらの具体的な名前が分からない。
普通の業務なら、詳しいお客さんに名前を教えてもらうところだろうけど、今回お客さんは居ないので Google 先生に教えてもらう。

自動販売機 | ekouhou.net

自動販売機の特許についてのページが引っかかった。
ここによると、飲み物を収めているところを**「商品収納庫」、お金を投入するところを「コインメック」**と呼んでいるらしい。

「コインメック」という言葉は初耳だったが、自動販売機業界?では常識っぽい。

コインメック | 自動販売機設置.jp
(自動販売機設置.jp て)

あと、「100円硬貨の在庫」と呼んでいたクラスは、「コインメック」の構造で調べると「金庫」と呼ぶらしい。

コインメック、ビルバリの取付方法

「100円硬貨の在庫」と比べたら大分マシな名前な気がする。

ということで、これらの話を踏まえてクラス構造を修正する。

oop-exercise.jpg

変更後

実装は下のような感じになった。

CoinMech.java
package vending.after;

public class CoinMech {
    
    private CashBox cashBox = new CashBox();
    private Change change = new Change();
    
    public CoinMech() {
        for (int i=0; i<10; i++) {
            this.cashBox.add(Coin.ONE_HUNDRED);
        }
    }

    public void addChange(Coin payment) {
        this.change.add(payment);
    }

    public void addChange(Change change) {
        this.change.add(change);
    }

    public void addCoinIntoCashBox(Coin payment) {
        this.cashBox.add(payment);
    }

    public boolean doesNotHaveChange() {
        return this.cashBox.doesNotHaveChange();
    }

    public Change takeOutChange() {
        return this.cashBox.takeOutChange();
    }

    public Change refund() {
        Change result = change.clone();
        change.clear();
        return result;
    }
}
Storage.java
package vending.after;

import java.util.HashMap;
import java.util.Map;

public class Storage {
    private Map<DrinkType, Stock> stocks = new HashMap<>();
    
    public Storage() {
        this.stocks.put(DrinkType.COKE, new Stock(5));
        this.stocks.put(DrinkType.DIET_COKE, new Stock(5));
        this.stocks.put(DrinkType.TEA, new Stock(5));
    }

    public boolean isEmpty(DrinkType kindOfDrink) {
        return this.stocks.get(kindOfDrink).isEmpty();
    }

    public void decrement(DrinkType kindOfDrink) {
        this.stocks.get(kindOfDrink).decrement();
    }
}
VendingMachine.java
package vending.after;

public class VendingMachine {

    Storage storage = new Storage();
    CoinMech coinMech = new CoinMech();
    
    public Drink buy(Coin payment, DrinkType kindOfDrink) {
        // 100円と500円だけ受け付ける
        if ((payment != Coin.ONE_HUNDRED) && (payment != Coin.FIVE_HUNDRED)) {
            coinMech.addChange(payment);
            return null;
        }
        
        if (storage.isEmpty(kindOfDrink)) {
            coinMech.addChange(payment);
            return null;
        }

        if (payment == Coin.FIVE_HUNDRED && coinMech.doesNotHaveChange()) {
            coinMech.addChange(payment);
            return null;
        }

        if (payment == Coin.ONE_HUNDRED) {
            // 100円玉を釣り銭に使える
            coinMech.addCoinIntoCashBox(payment);
        } else if (payment == Coin.FIVE_HUNDRED) {
            // 400円のお釣り
            coinMech.addChange(coinMech.takeOutChange());
        }

        storage.decrement(kindOfDrink);

        return new Drink(kindOfDrink);
    }

    public Change refund() {
        return coinMech.refund();
    }
}

else 句を使用しないこと

変更前

VendingMachine.java
        if (payment == Coin.ONE_HUNDRED) {
            // 100円玉を釣り銭に使える
            coinMech.addCoinIntoCashBox(payment);
        } else if (payment == Coin.FIVE_HUNDRED) {
            // 400円のお釣り
            coinMech.addChange(coinMech.takeOutChange());
        }

さて、どうしてくれようか。

よく見てみると、お金を扱うためのクラスとして「コインメック」を抽出したのに、「自動販売機」クラスがお金の計算をしているように見えることがそもそもおかしい感じがする。

現実の自販機の動きをイメージすると、「コインメック」に投入された「硬貨」が入って、「お釣り」がいくらになるかとかは全て「コインメック」が計算してくれそうな気がする。
今の実装は、「自動販売機」が「硬貨」の種類に応じて必要な「お釣り」を設定してしまっている。

たぶん、正しい流れは、

  1. 「自動販売機」は、投入された「硬貨」を「コインメック」に渡す。
  2. 「自動販売機」は、「コインメック」から「お釣り」を取得する。

な気がする。

そう考えると、問題箇所より上にある実装も怪しくなってくる。

VendingMachine.java
        // 100円と500円だけ受け付ける
        if ((payment != Coin.ONE_HUNDRED) && (payment != Coin.FIVE_HUNDRED)) {
            coinMech.addChange(payment);
            return null;
        }
        
        if (storage.isEmpty(kindOfDrink)) {
            coinMech.addChange(payment);
            return null;
        }

        if (payment == Coin.FIVE_HUNDRED && coinMech.doesNotHaveChange()) {
            coinMech.addChange(payment);
            return null;
        }

この辺も、「お釣り」の有無を「自動販売機」が判断しているように見える。

クラスごとの役割を考えて、この辺のロジックを見直す。

oop-exercise.jpg

「コインメック」はお金の管理を、「商品収納庫」は在庫操作だけを行う。
「自動販売機」は、これら2つのクラスを使って飲み物の購入という機能を実現する。

この結果、「コインメック」は投入硬貨をインスタンス変数として持たなくてはならなくなった(在庫切れなどで購入できなかった場合に、お釣りとして返却できないといけないので)。
「コインメック」は既に「金庫」と「お釣り」という2つのインスタンス変数を持っているため、このままだとエクササイズのルールに違反する。

そこで、「お釣り」と「硬貨」をまとめた「支払い」というクラスを抽出してみた。

oop-exercise.jpg

変更後

CoinMech.java
package vending.after;

public class CoinMech {
    
    private CashBox cachBox = new CashBox();
    private Payment payment;
    
    public CoinMech() {
        for (int i=0; i<10; i++) {
            this.cachBox.add(Coin.ONE_HUNDRED);
        }
    }

    public void put(Coin coin) {
        this.payment = new Payment(coin);
    }

    public boolean doesNotHaveChange() {
        return this.payment.needChange()
                && this.cachBox.doesNotHaveChange();
    }

    public Change refund() {
        return this.payment.refund();
    }
    
    public void commit() {
        this.payment.commit(this.cachBox);
    }
}
Payment.java
package vending.after.money;

public class Payment {

    private Change change;
    private Coin coin;
    
    public Payment(Coin coin) {
        this.coin = coin;
    }

    public boolean needChange() {
        return this.coin == Coin.FIVE_HUNDRED;
    }

    public void commit(CashBox cachBox) {
        if (this.coin == Coin.ONE_HUNDRED) {
            cachBox.add(this.coin);
            this.change = new Change();
        }
        
        if (this.coin == Coin.FIVE_HUNDRED) {
            this.change = cachBox.takeOutChange();
        }

        this.coin = null;
    }

    public Change refund() {
        return this.isNotCommited()
                ? new Change(this.coin)
                : this.change;
    }

    private boolean isNotCommited() {
        return this.coin != null;
    }
}
VendingMachine.java
package vending.after;

public class VendingMachine {

    private Storage storage = new Storage();
    private CoinMech coinMech = new CoinMech();
    
    public Drink buy(Coin payment, DrinkType drinkType) {
        
        this.coinMech.put(payment);
        
        if (this.coinMech.doesNotHaveChange()) {
            return null;
        }
        
        if (this.storage.doesNotHaveStock(drinkType)) {
            return null;
        }
        
        this.coinMech.commit();
        
        return this.storage.takeOut(drinkType);
    }

    public Change refund() {
        return this.coinMech.refund();
    }
}

else が無くなった。
「自動販売機」クラスも、かなりスッキリした。

1行につきドットは1つまでにすること

「取得したオブジェクトに委譲すべきなのに、不用意にメソッドの呼び出しを繋げてしまっていないか?」という観点で見たとき、該当しそうな場所はなさそうだった。

無理矢理見つけるとしたら、以下が該当するかもしれない。

Storage.java
    public boolean isEmpty(DrinkType kindOfDrink) {
        return this.stocks.get(kindOfDrink).isEmpty();
    }

相手は HashMap で変更のしようがないので、メソッドを切る。

Storage.java
    public boolean doesNotHaveStock(DrinkType kindOfDrink) {
        return this.findStock(kindOfDrink).isEmpty();
    }

    private Stock findStock(DrinkType drinkType) {
        return this.stocks.get(drinkType);
    }

(this. は許して)

すべてのエンティティを小さくすること

1ファイル50行までという制約は、コメントを除けばどのクラスも現時点で達成できている。

しかし、1パッケージ10ファイルは達成できていない。
現状、11のファイルが存在してしまっている。

各クラスを眺めて、関連が強いものどうしを同じパッケージにまとめてみる。

oop-exercise.jpg

これで、1パッケージ10クラス以下になった。

結果

こうなった。

当然のことながら、これが唯一の正解というわけではない。
プロジェクトやチームごとに妥当な答えを相談し合う必要はあると思う(特に Getter をどうするか、とか)。

所感(振り返り)

良くなった・良かったと思ったこと

  • 「自動販売機」クラスがスッキリした。
    • 最初に比べれば、「自動販売機」クラスの buy() メソッドが大分スッキリした。
    • 何をしているかが、ぱっと見ですぐに理解できるようになった。
    • 「そうなるように修正した」という点はあるかもしれないが、そもそも「インスタンス変数2個まで」ルールを適用するためにクラスを分割しなければ、最終形の状態に持って行くことはできなかった。
  • 単体テストが書きやすくなった。
    • 実は一番最初、「自動販売機」クラスのテストを書こうとした。
    • VendingMachineTest.java
    • しかし、 buy() メソッドの複雑な if 分岐を全て網羅するようなテストは、とてもじゃないが綺麗に書ききれる気がしなくて途中で投げてしまった。
    • 修正後は、1つ1つのクラスが非常に小さいので、簡単に単体テストを書くことができるようになっていた。
    • とりあえず、「お金」パッケージに絞ってテストを書いてみた結果が こちら
    • 個人的に、「オブジェクト指向らしさ」と「単体テストしやすさ」は、ほぼ一致していると思っている。
  • 概念を深く考えるキッカケになる。
    • 特にそれを強く感じたのは「1クラス2インスタンス変数まで」のルールで「支払い」クラスを抽出したとき。
    • たぶん、今までなら「コインメック」に全部任せてしまっていた。
    • しかし、上記ルールのおかげで「投入金額」と「お釣り」をまとめて明確に役割を分離したクラス(支払い)を作るキッカケとなった。
    • それぞれのインスタンスのライフサイクルを考えると、それぞれがクラスとして分離されるのは正しいと思う。
      • 「コインメック」は「自動販売機」がある限り1つのインスタンスが使われ続けるが、「支払い」は1回の購入ごとにインスタンスが生成されるはずで、ライフサイクルが異なる。
      • ライフサイクルの異なる処理を1つのクラスに混ぜてしまうと、たぶん単体テストがしづらくなっていくと思う。

難しいなと思った点・もっと改善しないとイケないなと感じた点・その他

  • 「飲み物」クラスが結局空っぽ。
    • とりあえず Getter 撲滅のために isXxx() メソッドを作ったけど、正直イケてないと思ってる。
    • 種類が増えたら、毎回メソッドを追加するのかと考えると、なんかコレジャナイ感がする。
    • 今回は「飲み物」クラスを直接使用する場所がコード上に存在しなかったので、こんな感じになったのだと思う。
    • もし「飲み物」クラスが、種別ごとに異なる挙動をすべきなら、種別ごとにクラスを切ってポリモーフィズムを利用することになるのかもしれない。
    • でも、その場合飲み物種別の指定はどうするのがスマートなんだろう?(Class オブジェクトで指定?)
      • 「飲み物種別」で指定して、「飲み物種別」から「飲み物」インスタンスを生成する?
      • Drink drink = drinkType.newDrink() とか。
  • Getter の扱い。
    • 特に難しいと感じたのが、この Getter の扱い。
    • なんだかんだ言っても、値を画面に出力するときとかはどうしても中身の値が必要になる。
    • そんなとき、 Getter を使わずにどうやって中身を取り出せば良いのかは、結構悩む。
    • 今回、金額の値を取り出す方法で悩んだが、結局 Money に包んで toString() で取り出せるようにして回避した。
    • しかし、実際はそう単純に済むものでもないと思う。
    • 現実的な落とし所は、妥協で Getter はつけるが、画面などへの値の受け渡しなどに用途を限定する、と言ったところだろうか。
    • しかし、やはり Getter を作るとそこからカプセル化が崩れていく危険性が増すので、代替策があるのであれば極力作らないようにしたい。
  • 単純にルールを適用しただけでは、綺麗にはならない。
    • あくまでエクササイズのルールは細かい視点でのルールであって、全体を俯瞰したときに「どういうクラスがあるべきか?」みたいなことまでは導いてくれない。
    • 各クラスを俯瞰できるようにクラス図などで全体像を見渡しながら、違和感無くクラス同士が協調し合える関係を考える必要がある。

参考

469
531
2

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
469
531

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?