パターン指向リファクタリング 読書メモ

Dec 31, 2016 ( Feb 11, 2022 更新 )

パターン指向リファクタリングの読書メモ。 読み終わったら別にまとめ記事を書こうと思います。

第1章 本書を執筆した理由

著者のJoshua Kerievsky氏は、デザインパターンを濫用するのではなくリファクタリングの道しるべとして使うようになったという。 あるときはパターンに近づくし、離れることもあるという。そうすることで、システムの作り込みすぎや作り込み不足を回避できるようになったと書いている。

第1章で特に印象に残った箇所は以下。

リファクタリングの目的

他の書籍でもリファクタリングの目的には触れられているが、当然この書籍でもリファクタリングの目的について説明している。

それは第2章「リファクタリング」で述べられている。

リファクタリングとパターン

Kerievsky氏は、リファクタリングをする中で、デザインパターンを使うことによって設計を改善できるところもあることに気づいたという。 この場合、パターンを取り入れたりパターンに近づくようなリファクタリング「パターン志向リファクタリング」をすることになる。

パターン志向リファクタリングの目的は以下である。

  • 重複を減らす・無くす
  • 複雑なものを単純化する
  • コードの意図が伝わりやすくする

デザインパターンは、パターンの適用によって設計上のどんな問題を解決できるのかを理解してうまく使えるものである。 以下はパターンと解決できる問題の例である。

  • Template Methodパターン
    • 同じクラス階層内のサブクラスの類似したメソッド間でコードの重複を減らしたりなくしたりできる
  • Stateパターン
    • 複雑な条件をともなった状態を変化させるロジックを単純化できる

私自身も含めてたいていのプログラマは、パターンの「目的」のセクションだけを読んで、自分の状況にパターンが向いているかどうかを判断している。しかし、そうするよりも、設計の問題とパターンが対処する問題とを比べてパターンを選択するほうがうまくいく。

なぜだろうか。パターンは問題を解決するためのものであり、ある状況において本当に役立つかどうかを知るには、そのパターンがどんな問題を解決しようとしているかを理解する必要があるからである。1

進化的設計

個人的に共感した筆者の考え方は以下の部分。むやみにパターンを適用するのではなく、以下のような改善をしていきましょうね、という話だと理解した。

  • リファクタリングをする
  • リファクタリングの中で、設計における問題点をパターンで解決できないか考えてみる
    • 前提として、パターンが「実装においてどのような変更をするか」かではなく、「設計のどのような問題を解決するのか」を知っておく

ソフトウエアに対してより優れた設計を行いたいなら、優れたソフトウエア設計そのものを調べるよりも、その優れた設計がどのように進化してきたかを調べるほうが有益だろう。真の知恵は進化の中に存在するからである。

(略)

今日まで、ソフトウエア設計の文献は、優れた解決策を教えることに重点をおき、その解決策に至る進化について教えることを怠ってきた。これは改めるべきだ。偉大な詩人ゲーテが言うように「先祖からゆずり受けたものは、それを真にわが物とするには自分の力で手に入れねばならぬ」のである。 リファクタリングに関する文献は、優れた設計に至る解決策がどのようにそれ相応の進化を遂げてきたかについて明らかにしている。そのため、それを読めば解決策をよりよく理解できるのだ。

パターンを最大限に活用しようとするなら、これと同じことが必要である。リファクタリングと関係ない、単なる再利用可能な要素としてパターンをとらえるのではなく、リファクタリングというコンテキストの中で、パターンを見なければならない。2

第2章 リファクタリング

リファクタリングとはなにか

リファクタリングとは「振る舞いを変えないまま形を変えること」、あるいは、マーチン・ファウラーの言葉を借りると「外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウエアの内部構造を変化させること」[F、53ページより引用]である。3

リファクタリングのプロセスには以下が含まれる。

  • 重複を取り除く
  • 複雑なロジックを単純化する
  • 不明瞭なコードを明確化する

リファクタリングによって問題を引き起こさないためには、変更によって何も壊れていないことを手動あるいは自動テストで確認する必要がある。

また、小さいステップで少しずつリファクタリングを行うと不具合が紛れ込むのを防ぐことができる。

新しいアーキテクチャを進化させる

Kerievsky氏は「進化的設計」として、システムのアーキテクチャに対するリファクタリング手法について、以下の方針を提案している。

  • チームは1つにする
  • アプリケーションのニーズをもとにフレームワークを構築する
  • リファクタリングによってアプリケーションとフレームワークを継続的に改善する

悪い例として、古いシステムを

  • アプリケーション層
  • フレームワーク層

に分けてリファクタリングした会社の話を挙げている。 アプリケーションチームはフレームワークチームに対して必要な機能を要求する。フレームワークチームはアプリケーションに影響を与えないようにリファクタリングする。

しかしこのやり方はリスクが高いとしている。

フレームワークチームがアプリケーションの要求についていけなければ、間違ったコードを作ってしまう可能性が高い。 アプリケーションチームのメンバーは、必要なものが得られなければ、締め切りに間に合わせるためにフレームワークを使わなくなるか、必要なものができるまで待ってペースを落とすことになる。 フレームワークを使わないというのは元の古いアーキテクチャに戻ってしまうことなのだが、コードができあがるのを待つというのもよいやり方ではない。4

チームを1つにしておくことにより、アプリケーションの要求とフレームワークの機能がちぐはぐになることがなくなる。フレームワークに価値のあるコードだけが残る。

こういった話題は、最近だとモノリシックorマイクロサービシスらへんの議論でも挙げられていると思う。 チームを分断するのは設計その他難易度が上がるため、マイクロサービシスに安易にすべきではないというのが多数派の意見…と感じる。 私自身もその意見に賛成する。

複合リファクタリングとテスト駆動型リファクタリング

  • 複合リファクタリング
    • 低いレベルのリファクタリングを組み合わせた高いレベルのリファクタリング
    • 低いレベルのリファクタリングのほとんどは、「コードの移動」。マーチン・ファウラーの「リファクタリング」本の下記の作業である
      • メソッドの抽出
      • メソッドの引き上げ
      • クラスの抽出
      • メソッドの移動
    • この書籍で紹介しているリファクタリングパターンのカタログのほとんどは、この「複合リファクタリング」に当たる
  • テスト駆動型リファクタリング
    • まず置き換えるためのコードを作成する
    • その後で、古いコードを新しいコードと入れ替える。このとき、古いコードで使っていたテストを使って再テストする

ほとんどの場合は複合リファクタリングだけで事足りるが、それでも改善が難しい場合はテスト駆動リファクタリングを使うとよりよい設計を安全に作ることができるとしている。

「アルゴリズムの取り替え」は、テスト駆動型リファクタリングが向いている。 ここでは、既存のアルゴリズムをよりシンプルできれいなものに置き換えることが目的となっている。 新しいアルゴリズムは既存の古いアルゴリズムのロジックとは異なっているため、コードの移動で済むリファクタリングではない。 また、ここでテストを作成しておくと新しいアルゴリズムに置き換えた後の複合リファクタリングにも使うことができる。

「BuilderによるCompositeの隠蔽」もテスト駆動型の例とされている。 このリファクタリングの目的は、Compositeの構築プロセスを単純にして、ユーザがCompositeを簡単に作れるようにすることである。 しかし、この設計が既存の設計とまったく異なっていると、新しい設計を適用することが不可能となってしまう。 そのため、まずテスト駆動型で古いコードに対するテストを作成し、古いコードを実装し直して置き換えていくのがよい、とされている。

複合リファクタリングが適用できそうな場面でも、テスト駆動型リファクタリングで「実装し直して置き換える」ほうが楽な場合もある、とも言っている。

第3章 パターン

パターンとは何か

ソフトウエアパターンは、建築家であり教授であり社会評論家であるクリストファー・アレグザンダーの2冊の著作に影響され生まれたとされている。

Christopher Alexander - Wikipediaを見ると、「A Pattern Language」という作がコンピュータサイエンス界隈でのデザインパターンのムーブメントを起こしたらしい。

一つ一つのパタンは、三つの部分で構成されるルールであり、一定の状況(コンテクスト)と、そこでの問題点(プロブレム)と、その解決策(ソリューション)との関係を表す。5

パターンの実装方法は数多く存在する

パターンの実装を最低限に抑えることは、進化的設計のプラクティスである。パターンを使っていない実装があったとしても、そのうちパターンを使わざるをえなくなるケースはよくあることだ。その場合は、シンプルなパターンの実装を取り入れるよう、設計をリファクタリングすることができる。本書ではこのやり方を使っている。6

パターンを取り入れる/近づく/離れるリファクタリング

リファクタリングの目的は、よりよい設計に到達することである。

Kerievsky氏は、リファクタリングの際には、常にパターンに「近づく」のではなく

  • パターンを取り入れる
  • パターンに近づく
  • パターンから離れる

というやり方があると述べている。

パターンに近づくことだけを改善だと思ってリファクタリングでしていると、結果的にコードの大部分を書き換えなければいけなくなることが予測できる。 パターンに近づく以外にも改善方法はあると言いたいのだという理解。

第4章 コードの臭い

  • 重複したコード
  • 長すぎるメソッド
    • メソッドを短くすると、ロジックを共有しやすくなる
    • 小さいコードのほうがわかりやすくなる
      • 目安は5行程度まで
  • 複雑な条件分岐
  • 基本データ型への執着
    • たいていの場合、基本データ型を使うよりもクラスを使ったほうが、よりシンプルで自然なモデリングになる
    • クラスを作ると、多くの場合、システムの他の部分にもそのクラスに含めるべきコードが見つかる。コードの再利用ができる。
  • 見苦しい露出
    • 重要ではない、あるいは間接的にしか重要ではない事柄をユーザが知ることになる。結果的に設計が複雑になる
      • 必要以上にpublicにしておくと、重要でない機能を別の場所から使われて設計が複雑になってしまいそう
      • こういった場合は、その重要ではないが他の場所で使いたい機能を切り出したほうがわかりやすくなりそう
  • 解決策の散在
    • 機能を追加したり変更する際に、あちこちの多くのコードを変更しなければならなくなる
  • クラスのインターフェース不一致
    • 複数のクラスのインターフェースが異なるにもかかわらず、クラス自体は極めて似ている場合
    • インターフェースの数が無駄に増え、複雑さが増すのでインターフェースを共通化できないか検討する
  • 怠け者クラス
    • 見返りに合わないようなクラスは複雑さが増すので避ける
    • Singletonを無駄に使ってグローバルデータ同然のものに依存してしまう場合など
  • 巨大なクラス
    • 責務を多く持ちすぎているクラス
    • 色々な機能に依存したりされている場合があるので変更にコストがかかりそう
  • スイッチ文
    • if elseやswitchで設計が必要以上に複雑になっている場合がある
  • 組み合わせの爆発的増加
    • 種類や量の異なるデータオブジェクトを使って同じことを行うコードが数多く存在している場合
    • 複雑になり変更にコストがかかるため改善すべき
  • 風変わりな解決策
    • ある問題が、同じシステム内の複数の場所で別の手法で解決されている場合。どれかは風変わりあるいは場当たり的な解決になっている
    • どの解決策が最善かを判断し、解決策に一貫性をもたせる
    • コードの再利用ができるようにする

第5章 パターンを取り入れるリファクタリングのカタログ

学習の順序

おすすめの学習順序が書いてった。この通りに読み進めていこうと思う。

  • Creation Methodによるコンストラクタの置き換え(59)
  • コンストラクタの連鎖(358)
  • Factoryによるクラス郡の隠蔽(82)
  • Factory Methodによるポリモーフィックな生成の導入(91)
  • Strategyによる条件判断の置き換え(135)
  • Template Methodの形成(217)
  • メソッドの構造化(129)
  • Compositeによる暗黙的なツリー構造の置き換え(188)
  • BuilderによるCompositeの隠蔽(100)
  • Collectiong Parameterによる累積処理の書き換え(331)
  • Compositeの抽出(226)
  • Compositeによる単数・複数別の処理の書き換え(236)
  • Commandによる条件つきディスパッチャの置き換え(202)
  • Adapterの抽出(272)
  • Adapterによるインターフェースの統合(260)
  • クラスによるタイプコードの置き換え(303)
  • Stateによる状態変化のための条件判断の置き換え(175)
  • ヌルオブジェクトの導入(319)
  • Singletonのインライン化(119)
  • Singletonによるインスタンス化の制限(314)
  • Observerによるハードコートされた通知の置き換え(249)
  • Decoratorによる拡張機能の書き換え(151)
  • インターフェースの統合(362)
  • パラメータの抽出(365)
  • Factoryによる生成処理の書き換え(70)
  • Visitorによる累積処理の書き換え(338)
  • Interpreterによる暗黙的な言語処理の書き換え(283)

第6章 生成

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

コンストラクタが無駄に増え、開発者はどのコンストラクタを使えばよいかわからなくなる。 コンストラクタの代わりに、Creation Methordという、インスタンスを生成するメソッドを用意し、適切な名前をつけることにより用途がわかりやすくなる。

例えば以下のような利点がる:

  • 同じ型と引数の数のコンストラクタを作れないという制限を回避して初期化を行うことができる
  • メソッドの名前で初期化の意味をあらわすことができる

コンストラクタが多すぎて煩雑なコードになった場合は、Creation Methodの作成に先んじて以下の変更でわかりやすくならないか考慮すべきである。

  • クラスの抽出
    • そのクラスの責務が多すぎる場合
  • サブクラスの抽出
    • 特定のコンストラクタで生成したインスタンスが、ごく1部の特定のインスタンス変数しか利用していない場合

2017/01/14 メモ

現時点だとCreation Methodを中途半端に入れるとわかりづらくなりそうなので、「クラスの抽出」のほうがましになる場合が多そうに思える。他にも生成のためのパターンはあるので、それも読み切ってからまた考えてみる。

Factoryによるクラス郡の隠蔽(Encapslate Classes with Factory)

1つのパッケージ内に存在して共通のインターフェースを実装しているクラス郡を、クライアントが直接インスタンス化している。7

この「クラス郡」のクラスのコンストラクタをpublicでなくして、クライアントにはFactory経由でインスタンスを生成させる。

動機

クライアントが直接クラスをインスタンス化できて役に立つのは、それらのクラスの存在そのものをクライアントが知る必要がある場合だけである。8

読んだ感じ、「Creation Methodによるコンストラクタの置き換え」の別パターンだと思った。

  • メリット
    • インスタンスの生成をわかりやすくできる。「Creation Methodによるコンストラクタの置き換え」と同様
    • 公開する必要のないクラスが隠蔽されるため、大きなプロジェクトの場合は誤った使い方をされることを避けることができる
      • 「Creation Methodによるコンストラクタの置き換え」の変形版っぽい。生成するインスタンスのクラスはprotected, private等で隠蔽される
  • デメリット
    • 新しい種類のインスタンスを生成したい場合はCreation MethodをFactoryに追加しなければいけない
    • 生成するインスタンスのクラス自体は隠蔽されているので、ソースコードレベルで公開していない場合は、ユーザがFactoryをカスタマイズすることができない

実装例

package com.github.mookjp.refactoring.encapslatebyfactory;

public interface User {

    String getName();
}
package com.github.mookjp.refactoring.encapslatebyfactory;

public class Professor implements User {

    private String name;

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

    @Override
    public String getName() {
        return "Dr. " + this.name;
    }
  }
package com.github.mookjp.refactoring.encapslatebyfactory;

public class Student implements User {

    private String name;

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

    @Override
    public String getName() {
        return this.name;
    }
}

User interfaceを実装するクラスをそのまま初期化した場合のテスト:

package com.github.mookjp.refactoring;

import com.github.mookjp.refactoring.encapslatebyfactory.Professor;
import com.github.mookjp.refactoring.encapslatebyfactory.Student;
import org.junit.Before;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

public class UserTest {

    private static final String STUDENT_NAME = "Alice";

    private static final String PROFESSOR_NAME = "Bob";

    private Student student;

    private Professor professor;

    @Before
    public void before() {
        student = new Student(STUDENT_NAME);
        professor = new Professor(PROFESSOR_NAME);
    }

    @Test
    public void should_get_student_name() {
        assertThat(student.getName(), is(STUDENT_NAME));
    }

    @Test
    public void should_get_professor_name() {
        assertThat(professor.getName(), is("Dr. " + PROFESSOR_NAME));
    }
}

Factory:

package com.github.mookjp.refactoring.encapslatebyfactory;

public class UserFactory {

    public Student getStudent(String name) {
        return new Student(name);
    }

    public Professor getProfessor(String name) {
        return new Professor(name);
  }
}

Factoryを使った場合のテスト:

package com.github.mookjp.refactoring;

import com.github.mookjp.refactoring.encapslatebyfactory.Professor;
import com.github.mookjp.refactoring.encapslatebyfactory.Student;
import com.github.mookjp.refactoring.encapslatebyfactory.UserFactory;
import org.junit.Before;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

public class UserFactoryTest {

    private static final String STUDENT_NAME = "Alice";

    private static final String PROFESSOR_NAME = "Bob";

    private UserFactory userFactory;

    @Before
    public void before() {
        userFactory = new UserFactory();
    }

    @Test
    public void should_get_student_name() {
        Student student = userFactory.getStudent(STUDENT_NAME);
        assertThat(student.getName(), is(STUDENT_NAME));
    }

    @Test
    public void should_get_professor_name() {
        Professor professor = userFactory.getProfessor(PROFESSOR_NAME);
        assertThat(professor.getName(), is("Dr. " + PROFESSOR_NAME));
    }
}

Factory Methodによるポリモーフィックな生成の導入(Introduce Polymorphic Creation with Factory Method)

階層内のクラスが、オブジェクトの生成ステップを除いて同じようにメソッドを実装している。 そういったメソッドをスーパークラスで1つにまとめ、そこでFactory Methodを呼び出してインスタンス化の処理を行う。9

以下のようなやり方で実装する。

  • 抽象クラスやインターフェースで、インスタンスを生成するメソッド(Factory Method)を宣言する。
  • 継承クラスや実装クラスで、Factory Methodをオーバーライドあるいは実装する。

動機

  • インスタンスの生成部分を除いて、ほぼ共通の処理を持つ複数のクラスがある

    • ほとんどコードが重複しているので、変更時に手間になる。共通化したい。
  • メリット

    • Factory Methodで使うためにクラスがどの型を実装しなければならないのかが明確になる
      • 具体的に何が楽になるのか理解できなかった
  • デメリット

    • Factory Methodを実装するクラスに不必要な引数を渡さなければならないことがある

BuilderによるCompositeの隠蔽

第11章 ユーティリティ

コンストラクタの連鎖

複数のコンストラクタに、同じコードが重複して含まれている。この場合は、コンストラクタを連鎖させるとコードの重複を減らすことができる。

package com.github.mookjp.refactoring.chainconstructor;

public class Profile {

    private String firstName;
    private String lastName;
    private String description;

    public Profile(String firstName) {
        this.firstName = firstName;
        this.lastName = SPECIAL_VALUES.NO_INPUT.toString();
        this.description = SPECIAL_VALUES.NO_INPUT.toString();
    }

    public Profile(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.description = SPECIAL_VALUES.NO_INPUT.toString();
    }

    public Profile(String firstName, String lastName, String description) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.description = description;
    }

    private enum SPECIAL_VALUES {

        NO_INPUT("no input");

        private final String message;

        SPECIAL_VALUES(String message) {
            this.message = message;
        }

        public String toString() {
            return this.message;
        }
    }
}

変更後

package com.github.mookjp.refactoring.chainconstructor;

public class RefactoredProfile {

    private String firstName;
    private String lastName;
    private String description;

    public RefactoredProfile(String firstName) {
        this(firstName, SPECIAL_VALUES.NO_INPUT.toString());
    }

    public RefactoredProfile(String firstName, String lastName) {
        this(firstName, lastName, SPECIAL_VALUES.NO_INPUT.toString());
    }

    public RefactoredProfile(String firstName, String lastName, String description) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.description = description;
    }

    private enum SPECIAL_VALUES {
    // 省略
   }
}

第7章 単純化

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

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

class-state

動機

  • 状態を変えるための複雑な条件ロジックを扱いやすくする
  • 状態が変わるオブジェクトのことをデザインパターンではContextと呼ぶ
    • Contextは、状態依存の振る舞いを状態オブジェクトに委譲する
    • Contextは、状態オブジェクトAから別の状態オブジェクトBを参照する、というやり方で状態遷移を表現する
  • 状態を変えるための条件ロジックを1つのクラスから取り出し、それぞれのロジックを異なる状態オブジェクトを生成するクラス郡に持たせることで、状態間の遷移を俯瞰しやすいシンプルな設計になる

クラス志向プログラミング言語では以下のようなクラス設計になると思われる。 Contextの状態を変更する際には、state attributeの参照を変更する。

context-and-state

他のパターンと同じく、ロジックが複雑にならない限りはむやみにパターンを使うべきではないというアドバイスがされている。

ただし、クラス内の状態遷移のロジックを簡単に理解できるのであれば、(今後もっと多くの状態遷移を追加する予定がないかぎり)Stateパターンを取り入れるリファクタリングを行う必要はないだろう。

(略)

Stateを取り入れるリファクタリングを行う前に、まずは、「メソッドの抽出」などの簡単なリファクタリングによって状態を変える条件ロジックを整理できないかを検討すべきである。 できなければ、Stateを取り入れるリファクタリングによって、何行にもわたる条件ロジックを取り除いたり減らしたりし、わかりやすく拡張しやすいシンプルなコードを作成することができる。10

Strategyパターンとの違い

Stateパターンは、前述したように

  • Contextオブジェクト
  • 状態オブジェクト

があり、Contextオブジェクトが状態に応じたロジックの分岐が多数発生する場合に有効である。

それに対してStrategyパターンは、

  • Strategy実行クラス
  • Strategyクラス
    • 複数のアルゴリズムをあらわす

があり、状態とは関係なく実行したいアルゴリズムの処理をStrategyクラスに委譲することによりコードの見通しをよくできる。

利点と欠点

  • 利点
    • 状態を変えるためのロジックがなくなる、あるいは減る、単純になる
  • 欠点
    • 状態遷移ロジックがもともと単純な場合には、Stateパターンの導入により余計に複雑にしてしまうだけである

手順

Contextは以下のような実装。

public class Context {
    private State state;

    public void setState(State state) {
        this.state = state;
    }

    public State getState() {
        return this.state;
    }

    public void update() {
        this.state.update(this);
    }
}

事前にContextのテストを作成しておく。 Contextテストは、リファクタリングの過程で随時変更していく。

public class ContextTest {
    private Context context;

    @Before
    public void setUp() {
        this.context = new Context();
    }

    @Test
    public void updateToStateB() {
        context.setState("stateA");
        context.update();
        assertThat(context.getState().getName(), is("B"));
    }

    @Test
    public void updateToStateA() {
        context.setState("stateB");
        context.update();
        assertThat(context.getState().getName(), is("A"));
    }
}
1. 状態オブジェクトを生成する状態クラスのスーパークラスを作成する。以下、「状態スーパークラス」とする
  • リファクタリング前は、状態による条件分岐はクラスのフィールドを参照することによって行われているはずである。このフィールドの部分を上記のように変更する
package com.github.mookjp.rtp.state;

public class State {
    private String name;
    public static final StateA STATE_A = new StateA();
    public static final StateB STATE_B = new StateB();

    public State(String name) {
        this.name = name;
    }
}
2. 1の状態スーパークラスを抽象クラスにする。それぞれの状態をあらわす定数を状態スーパークラスのサブクラスにする
public abstract class State {
    private String name;
    public static final StateA STATE_A = new StateA();
    public static final StateB STATE_B = new StateB();
    // 略
public class StateA extends State {

    public StateA() {
        super("A");
    }
}
3. 状態遷移によって、Contextの状態フィールドを変更している部分を新しく作成した状態スーパークラスに移行する
public abstract class State {

// 略

    public void update(Context context) {
        if (context.getState().equals(STATE_A)) {
            context.setState(STATE_B);
            return;
        }
        context.setState(STATE_A);
    }
4. 状態遷移の際に、特定の状態に対してのみ発生する操作を、その状態クラスに移行する
public abstract class State {

// 略

    public abstract void update(Context context);
public class StateA extends State {

// 略

    @Override
    public void update(Context context) {
        context.setState(STATE_B);
    }
}

ここまででテストコードは以下のようになる。

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


Martin Fowler 「新装版 リファクタリング 既存のコードを安全に改善する」より引用

「パターン志向リファクタリング」にはMartin Fowlerの「リファクタリング」からの引用が多い。 書籍の中で言及されている手法について、こちらにも書いておく。

メソッドの抽出

コードの断片をメソッドにして、それに目的を表すような名前をつける。11

動機

長すぎるメソッド や、コメントがなければその目的が理解できないメソッドを目にしたときは、その断片をメソッドにします。

(略)

うまく命名された短いコードが好ましいという理由は、いくつかあります。第1に、メソッドの粒度が細かければ、他のメソッドからそれを使える可能性が増えます。第2に、上位のメソッドがコメント列のように読めます。また、メソッドの粒度が細かければ、オーバーライドもしやすくなります。11

変更前

    public void printOwing(double amount) {
        printBanner();

        // 明細の表示
        System.out.println("name: " + name);
        System.out.println("amount: " + amount);
    }

    private void printBanner() {
        System.out.println("============== hello =============");
    }

変更後

    public void printOwing(double amount) {
        printBanner();
        printAmount(amount);
    }

    private void printBanner() {
        System.out.println("============== hello =============");
    }

    private void printAmount(double amount) {
        System.out.println("name: " + name);
        System.out.println("amount: " + amount);
    }

メソッド名の変更

メソッドの名前を変更する。12

動機

主張する重要なコードスタイルの1つに、複雑な処理を分解して小さなメソッドの集まりにすることがあります。

これをへたに行うと、たくさんの小さなメソッドが何をしているの かを調べる際に、あたふたしなければならない状況に陥ってしまいます。このような事態を回避する鍵は、メソッドのネーミングです。メソッドにはその意図が伝わるような名前をつけるべきです。12

メソッドの移動

同様の本体を持つ新たなメソッドを、それを最も多用するクラスに作成する。元のメソッドは、単純な委譲とするか、またはまるごと取り除く。13

動機

私は、クラスの振る舞いが多すぎる場合や、クラス間でのやり取りが多く、結合度が高すぎる場合にメソッドを移動します。メソッドを移動することで、クラスは単純になり、結果として、ひとまとまりの責務をすっきりした実装に収めることができます。13

メソッドの引き上げ

同じ結果をもたらすメソッドが複数のサブクラスに存在する。それらをスーパークラスに移動する。14

動機

重複がある限り、一方への修正が他方に反映されないことになるというリスクに直面しているのです。14

クラスの抽出

2つのクラスでなされるべき作業を1つのクラスで行っている。 クラスを新たに作って、適切なフィールドとメソッドを元のクラスからそこに移動する。15

動機

大きすぎて簡単には理解できないクラスがある。 こういったクラスから切り出せるところは切り出して、わかりやすくする。(責務を分けるとも言える)

切り出し元のクラス内で、互いに依存しあっているフィールドやメソッドがあれば、それはひとまとまりとして切り出せる候補になる。

サブクラスの抽出

あるクラスの特定のインスタンスにだけ必要な特性がある。 その1部の特性を持つサブクラスを作成する。16

動機

あるクラスが、特定のインスタンス郡でしか使われない振る舞いを持っている場合、その振る舞いをサブクラス化することによってロジックをわかりやすくすることができる。

この「特定のインスタンス郡でしか使われない振る舞い」は、タイプコードである場合がある。 この場合は、「サブクラスの抽出」ではなく、以下を行うことができる。

  • サブクラスによるタイプコードの置き換え
  • State/Strategyによるタイプコードの置き換え

「サブクラスの抽出(継承)」ではなく、「クラスの抽出(委譲)」を行うこともできる。 どちらのリファクタリング方法にも利点と欠点があります。

  • サブクラスの抽出(継承)
    • 設計・作業は難しくない
    • サブクラスなので、切り出し元の親クラスの制約を受ける。(クラスの抽出で委譲した場合、この制約はなくなる…が、前述したように難易度が高くなることもある)
  • クラスの抽出(委譲)
    • サブクラスの抽出のように、切り出し元の親クラスの制約を受けない
    • サブクラスの抽出と比べ、難易度が高くなる

class-state


  1. p.7から引用 ↩︎

  2. p.9から引用 ↩︎

  3. p.11より引用 ↩︎

  4. p.19より引用 ↩︎

  5. p.25より引用 ↩︎

  6. p.31より引用 ↩︎

  7. p.82より引用 ↩︎

  8. p.83より引用 ↩︎

  9. p.91から引用 ↩︎

  10. p.176から引用 ↩︎

  11. 「リファクタリング」p.110より引用 ↩︎

  12. 「リファクタリング」p.273より引用 ↩︎

  13. 「リファクタリング」p.142より引用 ↩︎

  14. 「リファクタリング」p.322より引用 ↩︎

  15. 「リファクタリング」p.149より引用 ↩︎

  16. 「リファクタリング」p.330より引用 ↩︎

Return to top