パターン指向リファクタリング入門を読んでみた

古いですが"パターン指向リファクタリング入門~ソフトウエア設計を改善する27の作法"を読んでみたので感想など

□生成

Creation Methodによるコンストラクタの置き換え

JavaC++ではコンストラクタがクラス名と一致するためどんなインスタンスが作られるのかが把握しづらくなる場合がある。
例えばローンを表すクラスがあってそれに対して支払い期日までに払わなければいけないタームローンや融資枠や有効期限が定められる回転融資など7種類のコンストラクタがある場合は、それぞれのインスタンスを作成すCreateionメソッドを用意した方がコードの見通しが良くなる。 コンストラクタをCreation Methodに置き換えた場合は以下のようになりどんなインスタンスが作られるのかがわかりやすくなる。

public class CapitalCalculation {
  public static Loan createTermLoan(double commitment, int riskRating, Date maturity)  
    return new Load(commitment, riskRating, maturity);

Factoryクラスによるインスタンス生成

Creation Methodの数が多い場合はインスタンスを生成するためだけのファクトリークラスを作ることで見通しが良くなる。 例えばローンなどインスタンスをどう生成するかで気をつけなければいけない場合は、一つのFactoryクラスでインスタンス生成を行うようにした方が良い。インスタンス生成のためのパラメータが後から増え他としてもFactoryクラスがあればどこにインスタンス生成のためのロジックがまとめられるのかがわかりやすくなる。

Factoryによるインスタンス生成を強制

Factoryによるインスタンスを強制する場合は、デフォルトのコンストラクタをprivateにする。

public class CapitalCalculation {
  private CapitalCalculation(,,,){
    ,,,
  }

javajava.util.Collectionsクラスはクラスをカプセル化する良い例らしい。javaのコレクション型ではListについてはUnmodifiableListやSynchronizedListなどあるが直接newするのではなくファクトリーメソッドを使うようにすることで利用者側でインスタンス生成について気をつけることが減っている。
開発時も同様で利用者側での負担を減らすためにファクトリーメソッドを強制して内部クラスを隠蔽する必要がある場合があるかもしれない。

BuilderによるCompositeの隠蔽

Compositeパターンに対してFactoryクラス一つですべてのインスタンス生成を行うのは無理があるのでインスタンス生成の責務をうまく分けた方が良いということか。流行っているDDDであればちゃんと集約をしておいて対応のfactoryでどこのインスタンスを生成するのかの責務をわかるようにしておけというのと同じ内容のはず。この辺りはどうモデリングして集約させるかに慣れたら自然と出来るようになる気がする。

Singoletonのインライン化

singletonのパターンは設計が楽になるし特に組み込み系などであれば全体の状態を管理するために必須となるしその為にもスレッドセーフなsingletonで実装を行わなければならない。ただ不必要にsingletonを使っている場合があってその場合は問題が起きる原因となりうるので回避出来るようにしたい。 スレッドセーフとかは別にしてシングルトンオプジェクトの取り扱う際に利用側からしたらオブジェクトの値を直接変更するよりも、参照、更新するための関数には決まったものを使うようにしたい。その為に方法として以下がある。 - Singletonの機能を1つのクラスに移し、そのオブジェクトを格納してアクセス手段を提供する。

以下にConsoleオブジェクトをクラスに変更してそれにアクセスするためのBlackjackクラスを利用するサンプルを示す。

public class Console {
  privae static HitStayResponse hitStayResponse = new HitStayResponse();

  private console() {
    super();
  }

  public static HitStayResponsne
    obtainHitStayResponse(BufferedReader input) {
      hitStayResponse.readFrom(input);
      return hitStayResponse;
    }

  public static void
    setPlayerResponse(HitStayResponse newHitStayResponse){
      hitStayResponse = newHitStayResponse;
    }
}

public class Blackjack...
  public HitStayResponse obtainHitStayResponse(BufferedReader input) {
    return Conseole.obtainHitStayResponse(input);
  }
  public void setPlayResponse(HitStayResponse newHitStayResponse){
    Console.setPlayerResponse(newHitStayResponse);
  }

singletonのstatic変数へのアクセスを提供するConsoleクラスおよびそれを利用するBlackjackで専用のメソッドは準備するとして、重要なのはConsole、Blackjackのそれぞれの粒度で利用側が使いやすくなるよううまく隠蔽を行うことのはず。

□単純化

メソッドの構造化

判定処理とかは一目で何をやっているのか分かるようにまとめる。

public void add(Object element) {
  if(!readOnly){
    int newSize = size+1;
    if(newSize > elements.length){
      Object[] newElements = new Object[Elements.length + 10];
      for(int i=0; i<size; i++){
        newElements[i] = elements[i];
      }
      elements = newElements;
    }
    elements[size++] = element;
  }
}

リファクタリング

public void add(Object element) {
  if(readOnly) return;
  if(atCapacity()){
    grow();
  }
  addElement(element);
}

これはリファクタリング後の方が圧倒的に見やすくなる。一目で分かる粒度のメソッドに分けることをComposed Methodというらしい。

Strategyによる条件判断の置き換え

例えばローンクラスに10種類のインスタンス生成パターンがあって3種類の料金計算Strategyがあるとしたらローンクラスのインスタンス生成は専用のファクトリークラスで行い、料金計算のためのStrategy生成はファクトリークラスがインスタンス生成の為にローンクラスのメソッドを呼び出すタイミングで行うようにする。Strategy生成の責務について3種類ぐらいであればローンクラスにもたせておいて複雑化してきたら別でstrategyを生成するためのfactoryを用意したら良いのか。

Decoratorによる拡張機能の書き換え

たまに使う場合がある機能を拡張機能として外に出すこと

public class StringNode extends AbstractNode...
  public satic Node createStringNode(StringBuffer textBuffer, int textBegin, int textEnd, boolean shouldDecode) {

    if(shouldDecode)
      return new DocodeingNode(
        new StringNode(textBuffer, textBegin, textEnd);
      )
    return new String Node(textBuffer, textBegin, textEnd);
  }

  public String toPlainTextString() {
    return textBuffer.toString();
  }
...

public class DecodingNode implements Node...
  private Node delegate;

  public DecodingNode(Node newDelegate){
    delegate = newDelegate;
  }

  public String toPlainTextString() {
    return Translate.decode(delegate.toPlainTextString());
  }

上記サンプルではStringNodeにshouldDecodeのオプションがあった場合はDecodingNodeのインスタンスが作られていてtoPlainTextString呼び出した際にはNodeクラスにロジックを利用した上でTranslate.decodeの追加処理を行っている
scalaのtraitをmix-inするのと同様か

Stateによる状態変化のための条件判断の置き換え

状態によって異なる初期があった場合はstateパターンによって、状態毎に生成するインスタンスを切り替えて利用する。似た処理を行う状態が増えることが予想される場合は使っても良いかも。

Compositeによる暗黙的なツリー構造の置き換え

データ型がツリー構造や再帰構造だったらcompositeを使えというもので、サンプルがxmlのパーサだったのでそういった時にはぴったしだと思う。

Commandによる条件付きディスパッチャの置き換え

特にwebアプリで使われるデザインパターン。クライアントからのリクエストをコマンドに投げて必要な処理を行う。最近はリアクティブが注目されているのでその場合はCRUDよりもCQRSの粒度でコマンドを作りたい。

□汎用化

Template Methodの形成

よく使われるので特に意識する必要はないと思う。サブクラスで共通で必要になるメソッドをスーパークラス側に持っていくというもの

Compositeの抽出

似通った特性を持つ2つ以上のクラスがあれば共通の特性をスーパークラスに移動して共通化させる。こうすることでサブクラスがぐっとシンプルになることがある。

Compositeによる単数・複数別の処理の置き換え

Compositeの抽出を繰り返す

Observerによるハードコードされた通知の置き換え

通知を受け取るためのインターフェイスを統一させて監視を行いやすくするというものでマルチスレッドでプログラムを動かし通知があったら逐次処理を反映していくとか用件があったら必要なんだと思う。

Adapterによるインタフェースの統合

利用側が違いを意識せず使えるようにするため必要

Adapterの抽出

利用側がAdapterの振る舞いにアクセスする必要がある。例えば使用するDBのバージョンによって使うクラスを切り替える場合などあり、その場合はクラスの生成側にインスタンス生成用のロジックを持たせる。

Interpreterによる暗黙的な言語処理の置き換え

例えばリストの検索を行う際などに絞り込みを行うためのインターフェースを用意しておいてそれを実装したクラスによるリストをiterateに反映することで期待した連続処理を行えるようにする。

□全体的な感想

既存の処理に対してデザインパターンをどう適用するかの本になっています。実践でデザインパターンをどう適用するのかわからない人は読む価値があるのかと思います。リファクタリングを行うとしたら上流のモデリングなどが必要になると思いますがこの本が扱っているのは上流の分析が済んだ上で、それをどうオブジェクト指向で実装するかというものになっています。レガシーで開発も行われるプログラムにどうリファクタリングで立ち向かうとかといったら別でこんなのを読んだ方が良いでしょうか。