第6章:時間が無いのに変更しなければなりません

はじめに

時間がない中、機能追加を行う際にはどうしたらいいかについての説明。
既存のクラスをテストで保護せずに変更を行うテクニックの紹介。

このテクニックを使うと、明確な新しい責務と古い責務を分離できる。
既存のクラスをテストで保護する時間を取れないという理由だけで新しいクラスを作成する場合がある。

  • このテクニックを使うと、「古い大きなクラスの残骸+新しいクラス&メソッドがある」という状態になる。
  • 古い残骸を避けるのをやめ、テストで保護するようになるらしい。

スプラウトメソッド(Sprout Method)

既存コードに対してある新規の実装が必要になったとき、その追加の処理を既存コードにベタ書きするのではなく、そのコードをメソッドとして記述する手法。
そのメソッドは、新しい機能を必要とする場所から呼び出す。
呼び出し側のコードをテストで保護するのは難しいかもしれないが、「新しいコード」に対してはテストを書いて保護することができる。


■実例
既存コードに対して「ただエントリを追加するのではなく、新しいエントリかどうかをチェックするコードを書く」必要が出た場合の例。
修正を既存コードにベタ書きした例が以下。

public class TransactionGate {
    …

    public void postEntries(List entries) {
        List entriesToAdd = new LinkedList();
        for(Iterator it = entries.iterator(); it.hasNext();) {
            Entry entry = (Entry)it.next();
            if(!transactionBundle.getListManager().hasEntry(entry)) {
                entry.postDate();
                entriesToAdd.add(entry);
            }
        }
        transactionBundle.getListManager().add(entriesToAdd);
    }
    …
}


■既存コードに追加修正をベタ書きすることの問題点

  1. 新しく追加したコードと既存のコードの区別がない。
  2. 日付の設定(postDate)と重複エントリのチェック(transactionBundle.getListManager().hasEntry(entry))という2つの操作が混じっている。
  3. 一時変数の利用(これ自体は必ずしも悪ではない)により、重複しないエントリのすべてに対して何らかの修正を行う必要性が出たとき、今後もpostEntriesメソッドに対してコードが追加されていくことになりかねない。


■解決策
重複エントリを除外する処理は、それを責務に持つ別メソッドを定義して扱う。
一時変数はあるものの、修正前よりは可読性はある。

public class TransactionGate {
    …

    List uniqueEntries(List entries) {
        List result = new ArrayList();
        for(Iterator it = entries.iterator(); it.hasNext(); ) {
            Entry entry = (Entry) it.next();
            if(!transactionBundle.getListManager().hasEntry(entry)) {
                result.add(entry);
            }
        }
    }
    …

    public void postEntries(List entries) {
        List entriesToAdd = uniqueEntries(entries);
        for(Iterator it = entriesToAdd.iterator(); it.hasNext();) {
            Entry entry = (Entry)it.next();
            entry.postDate();
        }
        transactionBundle.getListManager().add(entriesToAdd);
    }
    …
}

■長所
古いコードと新しいコードを明確に区別できるようになる。

  • 古いコードをすぐにテストで保護できなくても、少なくとも変更部分を独立して理解できる。
  • 新しいコードと古いコードのインタフェースも明確になる。
  • 影響を受ける変数をすべて把握できるため、その状況において正しいコードであるかの判断もしやすくなる。

■短所
単に新しいメソッドとして新しい機能を加えているだけ。

  • もとのメソッドはテストで保護されるわけでもなく、改善するわけでもない。保護するのはあくまで「新しいメソッド」。
  • とはいえ、少なくとも追加した部分についてはテストで保護されているし、古いコードと新しいコードの区別ができるようになることにもつながる。


スプラウトクラス(Sprout Class)

変更に必要な機能を別のクラスとして切り出し、そのクラスをもとのクラスから利用する方法。

以下のいずれかの場合においてスプラウトクラスは作成される。

  1. 変更のために、あるクラスにまったく新しい責務を持たせたい場合。
  2. 変更対象のオブジェクトを、テストハーネス内で生成できる方法が無い(あるいはそれを見つけるのに時間がかかって想定時間内に終わらない)場合。

 - テストハーネスの中でコンパイルできるのならスプラウトメソッドの適用ができるがそれができない。
 - サーブレットクラス等。


■長所
コードを直接書き換える方法よりも、確信をもって変更を進められる。

  • 変更点をテストできるので、少なくとも新しく作ったところについてはテストで動作が保証できる。

■短所
仕組みが複雑になる。

  • 本来1つのクラスにあるべき処理がバラバラになる。


ラップメソッド(Wrap Method)

振る舞いを追加する際のテクニックの一つ。
ラップメソッドには2つの側面があるが、いずれも「新しい要件を追加しつつ、接合部を導入できる方法」。

■側面①
呼び出し元はラップされたメソッド内でどんな修正があったのかを気にする必要なく、やりたい処理を委託できる。そしてそれは成功する。

public class Employee {
    …
    private void dispatchPayment() {
        Money amount = new Money();
        for(Iterator it = timecards.iterator(); it.hasNext(); )) {
            Timecard card = (Timecard) it.next();
            if(payPeriod.contains(date)) {
                amount.add(card.getHours() * payRate);
            }
        }
        payDispatcher.pay(this, date, amount);
    }

    public void pay() {
         logPayment();
         dispatchPayment();
    }

    public void logPayment() {
        …
    }
}

<追加要求に応えるための変更点>

  • payメソッドの名前をdispatchPaymentに変更してprivateにした。
  • 同名の新しいpayメソッドを作り、そこでは支払いを記録してから(logPaymentを実行してから)支払情報を送る(dispatchPaymentを実行する)。

<ミソ>

  • 「元のメソッドと同じ名前のメソッドを新しく作り、古いコードに処理を委譲する」ことで、payメソッドの呼び出し側は変更内容を知ることも気にする必要もない。payメソッドを実行したら処理は成功する。

■側面②
まだどこからも呼び出されていない新しいメソッドを追加した場合、そのクラスの利用者はやりたい処理を選択できる。

public class Employee {
   
    public void makeLoggedPayment() {
        logPayment();
        pay();
    }
    
    public void pay() {
        …  //支払処理
    }

    public void logPayment() {
        …  //支払いを記録
    }

}

<変更点>
makeLoggedPaymentメソッドを新規定義。
 ⇒Employeeクラスの利用者は、いずれかの方法で支払いを行うかを選ぶことができる。
# 側面①の場合良くも悪くもpayを呼ぶと「支払処理」と「支払記録」の両方が実行されるようになる。


■長所
既存メソッドの長さは変わらない。

既存の機能から新しい機能を明確に独立させられる。

  • 1つの目的のためのコードが別の目的のためのコードとまじりあってしまうこともない。


■短所
メソッドの名前を変えるとき、不適切な名前を付けてしまいがち。

  • payからdispatchPaymentにしたが、そもそもこの名前も好ましいとは言えない。

ラップクラス(Wrap Class)

既存のクラスに変更を加えずに、システムに新しい振る舞いを追加する手法。
既存のメソッドを使う「別のもの」(=クラス)に振る舞いを追加する手法が紹介されている。
早い話が、「Decoratorパターン」。

■ラップクラスのメリット
既存の実装を変更することなく、新しい振る舞いを定義することができる。

  • 必要な一連の操作を定義した抽象クラスを作成する。
  • その抽象クラスを継承したサブクラスを作成し、コンストラクタでは抽象クラスのインスタンスを受け取って、各メソッドの本体を提供する。


書籍ではEmployeeクラスの実装をもとに説明がされている。既存実装に「支払をした事実を記録する」という要求を受けて実装を追加する。

  • payメソッドを持つ別のクラスを作る。
  • そのクラスのオブジェクトはEmployeeオブジェクトを保持し、payメソッドで記録を行い、支払いを実行するためにEmployeeオブジェクトに処理を委譲する。

ラップする側のクラス(LoggingEmployee)は、クラスの呼び出し側にラッパーを意識させないようにするため、ラップ対象のクラス(Employee)と同じインタフェースを持つ必要がある。

■Decoratorパターンを使う際に注意しないといけないこと

  • ラップする対象の少なくとも1つは、ラッパーではない「基本的な」クラスにする必要がある。

  - 書籍の例だと「ACMEController」。

  • 使いすぎないようにする。

  - Decoratorの中にさらにDecoratorが、そのDecoratorにはさらに別のDecoratorが含まれていて…というコードがあると面倒くさいし、ややこしい。