Closed1

テスト駆動開発 まとめ

raamenwakamaturaamenwakamatu

はじめに

※この内容は「テスト駆動開発」を読んで、自分の理解でまとめたものです。
※内容は書籍の趣旨と異なる解釈をしている可能性があります。あくまで私の理解・整理用メモです。
※誤りなどがあればご指摘いただけると幸いです。


第1部 多国通貨

テスト駆動開発(TDD)の手順は下記の通り

  1. テストを一つ書く
  2. 全てのテストを実行し、新しいテストが失敗することを確認する
  3. 小さな変更を加える
  4. 全てのテストを実行し、全てが成功することを確認する
  5. リファクタリングして重複を削除する

仮実装

最初はテストを通すためだけに、適当にハードコーディングする。とりあえずテストが通ればいい段階。

assertEquals(5, adder.add(2, 3));

public class Adder {
    public int add(int a, int b) {
        return 5;  // 実際はa + bじゃなくて、とりあえず5返すだけ
    }
}

確認ポイント

  • テストを実行し、成功することは確認できるが、テストコードとプロダクションコードに重複が出てくる。
  • そもそも「5」はどこから来たかと言うと、頭の中で 2 + 3 を計算してたことに気づく。

三角計量

次に、異なる計算をテストするため、追加のテストケースを作成。

assertEquals(5, adder.add(2, 3));
assertEquals(7, adder.add(3, 4));

重複排除

重複のない一般的な処理に修正。

public int add(int a, int b) {
    return a + b;
}

このようにテスト駆動開発を進めていくと、機能が完成し、リファクタリングでクリーンなコードに仕上げられる。


第3部 テスト駆動開発のパターン

TDDのベストヒットパターン集。
メソッドを抽出するとき、別クラスに分離するときにどういった手順でリファクタリングするかや、デザインパターンなどが紹介されている

デザインパターン

・Commandパターン

操作をオブジェクトとしてカプセル化するパターン

interface Command {
    void execute();
}

class JumpCommand implements Command {
    public void execute() {
        System.out.println("ジャンプ!");
    }
}

class ShootCommand implements Command {
    public void execute() {
        System.out.println("弾を撃つ!");
    }
}

class GameController {
    private Command buttonX;

    void setButtonX(Command command) {
        this.buttonX = command;
    }

    void pressX() {
        buttonX.execute();
    }
}

GameController controller = new GameController();
controller.setButtonX(new JumpCommand());
controller.pressX(); // => ジャンプ!

controller.setButtonX(new ShootCommand());
controller.pressX(); // => 弾を撃つ!

メリット: 処理を遅延実行、記録、再実行できる。処理をオブジェクトとして抽象化できる。

・値オブジェクト

オブジェクト生成時に状態を設定したら、その後決して変えないようにする。(メソッドでは新しいオブジェクトを返す。)

public class Counter {
    private final int value;

    public Counter(int value) {
        this.value = value;
    }

    public Counter increment() {
        return new Counter(this.value + 1); // 変更せず、新しいインスタンスを返す
    }

    public int value() {
        return value;
    }
}

Counter counter1 = new Counter(0);
Counter counter2 = counter1;

Counter counter3 = counter1.increment(); // 新しいインスタンスを返すだけ
System.out.println(counter2.value()); // 0(変わらない!)
System.out.println(counter3.value()); // 1(増えてる!)

メリット:

  • 別名参照問題を防げる。
  • コードがシンプルで直感的になる。
  • 呼び出し側で破壊的メソッドのように実行されることがない。

・Null Objectパターン

null の代わりに「何もしないオブジェクト」を使うことで、null チェックのゴチャゴチャをなくすパターン

interface Animal {
    void makeSound();
}

class Dog implements Animal {
    public void makeSound() {
        System.out.println("ワン!");
    }
}

// Null Object
class NullAnimal implements Animal {
    public void makeSound() {
        // 何もしない
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = getAnimal(); // null の代わりに NullAnimal を返す
        animal.makeSound(); // null チェック不要
    }

    static Animal getAnimal() {
        return new NullAnimal(); // ← ここ重要
    }
}

メリット: null チェックが不要になる。

・Templateメソッド

処理の流れ(骨組み)を親クラスで決めて、細かい部分は子クラスに任せるパターン

abstract class Game {
    // テンプレートメソッド:処理の流れを定義
    public final void play() {
        initialize();
        startPlay();
        endPlay();
    }

    abstract void initialize();
    abstract void startPlay();
    abstract void endPlay();
}

class Football extends Game {
    void initialize() { System.out.println("Football Game Initialized!"); }
    void startPlay() { System.out.println("Football Game Started!"); }
    void endPlay() { System.out.println("Football Game Finished!"); }
}

class Baseball extends Game {
    void initialize() { System.out.println("Baseball Game Initialized!"); }
    void startPlay() { System.out.println("Baseball Game Started!"); }
    void endPlay() { System.out.println("Baseball Game Finished!"); }
}

Game game = new Football();
game.play(); // ← 処理の流れは親に任せて、中身は子が決めてる

メリット:

  • 処理の流れが親で統一 → 安定性・再利用性アップ
  • 子クラスに実装を強制できる(abstractprotected

・Pluggable Object パターン(Strategy)

オブジェクトの振る舞い(ロジック)を差し替え可能にするパターン

interface SortStrategy {
    void sort(List<Integer> list);
}

class BubbleSort implements SortStrategy {
    public void sort(List<Integer> list) {
        System.out.println("バブルソート実行");
    }
}

class QuickSort implements SortStrategy {
    public void sort(List<Integer> list) {
        System.out.println("クイックソート実行");
    }
}

class Sorter {
    private SortStrategy strategy;

    public Sorter(SortStrategy strategy) {
        this.strategy = strategy;
    }

    public void sort(List<Integer> list) {
        strategy.sort(list);
    }
}

Sorter sorter = new Sorter(new BubbleSort());
sorter.sort(myList); // バブルソート実行

sorter = new Sorter(new QuickSort());
sorter.sort(myList); // クイックソート実行

メリット:

  • if / switch を減らして、ロジックを分離できる。

・Factoryメソッド

オブジェクトを作成するメソッド
コンストラクターのように見えないのにオブジェクトを作成するメソッドなので柔軟性が不要な場合はコンストラクターを利用した方が良い。

・Imposterパターン

オブジェクトが期待されるインターフェースを実装しているように振る舞うが、実際にはその実装が正しい機能を提供していないパターン
テストに利用することが多い

public class RealObject {
    public String doSomething() {
        return "RealObject doing something!";
    }
}

public class ImposterObject {
    public String doSomething() {
        return "ImposterObject pretending to do something!";
    }
}

public class Client {
    public static void main(String[] args) {
        ImposterObject imposter = new ImposterObject();
        System.out.println(imposter.doSomething()); // "ImposterObject pretending to do something!"
    }
}

・Compositeパターン

部分と全体を同じように扱うパターン

  • Component(コンポーネント): 共通のインターフェースを定義。リーフオブジェクトとコンポジットオブジェクトは、このインターフェースを実装する
  • Leaf(リーフ): ツリー構造の末端に位置するオブジェクトで、実際のデータを保持する。
  • Composite(コンポジット): 子オブジェクト(リーフまたは他のコンポジット)を持つことができるオブジェクト。リーフオブジェクトと同様に、コンポジットオブジェクトも共通のインターフェースを実装する。
interface Holding {
    Money getBalance();
}

// 取引(リーフ)
class Transaction implements Holding {
    private final Money money;

    public Transaction(Money money) {
        this.money = money;
    }

    @Override
    public Money getBalance() {
        return money;
    }
}

// アカウント(Composite)
class Account implements Holding {
    private List<Holding> holdings = new ArrayList<>();

    public void add(Holding h) {
        holdings.add(h);
    }

    @Override
    public Money getBalance() {
        Money total = new Money(0);  // 初期値
        for (Holding h : holdings) {
            total = total.add(h.getBalance());
        }
        return total;
    }
}

Account mainAccount = new Account();
mainAccount.add(new Transaction(new Money(100)));
mainAccount.add(new Transaction(new Money(200)));

Account savings = new Account();
savings.add(new Transaction(new Money(500)));

mainAccount.add(savings);

System.out.println(mainAccount.getBalance());  // 800になる

メリット:

  • 階層的にネストできる
  • 一貫した操作
  • 拡張がしやすい

・Collecting Parameter パターン

再帰的なメソッド呼び出しの中で、結果や途中経過を外部に保持するために使われる引数(=collecting parameter)を追加するパターン。

・Singletonパターン

あるクラスで作られるインスタンスが、1つだけであることを保証するパターン。インスタンスの数が1つに限定されるケースに利用する。

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

リファクタリング

・差異をなくす

2つの条件分岐が似ている、二つのループ構造が似ている、二つのメソッドが似ている、二つのクラスが似ている場合、それらを完全に一致させる。

例えば、複数のサブクラスを消したい時、サブクラスを空にするのが目標。サブクラスのメソッドを親クラスのメソッドに一致させて、サブクラスから一つずつメソッドを減らしていく。最終的にサブクラスが空になったら、サブクラスの参照を親クラスの参照に置き換える。


・データ構造の変更

変更前のコード

public class User {
    private String phoneNumber;

    public User(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }
}

① 新構造のためのインスタンス変数を定義する

public class User {
    private String phoneNumber;
    private List<String> phoneNumbers; // 追加

    public User(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }
}

② 旧構造でデータが設定されている部分をその変数に置き換える

import java.util.ArrayList;
import java.util.List;

public class User {
    private String phoneNumber;
    private List<String> phoneNumbers;

    public User(String phoneNumber) {
        this.phoneNumber = phoneNumber;
        this.phoneNumbers = new ArrayList<>();
        this.phoneNumbers.add(phoneNumber); // 新しいほうにも追加
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }
}

③ 旧構造のデータを使っている部分をその変数に置き換える

public String getPhoneNumber() {
    return phoneNumbers.get(0); // 新構造から取る
}

④ 旧構造のコードを消す

public class User {
    private List<String> phoneNumbers;

    public User(String phoneNumber) {
        this.phoneNumbers = new ArrayList<>();
        this.phoneNumbers.add(phoneNumber);
    }

    public String getPhoneNumber() {
        return phoneNumbers.get(0);
    }
}

⑤ 外部インターフェースに新構造を反映する


・メソッドの抽出

変更前のコード

public class ScoreCalculator {
    public int calculateScore(List<Integer> points) {
        int total = 0;
        for (int point : points) {
            if (point > 50) {
                total += point * 2;
            } else {
                total += point;
            }
        }
        return total;
    }
}

① メソッド内から新しいメソッドとして切り出す意味のある部分を探す

if (point > 50) {
    total += point * 2;
} else {
    total += point;
}

この部分は「合計する責任」とは別の意味を持っているので、切り出す意味がある。

② 一時変数の代入が行われていないことを確認

③ 抽出したメソッドを作成

private int calculatePointScore(int point) {
    if (point > 50) {
        return point * 2;
    } else {
        return point;
    }
}

④ 旧メソッドを更新して、新メソッドを呼び出す

public class ScoreCalculator {
    public int calculateScore(List<Integer> points) {
        int total = 0;
        for (int point : points) {
            total += calculatePointScore(point);
        }
        return total;
    }

    private int calculatePointScore(int point) {
        if (point > 50) {
            return point * 2;
        } else {
            return point;
        }
    }
}

・インターフェースの抽出

① インターフェースを宣言する

public interface Animal {
}

② インターフェースを既存クラスが実装する

public class Dog implements Animal {
    public void bark() {
        System.out.println("ワンワン!");
    }
}

③ 必要なメソッドをインターフェースに加え、必要に応じて、メソッドの可視性をあげる

public interface Animal {
    void bark();
}

④ コードの中で宣言されている型をクラスからインターフェースに書き換える

Animal dog = new Dog();
dog.bark();

これで、「Dog」じゃなくて「Animal」という抽象的な存在に依存することになり、後から別のAnimal(CatやBirdなど)が来てもコードが影響を受けにくくなる。


・メソッドの移動

変更前のコード

// Orderクラス
public class Order {
    private Customer customer;

    public double calculateDiscount() {
        return customer.getLoyaltyLevel() * 0.05;
    }
}

① メソッドをコピーして、移動先クラスにペースト

// Customerクラス
public class Customer {
    private int loyaltyLevel;

    public int getLoyaltyLevel() {
        return loyaltyLevel;
    }

    // Orderから持ってきたメソッド
    public double calculateDiscount() {
        return loyaltyLevel * 0.05;
    }
}

② メソッドが移動先クラスに適切に配置されていることを確認

③ 旧クラスでメソッド呼び出しを新しいメソッドに置き換える

// Orderクラス
public class Order {
    private Customer customer;

    public double calculateDiscount() {
        return customer.calculateDiscount(); // Customerに丸投げ
    }
}

・メソッドオブジェクト

変更前のコード

public class Order {
    private List<Item> items;

    public double calculateTotalPrice(double taxRate, double discountRate) {
        double total = 0;
        for (Item item : items) {
            total += item.getPrice();
        }
        total = total * (1 + taxRate);
        total = total * (1 - discountRate);
        return total;
    }
}

① 新しいメソッドオブジェクトを作成

public class OrderTotalCalculator {
    private Order order;
    private double taxRate;
    private double discountRate;

    public OrderTotalCalculator(Order order, double taxRate, double discountRate) {
        this.order = order;
        this.taxRate = taxRate;
        this.discountRate = discountRate;
    }
}

② ローカル変数をインスタンス変数として表現

public class OrderTotalCalculator {
    private Order order;
    private double taxRate;
    private double discountRate;
    private double total = 0;  // 追加
}

③ 元のメソッドを移動してrunメソッドを作成

public class OrderTotalCalculator {
    private Order order;
    private double taxRate;
    private double discountRate;
    private double total = 0;

    public OrderTotalCalculator(Order order, double taxRate, double discountRate) {
        this.order = order;
        this.taxRate = taxRate;
        this.discountRate = discountRate;
    }

    public double run() {
        for (Item item : order.getItems()) {
            total += item.getPrice();
        }
        total = total * (1 + taxRate);
        total = total * (1 - discountRate);
        return total;
    }
}

④ 元のクラスでrunメソッドを呼び出す

public class Order {
    private List<Item> items;

    public double calculateTotalPrice(double taxRate, double discountRate) {
        return new OrderTotalCalculator(this, taxRate, discountRate).run();
    }

    public List<Item> getItems() {
        return items;
    }
}

これで、元のクラスがスッキリした。


・パラメータの追加

  1. メソッドがインターフェースに定義されている場合は、インターフェースに先にパラメータを追加する
  2. パラメータを追加する
  3. コンパイルエラーを使って呼び出し側の修正点を調べる

・メソッドからコンストラクタへのパラメータの移動

変更前のコード

public class Greeting {
    public String createMessage(String name) {
        return "Hello, " + name + "!";
    }
}

① コンストラクタにパラメータを追加する

public class Greeting {
    public Greeting(String name) {
    }

    public String createMessage(String name) {
        return "Hello, " + name + "!";
    }
}

② インスタンス変数を追加し、コンストラクタ内で代入

public class Greeting {
    private String name;

    public Greeting(String name) {
      this.name = name;
    }

    public String createMessage(String name) {
        return "Hello, " + name + "!";
    }
}

③ パラメータをインスタンス変数に置き換え

public class Greeting {
    private String name;

    public Greeting(String name) {
        this.name = name;
    }

    public String createMessage() {
        return "Hello, " + this.name + "!";
    }
}

④ メソッド内でインスタンス変数を使う

⑤ メソッドシグネチャの変更

このスクラップは5日前にクローズされました