第1章 リファクタリング - 最初の例
- リファクタリングに入る前に、一連のテストが用意されていることを確認する
- テストには、自己診断機能が不可欠
- リファクタリングでは、小さなステップでプログラムを変更し、テストを実行する
- 誤った修正をしても、バグを見つけることは簡単
- 一時変数の使用が問題を引き起こす場合がある
- メソッド内だけで有効で、長くて複雑なルーチンの原因になるから
第2章 リファクタリングの原則
リファクタリングの定義(P.53)
リファクタリングは2つの定義を持つ
- リファクタリング(名詞);外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること
- リファクタリングする(動詞);一連のリファクタリングを利用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること
リファクタリングとは、効率よく統制されたやり方で、コードをきれいにするテクニックを提供すること
<br/>
リファクタリングとは、ソフトウェアを分かりやすくするためだけに変更すること
- パフォーマンスの最適化とは対照的
- パフォーマンスチューニングは、コードが理解しにくくなることが一般的
- パフォーマンスの最適化とは対照的
2つの帽子(P.54)
リファクタリングとは、ソフトウェアの外的振る舞いを保つこと = 以前と同じ機能を提供すること
<br/>
機能追加とリファクタリングは別
- 機能追加は、既存のコードに変更を加えず、機能を拡張する
- リファクタリングは、コードの再構築をするのみ
- 機能追加はしない
- 原則として、テストの追加はしない(漏れていた場合は例外)
<br/> - 機能追加、リファクタリングのどちらの帽子をかぶっているか、常に意識すること - コードの構造を変更すれば、すぐに機能追加ができることに気づいたら、リファクタリングの帽子をかぶる - リファクタリングがおわったら、機能追加の帽子をかぶる - 機能を作り終えたがわかりにくい…リファクタリングの帽子をかぶる
リファクタリングを行う理由(p.55)
リファクタリングはソフトウェア設計を改善する(P.55)
- リファクタリングなしでは、プログラム設計が劣化していく
- 定期的なリファクタリングで、コードの品質をしっかり保つ
- 設計が悪いと、同じ処理をするコードを余計に書くことになる
- 重複したコードは、見つけたらすぐに排除
リファクタリングはソフトウェアを理解しやすくする(P.56)
- なんらかの変更を加えるときに、将来の開発者(もしかしたら自分かも…)はコードを読むことになるかも
- この時間を減らすことが重要
- リファクタリングして、コードの目的がより伝わりやすくする
- この時間を減らすことが重要
リファクタリングはバグを見つけ出す(P.57)
- コードが理解できると、バグを見つけやすくなる
- リファクタリングできれば、何をしているか分かり、理解が深めることができる
- プログラムの構造を明確にできる
- リファクタリングできれば、何をしているか分かり、理解が深めることができる
リファクタリングでより速くプログラミングができる(P.57)
※ ラピッドな開発 = RAD(Rabit Application Development)
- 優れた設計があれば、開発スピードを一定に維持できる
- 機能追加などの開発進んでコードが肥大化しても、開発速度が落ちることはない
- リファクタリングは、設計が劣化することを防ぐだけでなく、設計をより良くする
いつリファクタリングをすべきか(P.57)
3度目の法則(p.58)
- 同じようなコードを書くことが3度目だと感じたとき、リファクタリングを開始する
機能追加のときにリファクタリングを行う(P.58)
- 機能追加を行うときに、リファクタリングを開始することがある
- これから修正しようとしているコードを理解するため
- 簡単に機能追加できない設計があるため
- 「もし設計がこうなら、機能追加はずっと簡単なのに」と考えたらリファクタリングする → 将来の機能拡張を見越して
バグフィックスのときにリファクタリングを行う(P.58)
- バグフィックス時のリファクタリングは、コードを理解するため
コードレビューのときにリファクタリングを行う(P.59)
- 自分にとって分かりやすくても、チーム内では違うかも…
- よりよいアイディアでるかも
- レビュアーとレビューイが、簡単にリファクタリングできるか検討し、できそうであれば変更に着手する
管理者を説得するには(P.60)
- 管理者が技術に詳しいタイプなら:リファクタリングを紹介すれば理解してくれる
- 管理者が品質を重視するタイプなら:リファクタリングを行うことで、ソフトウェアの品質が向上する点を強調する
- 管理者が品質よりスケジュールを重視するタイプなら:管理者に無断でリファクタリングする
リファクタリングの問題点(P.62)
データベース(P.63)
- データベーススキーマと強く依存しているため、変更が難しい
- データベーススキーマの変更も、データ移行が必要なため、時間がかかる危険な作業。
- オブジェクト指向ではないデータベースを使っている場合
- オブジェクトモデルとデータベースモデルとの間にソフトウェアによる分離のための層を設ける(中間層)
インターフェースの変更(P.64)
- インターフェースは、変更すると何らかの影響を及ぼす可能性がある
- インターフェースを呼び出している箇所を検索、変更できない場合(公布済み(published)インターフェース)、呼び出し側を修正すればいい!という判断ができない
「published」とは、publicよりもさらに広く公開され、変更が困難なこと。具体的には、一般公開されているライブラリに含まれるpublicメソッドなど、書き手がそのメソッドの使用箇所を全て探しだして変更することが困難な状態になったメソッドのこと。この場合、インタフェースを変更するなら、古いインタフェースを残しつつ新しいインタフェースを提供する、という形式になる。
- Javaで古いAPI、インターフェースが非推奨といいながら、ずっと残っていることが良い例。
- 新しいメソッドには別の名前をつける
- 古いメソッドは、内部で新しいメソッドを呼ぶ
- 発生する例外を
RuntimeEception
のサブクラスの例外として変換して、コンパイルチェックされないようにする - 変換された
Exception
を投げることで、将来的にチェックされる例外になるという警告を与えることができる
リファクタリングしにくい設計(P.65)
- 間違った設計でもリファクタリングできるのか、という問に対する明確な答えはない
- 実際のリファクタリング作業をイメージ
- 古い設計から新しい設計へのリファクタリングすることに、どのくらい手間がかかるか考える
- 容易な場合:単純な方法で採用して、時間をかけない
- 困難な場合:時間をかけて設計を検討する
リファクタリングを避ける時(P.66)
- 変更するよりも最初から書き直したほうがよい場合
- 既存コードが動かないときは書き直し
- 期限(納期)が迫っている場合
- 期限が迫っているわけではないにも関わらず、リファクタリングを避けてはいけない
リファクタリングと設計(P.66)
- リファクタリングには、設計を補完する役割がある
- リファクタリングを行うことで、事前設計を改善することができる
リファクタリングとパフォーマンス(P.69)
- リファクタリングがパフォーマンスに影響を与えることがある
- パフォーマンス改善するための3つの方法
- 時間分割
- 設計を細かいコンポーネントに分けて、各コンポーネントがあらかじめ与えられた時間とメモリ使用量を超えてはいけない
- パフォーマンスを常に意識しておく
- すべてのプログラマが、開発全体にわたってパフォーマンスを高めるための様々なアプローチを実施し続ける
- パフォーマンスを高めると、だいたいコードは読みづらくなる → 開発効率の低下
- すべてのプログラマが、開発全体にわたってパフォーマンスを高めるための様々なアプローチを実施し続ける
- 90%の法則
- 全体の90%は読みやすく整理して、残り10%のパフォーマンスに影響を与える箇所をチューニングする
- 時間分割
第3章 コードの不吉な臭い
重複したコード(Duplicated Code)(P.76)
- 同じようなコードが2箇所以上で見られたら、1箇所にまとめることを考えるとよい
- もっとも単純なのは、同一クラス内の複数メソッドに同じ式があるもの
長すぎるメソッド(Long Method)(P.76)
- 長いメソッドほど理解しにくくなる
- 命名は適切に行う
- コメントで補足したくなったら、分かりやすいメソッド名で分割してしまう
- コードが長くても、意図が明確ならばOK
- メソッド名と実装との距離を埋めることが目的のため(メソッドを短くすることが目的ではない)
- パラメータや一時変数が多いものは、メソッド抽出を妨げる要因
- 「問い合せによる一時変数の置き換え(P.120)」
- 長いパラメータリストは…
- 「パラメータオブジェクトの導入(P.295)」
- 「オブジェクトそのものの受け渡し(P.288)」
- どうしても残ってしまう場合は、「メソッドオブジェクトによるメソッドの置き換え(P.135)」を試そう
- 条件分岐やループも抽出対象
- 「条件記述の分解(P.238)」
巨大なクラス(Large Class)(P.78)
インスタンス変数をもちすぎているかも
- 重複したコードが存在する可能性が高い
- 「クラスの抽出(P.149)」で、変数をひとまとめ
- 新しくできたコンポーネントがサブクラスになりそうな場合
- 「サブクラスの抽出(P.330)」
- クラスがインスタンス変数をすべて使っていない場合
- 「クラスの抽出(P.149)」
- 「サブクラスの抽出(P.330)」
コードが多すぎるクラスは
- 「クラスの抽出(P.149)」
- 「サブクラスの抽出(P.330)」
- 「インタフェースの抽出(P.341)」
長すぎるパラメータリスト(Long Parameter List)(P.78)
- 必要なデータは、オブジェクトに問い合わせて取得
- パラメータの数が多いと、ひとつひとつが何を意味しているか、理解しにくくなる
ユースケース
- 既知のオブジェクトに問い合わせることで、パラメータのデータを1つ取得できる場合
- 「メソッドによるパラメータの置き換え(P.292)」
- データをバラバラに渡さず、単一のオブジェクトで渡す場合
- 「オブジェクトそのものの受け渡し(P.288)」
- 論理的なオブジェクトとしてデータの集まりを表現できない場合
- 「パラメータオブジェクトの導入(P.295)」
変更の偏り(Divergent Change)(P.79)
- 変更しなければならないときは、特定の1箇所のみ特定して、変更したい
- 例:同一クラス内で、データベースが新しくなるたびに、いつも3つのメソッドを変更しなければならないし、金融商品を追加するたびに、毎回4つのメソッドを修正している
- クラスを2つに分けたほうがすっきりする!
- 「クラスの抽出(P.149)」で変更理由ごとにクラスをまとめていくことが重要
- 例:同一クラス内で、データベースが新しくなるたびに、いつも3つのメソッドを変更しなければならないし、金融商品を追加するたびに、毎回4つのメソッドを修正している
変更の分散(Shotgun Surgery)(P.80)
- 変更を行うたびに、あちこちのクラスが書き換わる場合は注意
- 重要な変更を実装し忘れる危険性がある
- 「メソッドの移動(P.142)」
- 「フィールドの移動(P.146)」
- 重要な変更を実装し忘れる危険性がある
※ 変更の偏り … 1つのクラスがさまざまな変更要求の影響を被る現象 ※ 変更の分散 … 1つの変更が複数のクラスの変更を引き起こす現象
特性の横恋慕(Feature Envy)(P.80)
- 自クラスより他クラスに興味をもっているメソッドは、古典的な誤り
- 大抵の場合は、メソッドの位置が悪いので「メソッドの移動(P.142)」
- メソッドの一部分だけ横恋慕している場合は「メソッドの抽出(P.110)」→「メソッドの移動(P.142)」
メソッド内でさまざまなクラスのデータが存在している場合は?
- あるクラスに大部分のデータを持たせる
- そのクラスにメソッドを移動
データの群れ(Data Clumps)(P.81)
- 数個のデータがグループとなって、クラスのフィールドやメソッドのシグネチャなど、さまざまな箇所に現れることがある
- オブジェクトとして1つにまとめるべき
ユースケース
- 属性の場合は?
- 「クラスの抽出(P.149)」
- メソッドのシグネチャの場合は?
- 「パラメータオブジェクトの導入(P.295)」
- 「オブジェクトそのもの受け渡し(P.288)」
基本データ型への執着(Primitive Obsession)(P.81)
レコード型 = 複数の基本データ型をまとめた構造体のようなデータ構造
- 個々のデータに対して「オブジェクトによるデータ値の置き換え(P.175)」を行うこと
ユースケース
- データが単純なタイプレコードを表しており、その値が全体の振る舞いに影響しない場合
- 「クラスによるタイプコードの置き換え(P.218)」
- タイプコードによる条件分岐がある場合
- 「サブクラスによるタイプコードの置き換え(P.223)」
- 「State/Strategy によるタイプコードの書き換え(P.227)」
- いくつかの属性がまとめられそうな場合
- 「クラスの抽出(P.149)」
- 基本データ型がパラメータリストにたくさん現れる場合
- 「パラメータオブジェクトの導入(P.295)」
- 配列のめんどくささから逃れたい場合
- 「オブジェクトによる配列の置き換え(P.186)」
※ タイプコードとは?
public static final int BLOODTYPE_A = 1;
public static final int BLOODTYPE_B = 2;
public static final int BLOODTYPE_AB = 3;
public static final int BLOODTYPE_O = 4;
スイッチ文(Switch Statement)(P.82)
- スイッチ文を見たら、ポリモーフィズムが使えないか検討する
- 「メソッドの抽出(P.110」→「メソッドの移動(P.142)」でポリモーフィズムが利用できるようにクラスにメソッドを定義する
- 継承構造ができていない場合
- 「サブクラスによるタイプコードの置き換え(P.223)」
- 「State/Strategy によるタイプコードの書き換え(P.227)」
- 継承構造ができている場合
- 「ポリモーフィズムによる条件記述の置き換え(P.255)」
例外
- 同一メソッド内でわずかなケースで分岐するだけ かつ 将来的に条件が追加されない場合
- 「明示的なメソッド群によるパラメータの置き換え(P.285)
- null 値を見て分割している場合
- 「ヌルオブジェクトの導入(P.260)」
パラレル継承(Parallel Inheritance Hierarchy)(P.83)
- 新たなサブクラスを定義するたびに、別の継承ツリーにもサブクラスを定義しなければならない状況
- 「メソッドの移動(P.142)」
- 「フィールドの移動(P.146)」
怠け者クラス(Lasy Class)(P.83)
- 十分な仕事をしないクラスは削除する
- 理解と保守する時間が無駄
ユースケース
- サブクラスが働いていない場合
- 「階層の平坦化(P.344)」
- ヘルパークラスが働いていない場合
- 「クラスのインスタンス化(P.154)」
疑わしき一般化(Speculative Generality)(P.83)
- いつか必要になる機能【いまは使われていない機能】は、削除したほうがマシ
- テストケースでのみ利用されている場合も注意
ユースケース
- 大して働いていない抽象クラスの場合
- 「階層の平坦化(P.154)」
- 使われていないパラメータをもつメソッドの場合
- 「パラメータの削除(P.277)」
- わかりにくい抽象的な名前のメソッドの場合
- 「メソッド名の変更(P.273)」
一時的属性(Temporary Field)
- 特定の状況でしか設定されないインスタンス変数が存在することがある
- 理解しづらい
- 「クラスの抽出(P.149)」を使って、変数の居場所を作る
- 変数の値が代入されないときは、かわりにヌルオブジェクト
- 「ヌルオブジェクトの導入(P.260)」
メッセージの連鎖(Message Chains)(P.84)
- クライアントがあるオブジェクトにメッセージ送り、受け取ったオブジェクトが別オブジェクトにメッセージを送り、受けとったオブジェクトがまた別オブジェクトにメッセージを送り。。。
- 「委譲の隠蔽(P157)」
仲介人(Middle Man)(P.85)
- メソッドの大半が別オブジェクトに委譲しているだけのクラスができてしまう場合がある
- 「仲介人の除去(P.160)」
ユースケース
- 仲介人メソッドがわずかな場合
- 「メソッドのインライン化(P.117)」で、呼び出し側にその部分を埋め込む
- なにか処理が加わっている場合
- 「継承による委譲の置き換え(P.355)」
「委譲の隠蔽(P.159)」⇔「仲介人(P.160)」
不適切な関係(Inappropriate Intimacy)(P.85)
- クラス同士が密な関係の場合は離す
- 「メソッドの移動(P.142)」
- 「フィールドの移動(P.146)」
- 「双方向関連の単方向への変更(P.200)」が適用できないかも検討する
ユースケース
- クラスが共通の興味を持っている場合
- 「クラスの抽出(P.149)」
- 「委譲の隠蔽(P.157)」
- サブクラス(子クラス)が親クラスのことを知りすぎている場合
- 「委譲による継承の置き換え(P.352)」
クラスのインタフェース不一致(Alternative Classes With Different Interfaces)(P.85)
- シグネチャのみが異なるメソッドは…
- 「メソッド名の変更(P.273)」
- この方法が使えない場合は…
- いまあるクラスでは処理が十分に行えない → 「メソッドの移動(P.142)」で、適切なクラスに配置
- 移動した結果、重複したコードが発生するようなら → 「スーパークラスの抽出(P.336)」
- いまあるクラスでは処理が十分に行えない → 「メソッドの移動(P.142)」で、適切なクラスに配置
未熟なクラスライブラリ(Incomplete Library Class)(P.86)
- 第三者によるライブラリは、メソッドの移動ができない
- 「外部メソッドの導入(P.162)」を行う
- 追加されるふるまいが多くなりそうであれば‥
- 「局所的な拡張の導入(P.164)」
データクラス(Data Class)(P.86)
- なるべく速くから「フィールドのカプセル化(P.206)」
ユースケース
- コレクションクラスが属性として使われていた場合…
- 「コレクションのカプセル化(P.208)」
- 値が変更されたくない場合…
- 「set メソッドの削除(P.300)」
存続拒否(Refused Request)(P.87)
- サブクラスが親クラスの属性や操作を継承する必要がない場合は、継承階層が間違っている
- サブクラスでスーパークラスを継承するが、インタフェースの機能は不要な場合は注意が必要
- 「委譲による継承の置き換え(P.352)」
コメント(Comments)(P.87)
- コメントが丁寧に書かれている場合、理解しにくいコードを隠すためだったというパターンがある
ユースケース
- メソッドの処理の一部を説明するためにコメントが必要な場合…
- 「メソッドの抽出(P.110)」
- それでもコメントがないとわかりにくい場合…
- 「メソッド名の変更(P.273)」
- システムが特定の状態である必要を明確に表現したい場合…
- 「表明の導入(P.267)」
第4章 テストの構築
- 堅実なテストは、リファクタリングするためには重要なもの
- よいテストをかくと、プログラミングは加速する
自己テストコードの意義(P.89)
- コードを書いている時間より、何が起こっているか理解する時間のほうが長い
- 「クラスにそれ自身のテストを行わせること」
- テストを完全に自動化して、その結果もテストにチェックさせること
- テスト実行に積極的になり、バグ検出に絶大な威力を発揮する→バグの発見にかかる時間が削減できる
- ちょっとの機能を加えたら、すぐにテストも追加する
- テストを完全に自動化して、その結果もテストにチェックさせること
- テストはプログラミングを始める前に手をつけるのがよい
JUnitテストフレームワーク(P.91)
TestSuite
には、単体のTestCase
や他のTestSuite
を含められる- 多様で大規模なテストを簡単に構築し、自動的に実行することができる
テストは頻繁に実行せよ
- 1日に最低一度はすべてのテストを実行せよ
- コンパイル時にはテストを局所化する
- リファクタリング時は、限られたテストだけ実行する
テストを書くときは、はじめは失敗するようにする
- 既存のコードならば、失敗するように変更するか、予想として正しくない値を設定するか工夫する
- 本当にテストが実行され、期待通りのテストをしているか確認するため
※TestSuiteとは?
- テストクラス内のテストメソッドは順番が保証されていない
- 実行順序を指定したい場合に
TestSuite
を使用する
- 実行順序を指定したい場合に
単体テストと機能テスト(P.96)
- 「単体テスト」と「機能テスト」は別物
単体テストとは?
- プログラマの生産性を向上するために行うもの
- 各テストクラスが1つのパッケージ内で動作するだけ
- 他の部分が正しく動作すること前提
機能テストとは?
- システム全体を可能な限りブラックボックスとして扱う
- GUIベースのシステムなら、GUIでテスト
- ファイルやDB更新するシステムなら、決められた入力で、どのようにデータが変更されるか調べるだけ
バグレポートを受け取ったら、そのバグを明らかにするための単体テストを書け
テストの追加(P.97)
クラスの責務を調べて、1つずつ不具合を起こしそうな条件のテストを追加する
- 「すべての公開メソッド(public)をテストする」とは、また別 → これはこれで重要
- フィールドを読み書きするだけのメソッドのテストは不要
- 単純な処理なので、バグがあるとは考えにくい
大事なことは、一番怪しいと思う部分をテストすること
- 一番効率がよい方法
- 実行されないテストより、実行される不完全なテストのほうがマシ
- 境界条件は失敗の恐れがあるため、重点的にテストする
- 失敗する(Exception)すると予想されるときは、例外が上がることをテストし忘れないこと
- テストですべてのバグが見つからないからといって、テストを書くことをやめないこと
- ほとんどのバグを見つけることはできる
第5章 リファクタリング・カタログに向けて
リファクタリングの形式(P.103)
- 本書のリファクタリングは、5つの要素からなる
- 名前
- 要約
- 動機
- 手順
- 例
参照の検索(P.105)
- リファクタリングを行うとき、メソッド、フィールド、クラスに対する参照をすべて見つけることが必要
- コンピュータでテキスト検索しよう
第6章 メソッドの構成
メソッドの抽出(P.110)
長すぎるメソッドや、コメントが無ければ目的が理解できないメソッドは、メソッドを分割
void printOwing(double amount) {
printBanner();
// 明細の表示
System.out.println(name);
System.out.println(amount);
}
void printOwing(double amount) {
printBanner();
printDetail();
}
void printDetail(double amount) {
System.out.println(name);
System.out.println(amount);
}
ローカル変数の再代入(P.114)
void printOwing(double amount) {
Enumeration e = _order.elements();
double outstanding = 0.0;
printBanner();
// 未払金の計算
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
printDetail(outstanding);
}
一時変数を抽出されるコードに移せばいい
void printOwing(double amount) {
printBanner();
double outstanding = getOutstanding();
printDetail(outstanding);
}
double getOutstanding() {
Enumeration e = _order.elements();
double outstanding = 0.0;
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
return outstanding;
}
メソッドのインライン化(Inline Method)(P.117)
- メソッドの中身が、命名した名前と同じくらい分かりやすい場合、メソッドを取り除く(メソッドがポリモーフィックでないことを確認してから…)
- サブクラスでオーバーライドしているメソッドをインライン化しないこと
int getRating() {
return (moreThenFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThenFiveLateDeliveries() {
return _numberOfLateDeliveries > 5;
}
int getRating() {
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}
一時変数のインライン化(Inline Temp)(P.119)
- 簡単な式によって、一度だけ代入される一時変数があり、他のリファクタリングの障害となっている場合、すべて式に置き換えてしまう
- 一時変数が
final
ではない場合、final
と宣言してコンパイルする- 一時変数への代入が本当に一度だけか確認 してから、本リファクタリングを行うと良い
- 一時変数が
double basePrice = another.basePrice();
return (basePrice > 1000);
return (another.basePrice() > 1000);
問い合わせによる一時変数の置き換え(Replace Temp With Query)(P.120)
- 式の結果を保持するために一時変数を使っている
final
宣言して、本当に代入が一度だけか確認 してから、本リファクタリングを行うとよい
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
if (getBasePrice() > 1000) {
return getBasePrice() * 0.95;
} else {
return getBasePrice() * 0.98;
}
// .................................
double getBasePrice() {
return _quantity * _itemPrice;
}
例
double getPrice() {
int basePrice = _quantity * _itemPrice;
double discountFactor;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
double getPrice() {
return basePrice() * discountFactor();
}
double basePrice() {
return _quantity * _itemPrice;
}
private discountFactor() {
if (basePrice() > 1000) return 0.95;
else return 0.98;
}
説明用変数の導入(Introduce Explaining Variding)(P.124)
- 複雑な式は、途中結果やその目的を説明する名前をつけた一時変数に代入する。
- 条件ロジックにおいて、各条件記述の意味を適切な名前の一時変数で説明できる場合は、特に有効
if ((platform.toUpperCase().indexOf("MAC") > -1) &&
(browser.toUpperCase().indexOf("IE") > -1) &&
wasIntialized() && resize > 0) {
// 略
}
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrower = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIEBrower && wasResized) {
// 略
}
- 「メソッドの抽出(P.110)」のほうが望ましい場合がある
- 他の部分でも同様のオブジェクトを欲してるとき
一時変数の分離(Split Temporary Variable)(P.128)
- 何回も代入する一時変数は、代入ごとに別の一時変数に分ける
- ループ変数は例外
- 新たな一時変数には
final
として宣言する
- 新たな一時変数には
- ループ変数は例外
double temp = 2 * (_height * _width);
System.out.println(temp);
temp = _height * _width;
System.out.println(temp);
final double perimeter = 2 * (_height * _width);
System.out.println(perimeter);
final double area = _height * _width;
System.out.println(area);
パラメータへの代入の除去(Remove Assignment To Parameters)(P.131)
- パラメータ(引数)への代入が行われている
- 一時変数を使うように変更する
int discount(int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;
int discount(int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;
- パラメータが参照渡しの場合は、呼び出し元のメソッドが呼び出し後にそのパラメータを再利用しているか調べる
- また、いくつの参照渡しパラメータに代入が行われ、呼び出し元のメソッドでその後使われていないか調べる
- 単一の値を戻り値とする
- 2つ以上あるときは、データの固まりをオブジェクトにできないか、別のメソッドを作れないか検討する
- また、いくつの参照渡しパラメータに代入が行われ、呼び出し元のメソッドでその後使われていないか調べる
メソッドオブジェクトによるメソッドの置き換え(Replace Method With Method Object)(P.135)
- ローカル変数を大量に持っているメソッドは、メソッド自身をオブジェクトとして、ローカル変数をそのオブジェクトのフィールドとする
class Account {
int gamma(int inputVal, int quantity, int yearToDate) {
int importantValue1 = (inputVal * quantity) + delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantValue1) > 100) {
importantValue2 -= 20;
}
int importantValue3 = importantValue2 * 7;
// 略
return importantValue3 - 2 * importantValue1;
}
int delta() {
return 1; // 例
}
}
public class Account {
int gamma(int inputVal, int quantity, int yearToDate) {
return new Gamma(this, inputVal, quantity, yearToDate).compute();
}
int delta() {
return 1; // 例
}
}
class Gamma {
private final Account _account;
private int inputVal;
private int quantity;
private int yearToDate;
private int importantValue1;
private int importantValue2;
private int importantValue3;
Gamma(Account source, int inputValArg, int quantityArg, int yearToDateArg) {
_account = source;
inputVal = inputValArg;
quantity = quantityArg;
yearToDate = yearToDateArg;
}
int compute() {
int importantValue1 = (inputVal * quantity) + _account.delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantValue1) > 100) {
importantValue2 -= 20;
}
int importantValue3 = importantValue2 * 7;
// 略
return importantValue3 - 2 * importantValue1;
}
}
アルゴリズムの取り替え(Substitute Algorithm)(P.139)
- アルゴリズムをよりわかりやすいものに置き換えたい
- メソッドの本体を新たなアルゴリズムで置き換える
- よりわかりやすい方法が見つかったら、置き換えるべき
String foundPerson(String[] people) {
for (int i = 0; i < people.length; i++) {
if (people[i].equals("Don")) {
return "Don";
}
if (people[i].equals("John")) {
return "John";
}
if (people[i].equals("Kent")) {
return "Kent";
}
}
return "";
}
String foundPerson(String[] people) {
List candidates = Arrays.asList(new String[] {"Don", "John", "Kent"});
for (int i = 0; i < people.length; i++) {
if (candidates.contains(people[i])) {
return people[i];
}
}
return "";
}
第7章 オブジェクト間での特性の移動
- オブジェクトの設計において、責務をどこに配置するかの判断が重要
メソッドの移動(Move Method)(P.142)
- クラスの振る舞いが多すぎる場合や、クラス間でのやり取りが多く、結合度が高すぎる場合は、メソッドを移動したほうがよい
- 元クラスのサブクラスやスーパークラスで、移動しようとしているメソッドを宣言していないか注意
- これは移動してはいけない
- 元のオブジェクトから移動先のオブジェクトを参照する方法を検討する
- 既存のフィールドやメソッドで手に入るかも…
- メソッドが簡単に作れるかも…
- 元オブジェクトに移動先のオブジェクトを保持する新しいフィールドを作成する必要があるかも…
- 元クラスのサブクラスやスーパークラスで、移動しようとしているメソッドを宣言していないか注意
フィールドの移動(Move Field)(P.146)
- フィールドを移動するときは、そのクラスのメソッドよりも別クラスのメソッドのほうがそのフィールドを多く使っているとわかった場合。
get
やset
メソッドも対象
- 「クラスの抽出(P.149)」を行うために、フィールドを先に移動してからメソッドを移動することもある
- フィールドが
public
ならば、「フィールドのカプセル化(P.206)」を適用する- フィールドを利用するメソッドが多いときは「自己カプセル化フィールド(P.171)」が有用かも…
- 元のオブジェクトから移動先のオブジェクトを参照する方法を決める
- 既存のフィールドやメソッドから見つかるかも…
- 参照するためのメソッドが簡単に作れるかも…
- できなければ、元のオブジェクトで移動先のオブジェクトを保持するためのフィールドを用意する必要かも
クラスの抽出(Extract Class)(P.149)
- 「クラスはきっちり抽象化されたものであり、少数の明確な責務を担うべきである」
- 2つのクラスでなされるべき作業を1つのクラスで行っている
- クラスを新たに作って、適切なフィールドとメソッドを元のクラスからそこに移動する
- 元クラスから新しいクラスへのリンクを張るとき、逆向きのリンクははらないこと
- 双方向が必要かもしれないが、実際に必要と分かるまでは単方向リンクで対応
クラスのインライン化(Inline Class)(P.154)
- 「クラスの抽出(P.149)」の逆
- あるクラスが役割を終えて、もはや存在しなくてよくなったとき使う
- あるクラスに何も残らなくなるまで、「メソッドの移動(P.142)」と「フィールドの移動(P.146)」を適用し続ける
委譲の隠蔽(Hide Delegate)(P.157)
- クライアントがあるオブジェクトの委譲クラスを呼び出している
- サーバにメソッドを作って委譲を隠す
- カプセル化とは?
- オブジェクトがもつシステムの他の部分についての知識を減らす必要があること
class Person {
Department _department;
publib Department getDepartment() {
return _department;
}
public void setDepartment(Department arg) {
_department = arg;
}
}
class Department {
private String _chargeCode;
private Person _manager;
public Department(Person manager) {
_manager = manager;
}
public Person getManager() {
return _manager;
}
}
クライアントが、ある人の上司を知りたい場合、
manager = john.getDepartment().getManager();
このときに、Department
クラスを隠蔽したい( getDepartment()
を省略したい)
Personクラスに、単純な委譲メソッドを用意する
public Person getManager() {
return _department.getManager();
}
そうすると、クライアントが上司を知りたい場合の呼び出しは、
manager = john.getManager();
となる
仲介人の除去(Remove Middle Man)(P.160)
- 「委譲の隠蔽(P.157)」と逆
- クラスがやっていることが単純な委譲だけになっている(単純な委譲メソッドしかない)
- クライアントに委譲オブジェクトを直接呼ばせる
外部メソッドの導入(Introduce Foreign Method)(P.162)
- 利用中のサーバクラスにメソッドを追加する必要があるが、そのクラスを変更できない
- クライアントクラスに、サーバクラスのインスタンスを第1引数にとるメソッドを作る
Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
- 本来は、サーバメソッドに追加すればよいが、サーバのメソッドが変更できず、クライアント側で書くしかない場合。
- クライアント側の他の場所で、複数回使用する可能性があるため、メソッドとして切り出しておく
Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
private static Date nextDay(Date arg) {
return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
局所的拡張の導入(Introduce Local Extension)(P.164)
- 利用中のサーバクラスにメソッドをいくつか追加する必要があるが、クラスを変更できない
- それらの追加されるメソッドを備えた新たなクラスを作る
- この拡張クラスは、元のクラスのサブプラスまたはラッパー
サブクラスを使う場合
class MfDateSub extends Date {
public MfDateSub(String dateString) {
super(dateString);
}
public MfDateSub(Date arg) {
super(arg.getTime());
}
Date nextDay() {
return new Date(getYear(), getMonth(), getDay() + 1);
}
}
class Client {
MfDateSub sub = new MfDateSub(PreviousEnd);
Date newStart = sub.nextDay();
}
ラッパーを使う場合
class MfDateWrap {
private Date _original;
public MfDateWrap(String dateString) {
_original = new Date(dateString);
}
public MfDateWrap(Date arg) {
_original = arg;
}
public int getYear() {
return _original.getYear();
}
public boolean equals(Object arg) {
if (this == arg) return true;
if (! (arg instanceof MfDateWrap)) return false;
MfDateWrap other = ((MfDateWrap) arg);
return (_original.equals(other._original));
}
}
class client {
Date nextDay(Date arg) {
return new Date(getYear(), getMonth(), getDate() + 1);
}
}
第8章 データの再編成
- アクセサが必要になったとき、「自己カプセル化フィールド(P.171)」
- 単純なデータ型を使っていたところを、あとになってオブジェクトにしたほうが有効であったと気がついたとき、「オブジェクトによるデータ値の置き換え(P.175)」
- オブジェクトが、プログラムの多くの場所で必要とされるインスタンスであるとわかったとき、「値から参照への変更(P.179)」
- データ構造として機能する配列があるとき、「オブジェクトによる配列の置き換え(P.186)」
- マジックナンバーが何をやっているかわかったとき、「シンボリック定数によるマジックナンバーの置き換え(P.204)」
- 新しい機能を提供するために双方向リンクが必要になったとき、「単方向関連への双方向への変更(P.197)」
- 双方向リンクが既に要らなくなったと気づいたとき、「双方向関連の単方向への変更(P.200)」
- GUIのクラスが、ビジネスロジックを処理しているのを見かけたとき、「観察されるデータの複製(P.189)」
- 公開データが裸で歩き回っているようなとき、「フィールドのカプセル化(P.208)」
- データがコレクションの場合は、「コレクションのカプセル化(P.208)」
- レコードがむき出しの場合は、「データクラスによるレコードの置き換え(P.217)」
- タイプコードが情報提供のためのみで、クラスの振る舞いを変えないとき、「クラスによるタイプコードの置き換え(P.218)」
- クラスの振る舞いを変える場合、「サブクラスによるタイプコードの置き換え(P.223)」
- 上記ができない場合は、「State/Strategy によるタイプコードの置き換え(P.227)」
自己カプセル化フィールド(Self Encapsulate Field)(P.171)
- フィールドを直接アクセスしているが、プログラムが汚くなり始めた
- フィールドに対する
get
メソッドとset
メソッドを作って、それだけを使ってアクセスするように変更する
- フィールドに対する
- やり方が決定するまでは、直接アクセスをしたほうがよい
private int _low, _high;
boolean includes(int arg) {
return arg >= _low && arg <= _high;
}
private int _low, _high;
boolean includes(int arg) {
return arg >= getLow() && arg <= getHigh();
}
public int getLow() {
return _low;
}
public int getHigh() {
return _high;
}
オブジェクトによるデータ値の置き換え(Replace Data Value with Object)(P.175)
- 追加データや振る舞いが必要なデータ項目がある
- そのデータ項目をオブジェクトに変える
- 「重複したコード」と「特性の横恋慕」の臭いがし始めたら、オブジェクトに変えよう
class Order {
private String _customer;
public Order(String customer) {
_customer = customer;
}
public String getCustomer() {
return _customer;
}
public void setCustomer(String arg) {
_customer = arg;
}
}
private static int numberOfOrdersFor(Collection orders, String customer) {
int result = 0;
Iterator iter = orders.iterator();
while (iter.hasNext()) {
Order each = (Order) iter.next();
if (each.getCustomer().equals(customer)) {
result++;
}
}
return result;
}
Orderクラスに定義されているCustomer情報を、Customerクラスとして切り出す そして、OrderクラスでCustomer情報を保持している箇所を、Customerクラスを使用するように変更する(あと命名も)
class Order {
private Customer _customer;
public Order(String customerName) {
_customer = new Customer(customerName);
}
public String getCustomerName() {
return _customer.getName();
}
public void setCustomer(String customerName) {
_customer = new Customer(customerName);
}
}
class Customer {
private final String _name;
public Customer(String name) {
_name = name;
}
public String getName() {
return _name;
}
}
private static int numberOfOrdersFor(Collection orders, String customer) {
int result = 0;
Iterator iter = orders.iterator();
while (iter.hasNext()) {
Order each = (Order) iter.next();
if (each.getCustomerName().equals(customer)) {
result++;
}
}
return result;
}
- Customer クラスにクレジットの信用限度や住所のような項目を加えることは、いまはできない
- Order オブジェクトは、それぞれ Customer オブジェクトを保持しているから
- 「値から参照への変更(P.179)」を適用する
- 同じ顧客に対するすべての注文で、同一の顧客オブジェクトを共有するようにする
値から参照への変更(Change Value to Reference)(P.179) // TODO
- 同じインスタンスが多数存在するクラスがある。これらを1つのオブジェクトにしたい。
- そのオブジェクトを参照オブジェクトに変える
参照オブジェクトとは?
顧客や勘定といったもので、実世界における1個のオブジェクトを表しており、それらが同じであるかどうか調べるためには、オブジェクト識別が用いられる
値オブジェクトとは?
日付やお金のような、それ自身のデータ値によって定義される
例
- 「Factory Methodによるコンストラクタの置き換え(P.304)」を適用する
class Customer {
private final String _name;
private Customer(String name) {
_name = name;
}
public static Customer create(String name) {
return new Customer(name);
}
}
class Order {
public Order(String customer) {
_customer = Customer.create(customer);
}
}
Customerメソッドを前もって生成しておくか、その都度生成するか決める。(今回は後者)
class Customer {
private static Map<String, Customer> _instances = new HashMap<String, Customer>();
private final String _name;
// アクセスポイントとなるレジストリオブジェクトを作成(前もって作っておくパターン)
static void loadCustomers() {
new Customer("A").store();
new Customer("B").store();
new Customer("C").store();
}
private void store() {
_instances.put(this.getName(), this);
}
public static Customer getNamed(String name) {
return (Customer) _instances.get(name);
}
public Customer(String name) {
this._name = name;
}
public String getName() {
return _name;
}
}
参照から値への変更(Change Reference to Value)(P.183)
- 小さくて、不変で、コントロールが煩わしい参照オブジェクトがある
- 値オブジェクトに変える
- 「値から参照への変更(P.179)」の逆
- 値オブジェクトの重要な性質は、不変なこと
- 候補のオブジェクトが不変であるか、または不変にできるかチェックする
- 不変でないならば、「setメソッドの削除(P.300)」
- 不変になりえないならば、諦める
オブジェクトによる配列の置き換え(Replace Array with Object)(P.186)
- 配列の各要素が、それぞれ異なる意味を持っている
- その配列を、要素ごとに対応したフィールドを持つオブジェクトに置き換える
String[] row = new String[3];
row[0] = "Liverpool";
row[1] = "15";
Perfomance row = new Performance();
row.setName("Liverpool");
row.setWins("15");
- 「配列の第1要素は氏名」「第2要素は勝利数」というような暗黙ルールがあったりする場合がある
- 新たなクラスを作成して、オブジェクトに変更する
- 情報をカプセル化し、「メソッドの移動(P.142)」を適用して、オブジェクトの振る舞いを加えることも可能
- 新たなクラスを作成して、オブジェクトに変更する
観察されるデータの複製(Duplicate Observed Data)(P.189) // TODO
- あるGUIコントロールのみ有効なドメインデータがあり、ドメインメソッドからもアクセスする必要がある
- そのデータをドメインオブジェクトにコピーして、それらを同期させるためのオブザーバを設ける
手順
- プレゼンテーションクラスをドメインクラスのObserver[GoF]にする
- ドメインクラスが全くなければそれを作る
- プレゼンテーションクラスからドメインクラスへのリンクがないときは、プレゼンテーションクラスのフィールドにドメインクラスを置く
- GUIクラス内の、ドメインデータに対して自己カプセル化フィールドを適用する
- コンパイルしてテストする
- イベントを処理するメソッドにsetのコールを追加し、Observerの構成要素を変数の直接アクセスをつかて現在値に更新する
- メソッドは、現在地に基づいて構成要素の値を更新するイベント処理メソッド中に置く
- もちろんこれはその値を現在値に変更するだけで、完全に不要なものであるが、setを使うことで、どんな振る舞いもそこで実行可能になる
- この変更を行うとき、その構成要素に対してはgetを使ってはならず、代わりに変数の直接アクセスを使う。
- あとでgetはドメインからその値を取得することになるが、setが実行されるまではそのドメインデータは変更されない
- テストコードを使って、イベント処理のメカニズムがトリガされることを確認する
- コンパイルしてテストする
- ドメインクラス中にデータとアクセサを定義する
- ドメインのsetが、Observerにおける通知メカニズムを起動することを確認する
- ドメインのデータ型は、プレゼンテーションの型と同じにする
- 後に行うリファクタリングで、そのデータ型を変える
- アクセサを変更して、ドメインフィールドに書きに行くようにする
- オブザーバのupdateメソッドを変更して、そのデータをドメインフィールドからGUIコントロールへコピーさせる
- コンパイルしてテストする
単方向関連への双方向への変更(Change Unidirectional Association to Bidirectional)
- 2つのクラスが互いにその特性を使う必要があるが、単方向へのリンクしかない
- 逆ポインタを加えて、両方のリンクを更新するように更新操作を変更する
手順
- 逆ポインタ用のフィールドを追加する
- どちらのクラスが、その関連に対するコントロールを持つかを決める。
- その関連のコントロールを持たない側にヘルパーメソッドをつくる。ヘルパーメソッドには、それが制限付きで使用されていることを的確に表す名前をつける
- 既存の更新操作が、すでにコントロール側にあるならば、それを修正して、逆ポインタを更新するようにする
- 既存の更新操作が、コントロールされる側にあるときは、コントロールする側にメソッドを作って、既存の更新操作からそれを呼ぶ。
双方向関連の単方向への変更(Change Bidirectional Association to Unidirectional)(P.200)
- 双方向関連があるが、一方のクラスはもはや他方の特性を必要としていない
- 不要になった関連に一報を削除する
- 双方向がたくさんあると、必要もないオブジェクトが残り続けてしまう(メモリの無駄)
手順
- 削除したいポインタを保持しているフィールドを参照するメソッドをすべて探す
- フィールドを直接読んでいるメソッドと、さらにそのメソッドを呼び出しているメソッドを探す
- ポインタを使わずに相手のオブジェクトを決定できるか検討する
- できるなら「アルゴリズムの取り替え(P.139)」
- できないなら、クライアントが使えるようにする
- メソッドの引数を、そのオブジェクトにすべて加えることを検討する
- クライアントが get メソッドを使いたい場合、「自己カプセル化(P.171)」→「アルゴリズムの取り替え(P.139)」をして、コンパイルしてテストする
- 使う必要がない場合、別の方法でフィールドのオブジェクトを取り出すように、クライアント側を修正して、コンパイルしてテストする
- よくやる方法は引数として渡すこと
- 使う必要がない場合、別の方法でフィールドのオブジェクトを取り出すように、クライアント側を修正して、コンパイルしてテストする
- フィールドを読んでいるクライアントがいないなら、フィールドの更新処理も、フィールド自身も削除する
- コンパイルしてテストする
シンボリック定数によるマジックナンバーの置き換え(Replace Magic Number with Symbolic Constant)(P.204)
- 特別な意味を持った数字のリテラルがある
- 定数を作り、それにふさわしい名前をつけて、そのリテラルを置き換える
double potentialEnergy(double mass, double height) {
return mass * 9.81 * height;
}
static final double GRAVITATIONAL_CONSTANT = 9.81;
double potentialEnergy(double mass, double height) {
return mass * GRAVITATIONAL_CONSTANT * height;
}
手順
- 定数を定義して、マジックナンバーの値をセットする
- マジックナンバーを使っているところをすべて検索する
- そのマジックナンバーを定数に変更してよければ、変更する
- すべてのマジックナンバーを変更したら、コンパイルしてテストする
- テスト方法として、定数の値を変更して、テスト結果が変わることを確認するとよい
コレクションのカプセル化(Encapsulate Collection)(P.208)
- メソッドがコレクションを返している
- 読み取り専用のビューを返して、追加と削除のメソッドを提供する
get
メソッドを呼んだら、コレクションオブジェクトがそのまま返していることがあるが、オブジェクト内部のデータ構造を見せることになる- コレクションの内容の操作を防止すべき
手順
- コレクションに対する追加と削除のメソッドを作成する
- フィールドをからのコレクションで初期化する
set
メソッドの呼び出し元を探す。set メソッドを変更して、追加・削除の操作を行うか、クライアントに追加・削除の操作を呼ばせるようにするset
メソッドは2つのケースで使われることが想定される- コレクションが空のケース
- 空ではないコレクションを置き換えるケース
- 「メソッド名の変更(P.273)」を使って、set メソッドの名前を変更したい
initialize
とかreplace
とか…
- テストする
- コレクションを変更する
get
メソッドを見つける。- 追加と削除のメソッドを使用するように変更する
- 終わったらテストする
- すべて変更したら、get メソッドは、コレクションの読み取り専用のビューを返すように変更する(Unmodifiable Set)
- テストする
get
メソッドのユーザを探して、コレクションを保持するオブジェクトにあるべきコードを探す- 「メソッドの抽出(P.110)」
- 「メソッドの移動(P.142)」
データクラスによるレコードの置き換え(Replace Record with Data Class)(P.217)
- 古いプログラミング環境のレコード構造とインターフェースをとる必要がある
- そのレコード用に、振る舞いを持たないデータオブジェクトを作る
手順
- レコードを表現するためのクラスを作る
- そのクラスに、各データ項目に対応する
private
フィールドを設け、get
メソッドとset
メソッドを用意する
クラスによるタイプコードの置き換え(Replace Type Code with Class)(P.218)
- 振る舞いに影響しない数字のタイプコードを持つクラスがある
- その数字を新しいクラスで置き換える
- いまなら
enum
+switch
で表現しているかも…?
手順
- タイプコードのためのクラスを新規作成する
- 必要なもの
- タイプコードに対応するフィールド
get
メソッド- 元のタイプコードを引数として受け取って、適切なインスタンスを返す
static
メソッド
- 必要なもの
- 新しいクラスを使用するように、元のクラスの実装を変更する
- テストする
- タイプコードを使う元のクラスのメソッドごとに、新しいクラスを使うメソッドを新たなに作成する
- 引数にタイプコードを使っているメソッドの場合、引数に新しいクラスのインスタンスをとる新しいメソッドが必要
- タイプコードを返すメソッドの場合、新たなクラスのインスタンスを返す新しいメソッドが必要
- 1つずつ元クラスのクライアント(ユーザ)を変更して、新しいインターフェースを使用するようにする
- クライアントを更新するたびにテストする
- タイプコードを使用する古いインターフェースを削除して、そのタイプコードの
static
宣言を削除する - テストする
class Person {
public static final int O = 0;
public static final int A = 1;
}
class BloodGroup {
public static final BloodGroup O = new BloodGroup(0);
public static final BloodGroup A = new BloodGroup(1);
public static final BloodGroup B = new BloodGroup(2);
public static final BloodGroup AB = new BloodGroup(3);
private final int _code;
private BloodGroup(int code) {
_code = code;
}
public int getCode() {
return _code;
}
}
class Person {
private int bloodO = BloodGroup.O.getCode();
private int bloodA = BloodGroup.A.getCode();
}
サブクラスによるタイプコードの置き換え(Replace Type Code with Subclasses)(P.223)
- クラスの振る舞いに影響を与える不変のタイプコードがある
- そのタイプコードをサブクラスに置き換える
- タイプコードを見て、
switch
とif-then-else
で分岐しているようなコード
- できない場合もある
- オブジェクトが生成されたあと、タイプコードの値が変わる場合
- 「State/Strategy によるタイプコードの置き換え(P.277)」を適用
- オブジェクトが生成されたあと、タイプコードの値が変わる場合
- 本リファクタリングを行う理由
- 「ポリモーフィズムによる条件記述の置き換え(P.255)」をしたい
- あるタイプコードをもつオブジェクトにだけ関係する特性が存在する場合
- 「メソッドの引き下げ(P.328)」をしたい
- 「フィールドの引き下げ(P.329)」をしたい
- メリットは、条件分岐が増えるときにサブクラスを追加するだけで済む
手順
- タイプコードを自己カプセル化する
- 「自己カプセル化フィールド(P.171)」
- タイプコードがコンストラクタに渡されているならば、「Factory Methodによるコンストラクタの置き換え(P.304)」
- タイプコードの値の名前をもつサブクラスを作成する。そのサブクラスにあるタイプコードの
get
メソッドをオーバーライドして、適切な値を返すようにする - タイプコードの値をそれぞれのサブクラスに置き換えたら、テストする
- サブクラスからタイプコードを削除する。
- タイプコードのアクセサを
abstract
として宣言する
- タイプコードのアクセサを
- テストする
State/Strategy によるタイプコードの置き換え(Replace Type Code with State/Strategy)(P.227)
- クラスの振る舞いに影響するサブクラスがあるが、サブクラス化できない
- 状態オブジェクトでタイプコードを置き換える
- 主に、サブクラス化が不都合なときに活用する
手順
- タイプコードを自己カプセル化する
- 新しいクラスを作って、タイプコードの目的にちなんだ名前をつける = 状態オブジェクト
- タイプコードごとに1つずつ、状態オブジェクトのサブクラスを追加する
- 1つずつ行うより、一度にすべてのサブクラスを追加する
- 状態オブジェクトの中に抽象の問い合わせメソッドを作り、タイプコードを返すようにする
- 状態オブジェクトのサブクラスごとに問い合わせメソッドをオーバーライドし、適切なタイプコードを返す
- 古いクラスの中に、新しい状態オブジェクトのためのフィールドを作る
- 元のクラスにあるタイプコードの問い合わせメソッドを、状態オブジェクトに委譲するように手を加える
- 元のクラスにあるタイプコードの
set
メソッドを、適切な状態オブジェクトのサブクラスのインスタンスを割り当てるように手を加える - テストする
フィールドによるサブクラスの置き換え(Replace Subclass with Fields)(P.232)
- 定数データを返すメソッドだけが異なるサブクラスがある
- そのメソッドをサブクラスのフォールドに変更して、サブクラスを削除する
手順
- サブクラスに「Factory Methodによるコンストラクタの置き換え(P.304)」を適用する
- サブクラスを参照しているコードがあれば、その参照をスーパークラスへの参照を置き換える
- スーパークラスのコンスタントメソッドごとに
final
フィールドを定義する - そのフィールドを初期化する
protected
指定のスーパークラスのコンストラクタを定義する - サブクラスのコンストラクタを追加または変更して、スーパークラスの新しいコンストラクタを呼ぶようにする
- テストする
- スーパークラスの中の各コンスタントメソッドでそのフィールドを返すように実装し、サブクラスからそのメソッドを削除する
- 削除するたびに、テストする
- サブクラスのメソッドがすべて削除できたら、「メソッドのインライン化(P.117)」
- スーパークラスのファクトリメソッド中にコンストラクタをインライン化する
- テストする
- サブクラスを削除する
- テストする
- サブクラスがなくなるまで、コンストラクタをインライン化し、サブクラスの除去を繰り返す
第9章 条件記述の単純化
- 単純化するための中心的なリファクタリング
- 「条件記述の分解(P.238)」
- 複数の条件判定があり、それらの結果がすべておなじになるとき
- 「条件記述の統合(P.240)」
- 条件記述のコード中の重複を取り除くとき
- 「重複した条件記述の断片の統合(P.243)」
- 特殊ケースの条件記述を明確化するとき
- 「ガード節による入れ子条件記述を置き換え(P.250)」
- 不細工な制御フラグを排除するとき
- 「制御フラグの削除(P.245))」
- オブジェクト指向には、switch 文はほとんど使わない
- 「ポリモーフィズムによる条件の置き換え(P.255)」で switch 文を消す
- null 値のチェック処理を排除
- 「ヌルオブジェクトの導入(P.260)」
条件記述の分解(Decompose Conditional)(P.238)
- 複雑な条件記述(if-then-else)がある
- その条件記述部と then 部および else 部から、メソッドを抽出する
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * _winterRate + _winterServiceCharge;
} else {
charge = quantity * _summerRate;
}
長いコードブロックでも、それを分解する。 そして、そのブロックにふさわしい名前をもったメソッドの呼び出しでコードを置き換える
if (notSummer(date)) {
charge = winterCharge(quantity);
} else {
charge = summerCharge(quantity);
}
private boolean notSummer(Date date) {
return date.before(SUMMER_START) || date.after(SUMMER_END);
}
private double summerCharge(int quantity) {
return quantity * _summerRate;
}
private double winterCharge(int quantity) {
return quantity * _winterRate + _winterServiceCharge;
}
手順
- 条件記述を抽出して、専用のメソッドを入れる。
- then 部と else 部を抽出して、専用のメソッドに入れる
- 抽出は別々に行って、それぞれが終わるごとにテストする
条件記述の統合(Consolidate Conditional Expression)(P.240)
- 同じ結果を持つ一連の条件判定がある
- それらを1つの条件記述にまとめて抽出する
double disabilityAmount() {
if (_seniority < 2) return; 0;
if (_monthDisabled > 12) return 0;
if (_isPartTime) return 0;
// 傷病給付金を計算する
}
- 一連の条件判定が異なっているのに、結果アクションが同じ場合は、and や or を使って統合して、同じ結果を持つ1つの条件判定にする
double disabilityAmount() {
if (_seniority < 2) || (_monthsDisabled > 12) || (_isPartTime)) return 0;
}
- さらに「メソッドの抽出(P.110)」を行うことで、この条件式で判定したいことを明示する
- else だったら、三項演算子をつかって、1つの return 文にすることもできる
double disabilityAmount() {
if (isNotEligibleForDisability()) return 0;
// 傷病給付金を計算する
}
boolean isNotEligibleForDisability() {
return (_seniority < 2) || (_monthsDisabled > 12) || (_isPartTime));
}
手順
- その条件記述が副作用を持っていないことを確かめる
- 副作用があるならば、このリファクタリング対象外
- 条件記述の文字列を、理論演算子を使って単一の条件記述に置き換える
- テストする
- その条件記述に「メソッドの抽出(P.110)」を適用することを検討する
重複した条件記述の断片の統合(Consolidate Duplicate Conditional Fragments)(P.243)
- 条件式のすべての分岐に同じコードの断片がある
- それを式の外側に移動する
if (ifSpecialDeal()) {
total = price + 0.95;
send();
} else {
total = price * 0.98;
send();
}
if (ifSpecialDeal()) {
total = price + 0.95;
} else {
total = price * 0.98;
}
send();
- 例外記述にも適用可能
- try ブロック内の例外の原因となるステートメントの後、およびすべての catch ブロック中にコードの重複があるときは、それをfinally ブロックの中に移動する
手順
- 条件判定の結果にかかわらず、共通に実行されるコードを識別する
- その共通コードが先頭にあるならば、条件記述の前に出す
- その共通コードが末尾にあるならば、条件記述の後に出す
- 中間になるときは、その前後で何らかの変更が行われているかを調べる
- 変更が行われているときは、共通コードを前または後の端に移動する
- コードが末尾か先頭にある場合と同じ方法で移動できる
- 複数のステートメントがあるときは、そのコードを抽出してメソッドにするとよい
制御フラグの制御
- 一連の論理型の式に対して制御フラグとして機能する変数が1つある
- 代わりに break またh return を使う
public static boolean find(int[] data, int target){
boolean flag = false;
for (int i = 0; i < data.length &&!flag; i++){
if(data[i] == target){
flag = true;
}
}
return flag;
}
public static void main(String[] args) {
int[] data = {1,9,0,2,8,5,6,3,4,7};
if (FindInt.find(data, 5)){
System.out.println("Found!");
} else {
System.out.println("Not Found...");
}
}
public static boolean find(int[] data, int target){
for (int i = 0; i < data.length; i++){
if(data[i] == target){
return true;
}
}
return false;
}
public static void main(String[] args) {
int[] data = {1,9,0,2,8,5,6,3,4,7};
if (FindInt.find(data, 5)){
System.out.println("Found!");
}else{
System.out.println("Not Found...");
}
}
ガード節による入れ子条件記述の置き換え
- メソッド内に正常ルートが不明確な条件つき振る舞いがある
- 特殊ケースすべてに対してガード節を使う
- 条件記述には2つの形式
- 条件判定の両方が正常処理で、どちらのルートを撮るか判定するもの
- 条件判定の一方が正常処理で、一方があまり起こらない条件であるもの
- 両方が正常処理ならば、 if と else をもった条件記述を使うべき
- 扱う条件があまり起こらない場合、チェックしてその結果が真のときにはリターンする
- if-then-else 構造を使うとき、if 部にも else 部にも同じウエイトを置くように
- 逆に、ガード節は「めったに起きないが、起きたときには、何かしらのことをやって出ていく」ことを伝える
死亡したり、離職したり、退職した従業員に対して、特別なルールを持つ給与計算システムを想定
double getPayAmount() {
double result;
if (_isDead) result = deadAmount();
else {
if (_isSeparated) result = separatedAmount();
else {
if (_isRetired) result = retiredAmount();
else result = normalPayAmount();
}
}
return result;
}
double getPayAmount() {
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return mormalPayAmount();
}
手順
- 1つひとつ、ガード節に条件判定を入れる
- ガード節は return するか例外を投げるかどちらかである
- 条件判定をガード節で置き換えるたびに、コンパイルしてテストする
- ガード条件がすべて同じ結果となるとき、「条件記述の統合(P.240)」を適用する
ポリモーフィズムによる条件記述の置き換え(Replace Conditional with Polymorphism)(P.255)
- オブジェクトのタイプによって異なる振る舞いを選択する条件記述がある
- 条件記述の各アクション部をサブクラスでオーバーライドするメソッドに移動する。(元のメソッドは abstract にする)
- ポリモーフィズムは、オブジェクトの振る舞いがその型によって変わるとき、明示的な条件記述を書かなくてもいいようにすること
- 新しいタイプを追加したいとき、すべての条件記述を見つけて変更しなければならない
- サブクラスならば、新たなサブクラスを作って、適切なメソッドを追加するだけで済む
- 新しいタイプを追加したいとき、すべての条件記述を見つけて変更しなければならない
手順
- 継承構造を作るにあたって、2つの選択肢がある
- 最も簡単な方法は、「サブクラスによるタイプコードの置き換え(P.223)」
- オブジェクトが作られたあとでタイプコードを変更する場合は、「State/Strategy によるタイプコードの置き換え(P.227)」
- その条件文が大きなメソッドの一部ならば、その条件文を分離して「メソッドの抽出(P.110)」
- 条件文を継承構造の最上位におけるように、必要に応じて「メソッドの移動(P.142)」
- サブクラスの1つを取り上げて、その条件文メソッドをオーバーライドするようなサブクラスのメソッドを作る
- その条件文のアクション部をサブクラスのメソッドにコピーして、適合するように修正する
- これを行うために、スーパークラスの private メンバのいくつかを protected にする必要があるかも…
- テストする
- コピーした条件分のアクション部を削除する
- テストする
- 条件文のアクション部1つひとつにおいて、すべての処理がサブクラスのメソッドになるまで繰り返す
- スーパークラスのメソッドを abstract とする
class Employee {
int payAmount() {
switch (getType()) {
case EmployeeType.ENGINEER:
return _monthlySalary;
case EmployeeType.SALESMAN:
retrun _monthlySalary + _commision;
case EmployeeType.MANAGER:
return _monthlySalary + _bonus;
default:
throw new RuntimeException("不正な社員");
}
}
}
class Engineer extends EmployeeType {
int payAmount(Employee emp) {
return emp.getMonthlySalary();
}
}
class Salesman extends EmployeeType {
int payAmount(Employee emp) {
return emp.getMonthlySalary() + emp.getCommission();
}
}
class Manager extends EmployeeType {
int payAmount(Employee emp) {
return emp.getMonthlySalary() + emp.getBonus();
}
}
class EmployeeType {
abstract int payAmount(Employee emp);
}
ヌルオブジェクトの導入
- null 値チェックが繰り返し現れる
- その null 値をヌルオブジェクトで置き換える
- メリット:コードがシンプルになり、どこをポリモーフィックにすべきか分かりやすくなる
手順
- 元のクラスに null を表すサブクラスを作り、元のクラスとサブクラス両方に isNull メソッドを実装する。元のクラスでは false を返し、サブクラスでは true を返すようにする。
- isNull メソッドに対して、明示的に Nullable インタフェースを作成するとよいかも…
- これとは別に、null かどうか見るためのチェック用のインタフェースを使う方法もある
- コンパイルする
- 元のクラスのオブジェクトのインスタンスを要求した時に、 null 値を返す箇所を、ヌルオブジェクトを返すように置き換える
- 元のクラスの型の変数と null 値を比較している箇所を、 isNull メソッドの呼び出しで置き換える
- コンパイルしてテストする
- null かそうでないかによって異なる処理をする部分を見つけ、 null である場合の処理を、ヌルクラスの中にオーバーライドする形で定義する
- オーバーライドされた処理に対応する条件分岐を削除する
- null である場合はヌルオブジェクトを返すようにしている
- さらに、それ特有の処理をメソッドのオーバーライドで実装しているので、条件分岐が不要になる
- コンパイルしてテストする
すごい分かりやすかったサンプルコード https://toburau.hatenablog.jp/entry/20070906/1189096553
表明の導入(Introduce Assertion)(P.267)
- コードのある部分が、そのプログラムの状態について何らかの前提事項を持っている
- その前提事項を、表明を使って明示する
- 表明は常に真になるとみなされる条件記述
- 将来の変更を防ぐことができるかも…
- 表明は、本番コードでは取り除かれるのが普通
- コミュニケーションとデバッグのツールとして働く
- コミュニケーション:コードの読み手が理解しやすく
- デバッグ:バグをその原因の近いところで発見しやすくなる
- 表明の使いすぎに注意
double getExpenseLimit() {
// 支払上限か優先プロジェクトか、どちらかを持つこと
return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit : _primaryProject.getMemberExpenseLimit();
}
double getExpenseLimit() {
Assert.isTrue(_expenseLimit != NULL_EXPENSE || _primaryProject != null);
return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit : _primaryProject.getMemberExpenseLimit();
}
手順
- 条件が真になることが前提事項としてわかっているときは、それを明示する表明を加える
- 表明の振る舞いのために使える Assert クラスを用意すること
第10章 メソッドの呼び出しの単純化
- そのプログラムがなにをしているか正確に理解できているならば
- 「メソッド名の変更(P.273)」
- パラメータに関するリファクタリング
- 「パラメータの追加(P.275)」
- 「パラメータの削除(P.277)」
- 1つのオブジェクトから複数の値を取り出して渡しているならば
- 「オブジェクトそのものの受け渡し(P.288)」
- オブジェクトが存在しない場合
- 「パラメータオブジェクトの導入(P.295)」
- メソッドがアクセスしているオブジェクトからデータを取得できる場合
- 「メソッドによるパラメータの置き換え(P.292)」
- 条件に応じた振る舞いを決定するためにパラメータが使われている場合
- 「明示的なメソッド群によるパラメータの置き換え(P.285)」
- 似たようなメソッドが複数ある場合
- 「メソッドのパラメタライズ(P.283)」
- 長いパラメータは、不変オブジェクトに置き換え可能
- 状態を更新するメソッドと、状態を問い合わせるメソッドを明確に分離する
- メソッドも可能な限り隠蔽する
- 「メソッドの隠蔽(P.303)」
- 「 set メソッドの削除(P.300)」
- コンストラクタは厄介 → 生成するオブジェクトクラスを事前に知っておく必要があるため。
- 「Factory Method によるコンストラクタの置き換え(P.304)」
- キャスト問題(Java固有の問題?)
- 「ダウンキャストのカプセル化(P.308)」
- 例外処理にエラーコードが使用されている場合
- 「例外によるエラーコードの置き換え(P.310)」
- 例外の使用が不適切な場合(どんなとき?)
- 「条件判定による例外の置き換え(P.315)」
メソッド名の変更(Rename Method)(P.273)
- メソッドの名前がその目的を正しく表現できていない
- メソッドの名前を変更する
個人の電話番号を取得するためのメソッド
public String getTelephoneNumber() {
return ("(" + _officeAreaCode + ")" + _officeNumber);
}
```java:after
委譲メソッドを作成する
```java:afterその1
public String getTelephoneNumber() {
return getOfficeTelephoneNumber();
}
public String getOfficeTelephoneNumber() {
return ("(" + _officeAreaCode + ")" + _officeNumber);
}
古いメソッドの呼び出しを見つけて、新しいメソッド名に置き換える すべて置き換えたら、古いメソッドを削除する(いまはIDEで一括置換できるので、こんなに丁寧に作業する必要がないかも)
手順
- 同じシグネチャを持つメソッドが、スーパークラスあるいはサブクラスで実装されているかどうか調べる
- 実装されている場合は、その実装ごとに以下のステップを実行する
- 新しい名前をつけた新しいメソッドを定義する
- 古いメソッドのコードを新しいメソッドにコピーして、それに合わせるための修正を行う
- コンパイルする
- 古いメソッドの内容を変更して、新しいメソッドを呼び出すようにする
- 呼び出し側が少ない場合は、このステップを飛ばして良い
- テストする
- 古いメソッドへの呼び出しをすべて探して、新しい名前に変更する
- 変更のたびにコンパイルしてテストする
- 古いメソッドを削除する
- テストする
パラメータの追加(Add Parameter)(P.275)
- あるメソッドが、呼び出し元からより多くの情報を必要としている
- その情報を渡すために引数を追加する
- 長いパラメータリストは危険、覚えるのも大変
- 新しいパラメータを追加するときは、「パラメータオブジェクトの導入(P.295)」についても検討すべき
手順
「メソッド名の変更(P.273)」と似ているので省略。
- もし古いメソッドがインターフェースの一部となっていて削除できない場合は、 de[recated とマークする
パラメータの削除(Remove Parameter)(P.277)
- あるパラメータが、もはやメソッド本体から使われていない
- パラメータを削除する
- ポリモーフィックなメソッドに関しては慎重に対処する必要がある
- メソッドの別の実装では、パラメータが使用されているかも…
- パラメータを取得するために余計な処理をしていたり、クラス階層があらかじめわかっていて、null を指定しても問題なければ、パラメータをもたない特別なメソッドを追加する
- 呼び出し側がどのクラスがどのメソッドを持っているか知らずに済んでいるなら、何もしない
手順
「メソッド名の変更(P.273)」と「パラメータの追加(P.275)」と似ているので省略。 パラメータの追加や削除を同時に行うこともできる(それってパラメータの変更では…?)
問い合せと更新の分離(Separate Query from Modifier)(P.279)
- 1つのメソッドが値を返すと同時にオブジェクトの状態を変更している
- 問い合せ用と更新用の2つのメソッドをそれぞれ作成する
- 値を返すと同時に、副作用を伴うようなメソッドを見つけた場合、分離する
- 責務を明確にする
手順
- 元のメソッドと同じ値を返す問い合せメソッドを作成する
- 元のメソッドを修正して、(新たに作成した)問い合せメソッドの戻り値を返すようにする
- 元のメソッドで戻り値を返す処理部分を、たとえば
return newQuery()
のように、新しい問い合せメソッドに置き換える - そのメソッドが一時変数を使用しており、その変数への代入が戻り値を取得している1箇所だけなら、その一時変数を削除できる
- 元のメソッドで戻り値を返す処理部分を、たとえば
- コンパイルしてテストする
- すべての呼び出しに対して、元のメソッドに対する呼び出しを問い合せメソッドの呼び出しで置き換える。問い合せメソッドの呼び出し行の前に、元のメソッドに対する呼び出しを追加する、
- 変更のたびにテストする
- 元のメソッドの戻り値の型を void にして、 return 文を削除する
String foundMiscreant(String[] people) {
for (int i = 0; i < people.length; i++) {
if (people[i].equals("Don")) {
sendAlert();
return "Don";
}
if (people[i].equals("John")) {
sendAlert();
return "John";
}
}
return "";
}
void checkSecurity(String[] people) {
String found = foundMiscreant(people);
someLaterCode(found);
}
メソッドのパラメタライズ(Parameterize Method)(P.283)
- 複数のメソッドがよく似た振る舞いをしているが、それをメソッド内部にもつ異なる値に基づいている
- その異なる値をパラメータとして受け取るメソッドを1つ作成する
- パラメータの値によって異なる振る舞いをするメソッドを1つ用意すること
- メソッド全体には適用できないが、メソッドの一部に対して可能かもしれない
Dollars baseCharge() {
double result = Math.min(lastUsage(), 100) * 0.03;
if (lastUsage() > 100) {
result += (Math.min(lastUsage(), 200) - 100) * 0.05;
}
if (lastUsage() > 200) {
result += (lastUsage() - 200) * 0.07;
}
return new Dollars(result);
}
Dollars baseCharge() {
double result = usageInRange(0, 100) * 0.03;
result += usageInRange(100, 200) * 0,05;
result += usageInRange(200, Integer.MAX_VALUE) * 0.07;
return new Dollars(result);
}
int usageInRange(int start, int end) {
if (lastUsage() > start) {
return Math.min(lastUsage(), end) - start;
} else {
return 0;
}
}
手順
- 類似したメソッドを置き換えるパラメタライズされたメソッドを作成する
- コンパイルする
- 古いメソッドの呼び出しを新しいメソッドに置き換える
- テストする
- すべてのメソッドに対してこれを繰り返し、そのたびにテストする
明示的なメソッド群によるパラメータの置き換え(Replace Parameter with Explicit Methods)(P.285)
- パラメータの値によって異なるコードが実行されるメソッドがある
- パラメータの値に対応する別々のメソッドを作成する
- 「メソッドのパラメタライズ(P.283)」の裏返し
void setValue(String name, int value) {
if (name.equals("height")) {
_height = value;
return;
}
if (name.equals("width")) {
_width = value;
return;
}
Assert.shouldNeverReachHere();
}
void setHeight(int arg) {
_height = arg;
}
void setWidth(int arg) {
_width = arg;
}
手順
- パラメータの各々の値に対応する明示的なメソッドを作成する
- (古いメソッドの)条件記述のアクション部で、適切な新しいメソッドを呼び出すようにする
- アクション部の変更を行うごとに、コンパイルしてテストする
- 条件付きメソッド呼び出しを、適切な新しいメソッド呼び出しで置き換える
- コンパイルしてテストする
- 呼び出し側の変更がすべて終わったら、条件つきメソッドを削除する
オブジェクトそのものの受け渡し(Preserve Whole Object)(P.288)
- あるオブジェクトから複数の値を取得し、それらの値をメソッド呼び出しのパラメータを渡している
- 代わりにオブジェクトそのものを渡す
class Room {
boolean withinPlan(HeatingPlan plan) {
return plan.withinRange(daysTempRange());
}
}
class HeatingPlan {
private TempTange _range;
boolean withinRange(TempRange roomRange) {
return (roomRange.getLow() <= _range.getLow() && roomRange.getHigh() <= _range.getHigh());
}
メソッドによるパラメータの置き換え(Replace Parameter with Method)(P.292)
- あるオブジェクトがメソッドを呼び出し、その戻り値を別のメソッドのパラメータとして渡している。
- そのメソッドは受信側でも呼び出すことができる
- パラメータを削除し、受信側にそのメソッドを呼び出させる
手順
- 必要に応じて、パラメータに対する計算処理をメソッドとして抽出する
- メソッド本体におけるパラメータへの参照を、メソッドの呼び出しに置き換える
- 各々の置き換えのたびにテストする
- そのパラメータに対して「パラメータの削除(P.277)」を行う
パラメータオブジェクトの導入(Introduce Parameter Object)
- 本来まとめて扱うべきひとまとまりのパラメータがある
- それらをオブジェクトに置き換える
手順
- 置き換えようとしているパラメータ群を表現する新しいクラスを作成する。そのクラスは変更不可とする
- 新しいクラスを作って「パラメータの追加(P.275)」を行う。
- 呼び出し元はすべて
null
を設定する- 呼び出し側が複数ある場合は、古いシグネチャをそのまま残しておき、その中で新しいメソッドを呼び出すようにする
- 最初に古いメソッドに本リファクタリングを実行
- 呼び出し側の修正を順次行い、それが終わったら古いメソッドを削除する
- 呼び出し元はすべて
set メソッドの削除(Remove Setting Method)(P.300)
- フィールドの値が生成時に設定され、決して変更されない
- そのフィールドに対するすべての
set
メソッドを削除
- そのフィールドに対するすべての
- 変更してほしくないフィールドなのに、
set
メソッドなんて用意するな!
手順
set
メソッドがコンストラクタ内部、あるいはコンストラクタ内で呼び出されるメソッドからしか呼び出されていないことを確認する- 変数を直接アクセスするようにコンストラクタを変更する
- サブクラスでスーパークラスの private フィールドに値を設定している場合、リファクタリングできない。
- 上のような場合、この値を設定する protected 指定のメソッド(理想的にはコンストラクタ)をスーパークラスに用意するべき。
- スーパークラスに用意するメソッドには、
set
メソッドと混乱するような命名をしない
- スーパークラスに用意するメソッドには、
- テストする
set
メソッドを削除する- そのフィールドが
final
指定されておらず、かつ可能な場合には、final
にする - テストする
メソッドの隠蔽
- メソッドが自分の定義されているクラス以外から全く使用されていない
- そのメソッドを非公開にする
- ツールで自動チェックもできるが、定期的にチェックすべき
- インターフェースを充実させて、多くの振る舞いを提供したとき、get/set メソッドを隠蔽する場合が一般的
- 多くの振る舞いがそのクラスに追加されて、get/set メソッドのほとんどを公開する必要がなくなる
- get/set メソッドを private にして、「変数の直接アクセス」を使うようにすれば、メソッドを全削除できる
- 多くの振る舞いがそのクラスに追加されて、get/set メソッドのほとんどを公開する必要がなくなる
手順
- メソッドをさらに隠蔽できないか定期的にチェックする
- 特に set メソッドに注目
- メソッドをできる限り隠蔽する
- まとめて隠蔽したらコンパイルする
- コンパイラが自動的にチェックしてくれるので、変更のたびにコンパイルする必要はない
Factory Method によるコンストラクタの置き換え(Replace Constructor with Factory Method)(P.304)
- オブジェクトを生成する際に、単純な生成以上のことをしたい
- ファクトリメソッドを使って、コンストラクタを置き換える
- 最もよくあるパターンは、サブクラス化することによってタイプコードを置き換えたいとき
- ファクトリメソッドは、コンストラクタにはできないような値を設定することができる
- 「値から参照への変更(P.179)」の適用に不可欠な仕組み
手順
- ファクトリメソッドを作成する
- その本体で、現在のコンストラクタを呼び出すように実装する
- コンストラクタへのすべての呼び出しを、ファクトリメソッドへの呼び出しで置き換える
- 変更するたびにテストする
- コンストラクタを private と定義する
- コンパイルする
ダウンキャストのカプセル化(Encapsulate Downcast)(P.308)
- メソッドが返すオブジェクトを、呼び出し側でダウンキャストする必要がある
- ダウンキャスト処理をメソッド内に移動する
- ダウンキャストは、極力少なくするべき
- クライアントにダウンキャストを強制せず、特化した戻り値を提供するように
Object lastReading() {
return readings.lastElement();
}
Reading lastReading() {
return (Reading) readings.lastElement();
}
手順
- メソッド呼び出しの戻り値をダウンキャストしなければならない状況を探す
- これはコレクションやイテレータを返すメソッドでよく見受けられる
- ダウンキャスト処理をメソッド内に移動する
- コレクションを返すメソッドについては、「コレクションのカプセル化(P.208)」を適用する
例外によるエラーコードの置き換え(Replace Error Code with Exception)(P.310)
- エラーを示す特別なコードをメソッドがリターンしている
- 代わりに例外を発生させる
- プログラムがエラーを特定する部分と、解決する部分が常に同じとは限らないから
- 呼び出し元でもエラーの通知が連鎖するかもしれない
- Java の場合は、正常処理とエラー処理を分離できるから例外を使用すべし
int withdraw(int amount) {
if (amount > _balance) {
return -1;
} else {
_barance -= amount;
return 0;
}
}
void withdraw(int amount) throws BalanceException {
if (amount > _balance) throw new BalanceException();
_balance -= amount;
}
手順
- 例外がチェックされるべきか、されないでよいか決める
- 呼び出し側に条件判定をする責任がある場合、チェックされない例外とする
- チェックされるべき場合は、新しい例外を定義するか、既存の例外を使用する
- すべての呼び出し元を見つけて、例外を使用するように修正する
- 新しい例外の使用を反映するように、メソッドのシグネチャを変更する
手順(呼び出し元がたくさんある場合)
- 例外がチェックされるべきか、されないでよいか決める
- 例外を使用する新しいメソッドを作成する
- 古いメソッドを修正して、新しいメソッドを呼び出すようにする
- テストする
- 古いメソッドの呼び出し元を修正して、新しいメソッドを呼び出すようにする
- 古いメソッドを削除する
条件判定による例外の置き換え(Replace Exception with Test)(P.315)
- 例外を発生させているが、本来は呼び出し側が先にチェックすべきである
- 最初に条件判定をするように呼び出し側を修正する
- 条件判定の代わりとして例外処理は使うべきではない
- 条件判定をさせることが妥当であれば、条件判定用のメソッドを提供するべき
double getValueForPeriod(int periodNumber) {
try {
return _value[periodNumber];
} catch (ArrayIndexOutOfBoundsException e) {
return 0;
}
}
double getValueForPeriod(int periodNumber) {
if (periodNumber >= _values.length) {
return 0;
}
return _values[periodNumber];
}
手順
- 条件判定を前に移動し、 catch 節のコードを if 文の適切な位置にコピーする
- catch 節の表明を追加し、 catch 節が実行されたかどうかがわかるようになる
- テストする
- その他の catch 節がなければ、 catch 節と try 節を削除する
- テストする
第11章 継承の取り扱い
- 機能を階層の上位に昇格させるもの
- 「フィールドの引き上げ(P.320)」
- 「メソッドの引き上げ(P.322)」
- 機能を下位に降格させるもの
- 「メソッドの引き下げ(P.328)」
- フィールドの引き下げ(P.329)」
- コンストラクタを引き上げるのは少し厄介
- 「コンストラクタ本体の引き上げ(P.325)」
- コンストラクタ引き下げよりは…
- 「Factory Method によるコンストラクタの置き換え(P.304)」
- 基本構造はよく似ているが、詳細が異なるメソッドがある場合
- 「Template Method の形成(P.345)」
- 階層間でのメソッドの移動に加えて、新しいクラスを定義して階層を変更したい
- 「サブクラスの抽出(P.330)」
- 「スーパークラスの抽出(P.336)」
- 「インターフェースの抽出(P.341)」
- 型付けされてあシステムにおいて、少数のまとまった機能を際立たせたい場合
- 不要なクラスを見つけた場合
- 「階層の平坦化(P.344)」
- 継承の代わりに委譲を使いたい
- 「委譲による継承の置き換え(P.352)」
- 委譲を継承にしたい
- 「継承による委譲の置き換え(P.355)」
フィールドの置き換え(Pull Up Field)(P.320)
- 2つのサブクラスが同じフィールドを持っている
- そのフィールドをスーパークラスに移動する
- 重複した機能をもつことが多い
- フィールドが特に多い
- しかも同じような名前
手順
- 候補となるフィールドの使用部分をすべて詳しく調べて、同じように使われていることを確認する
- フィールドが同じ名前でなければ、スーパークラスのフィールドとして使いたい名前に変更する
- テストする
- スーパークラスに新しいフィールドを作成する
- フィールドが private の場合は、サブクラスから参照できるように、スーパークラスのフィールドを protected にする必要あり
- サブクラスのフィールドを削除する
- テストする
- 対象のフィールドに対して「自己カプセル化フィールド(P.171)」の適用を検討する
コンストラクタ本体の引き上げ(Pull Up Constructor Body)(P.325)
- 複数のサブクラスに内容がほとんど同一のコンストラクタがある
- スーパークラスのコンストラクタを作成して、サブクラスから呼び出す
class Employee {
protected String __name;
protected String _id;
}
class Manager extends Employee {
private int _grade;
publlic Manager(String name, String id, int grade) {
_name = name;
_id = id;
_grade = grade;
}
}
class Employee {
protected String _name;
protected String _id;
protected Employee(String name, String id) {
_name = name;
_id = id;
}
}
class Manager extends Employee {
private int _grade;
public Manager(String name, String id, int grade) {
super(name, id);
_grade = grade;
}
}
手順
- スーパークラスのコンストラクタを定義する
- 先頭の共通コードの部分を、サブクラスからスーパークラスのコンストラクタに移動する
- これはコード全体になる場合がある
- 共通コードをコンストラクタの先頭にできるだけ移動する
- サブクラスのコンストラクタの最初で、スーパークラスのコンストラクタを呼び出す
- すべてのコードが共通な場合、これがサブクラスのコンストラクタで唯一の命令行になる
- テストする
- 後半部分に共通のコードがある場合、「メソッドの抽出(P.110)」を行って共通コードを抽出し、「メソッドの引き上げ(P.322)」を行って引き上げる
メソッドの引き下げ(Push Down Method)(P.328)
- スーパークラスの振る舞いが、いくつかのサブクラスだけに関係している
- そのメソッドをサブクラスに移動する
手順
- すべてのサブクラスで当該のメソッドを宣言し、そこにメソッドをコピーする
- そのメソッドがアクセスするフィールドを protected にする必要があるかも…
- スーパークラスのメソッドを削除する
- 呼び出し側の変数やパラメータ宣言で、サブクラスを使用するように修正する必要があるかも…
- コンパイルしてテストする
- 不必要なサブクラスからそのメソッドを削除する
- コンパイルしてテストする
フィールドの引き下げ(Push Down Field)(P.329)
- フィールドがいくつかのサブクラスだけで使われている
- そのフィールドを、それらのサブクラスに移動する
public class JobItem {
private int _unitPrice;
private int _quantity;
private Employee _employee;
private boolean _isLabor;
public JobItem(int unitPrice, int quantity, boolean isLabor, Employee employee) {
_unitPrice = unitPrice;
_quantity = quantity;
_employee = employee;
_isLabor = isLabor;
}
public int getTotalPrice() {
return getUnitPrice() * _quantity;
}
public int getUnitPrice() {
return _isLabor ? _employee.getRate() : _unitPrice;
}
public int getQuantity() {
return _quantity;
}
public Employee getEmployee() {
return _employee;
}
}
class Employee {
private int _rate;
public Employee (int rate) {
_rate = rate;
}
public int getRate() {
return _rate;
}
}
public class JobItem {
private int _unitPrice;
private int _quantity;
protected Employee _employee;
public JobItem (int unitPrice, int quantity) {
_unitPrice = unitPrice;
_quantity = quantity;
}
public int getTotalPrice() {
return getUnitPrice() * _quantity;
}
public int getUnitPrice() {
return isLabor() ? _employee.getRate() : _unitPrice;
}
public int getQuantity() {
return _quantity;
}
public Employee getEmployee() {
return _employee;
}
protected boolean isLabor() {
return false;
}
}
class Employee {
private int _rate;
public Employee (int rate) {
_rate = rate;
}
public int getRate() {
return _rate;
}
}
class LaborItem extends JobItem {
protected Employee _employee;
public LaborItem(int quantity, Employee employee) {
super(0, quantity);
_employee = employee;
}
public Employee getEmployee() {
return _employee;
}
protected boolean isLabor() {
return true;
}
public int getUnitPrice() {
return _employee.getRate();
}
}
手順
- そのフィールドをすべてのサブクラスで宣言する
- スーパークラスからそのフィールドを削除する
- コンパイルしてテストする
- フィールドを必要としないサブクラスからそのフィールドを削除する
- コンパイルしてテストする
サブクラスの抽出(Extract Subclass)(P.330) //TODO
- あるクラスの特定のインスタンスだけに必要な特性がある
- その一部の特性を持つサブクラスを作成する
- 「サブクラスの抽出」の有力な代替案
- 「クラスの抽出(P.149)」 → 委譲と継承のどちらかを使うかの選択
- 一般的には「サブクラスの抽出」を行うが、クラスを定義して、そこからインスタンスを生成してコードを動かす方式(クラスベース)の振る舞いを変えることができない
- 「クラスの抽出(P.149)」を行えば、異なるコンポーネントを差し替えるだけで、クラスベースの振る舞いを簡単に変更できる
手順
- 元のクラスに新しいサブクラスを定義する
- 新しいサブクラスにコンストラクタを用意する
- 単純なときは、スーパークラスのコンストラクタの引数をコピーして、super を使って呼び出せばいい
- クライアントからサブクラスの存在を隠蔽したい場合は、「Factory Method によるコンストラクタの置き換え(P.304)」を適用
- スーパークラスのコンストラクタへの呼び出しをすべて見つける。
- もしそれらがサブクラスを必要とするなら、新しいコンストラクタへの呼び出しに置き換える
- 「メソッドの引き下げ(P.328)」と「フィールドの引き下げ(P.329)」を適用して、1つずつ特性をサブクラスに移動する
- 継承階層で表現できる情報を示すフィールドを探す(通常、これは
boolean
型かタイプコード)。これを「自己フィールドカプセル化フィールド(P.171)」を適用して削除し、 get メソッドは定数を返すポリモーフィズムなメソッドに置き換える。このフィールドのすべてのユーザーは「ポリモーフィズムによる条件記述の置き換え(P.255)」により、リファクタリングを行う
スーパークラスの抽出(Extract Superclass)(P.336)
- 似通った特性を持つ2つのクラスがある
- スーパークラスを作成して、共通の特性を移動する
- 2つのクラスで似たようなことを同じやり方をしている、あるいは似たようなことを異なるやり方で行っている
Employee(従業員)クラスと Department(部署)クラスを例とする
class Employee {
private String _name;
private int _annualCost;
private String _id;
public Employee(String name, String id, int annualCost) {
_name = name;
_id = id;
_annualCost = annualCost;
}
public int getAnnualCost() {
return _annualCost;
}
public String getId() {
return _id;
}
public String getName() {
return _name;
}
}
class Department {
private String _name;
private Vector _staff = new Vector();
public Department(String name) {
_name = name;
}
public int getTotalAnnualCost() {
Enumeration e = getStaff();
int result = 0;
while (e.hasMoreElements()) {
Employee each = (Employee) e.nextElement();
result += each.getAnnualCost();
}
return result;
}
public Enumeration getStaff() {
return _staff.elements();
}
public int getHeadCount() {
return _staff.size();
}
public void addStaff(Employee arg) {
_staff.addElement(arg);
}
public String getName() {
return _name;
}
}
abstract class Party {
private String _name;
protected Party(String name) {
_name = name;
}
public String getName() {
return _name;
}
abstract int getAnnualCost();
}
class Employee extends Party {
private int _annualCost;
private String _id;
public Employee(String name, String id, int annualCost) {
super(name);
_id = id;
_annualCost = annualCost;
}
public int getAnnualCost() {
return _annualCost;
}
public String getId() {
return _id;
}
}
class Department extends Party {
private Vector _staff = new Vector();
public Department(String name) {
super(name);
}
public int getAnnualCost() {
Enumeration e = getStaff();
int result = 0;
while (e.hasMoreElements()) {
Employee each = (Employee) e.nextElement();
result += each.getAnnualCost();
}
return result;
}
public Enumeration getStaff() {
return _staff.elements();
}
public int getHeadCount() {
return _staff.size();
}
public void addStaff(Employee arg) {
_staff.addElement(arg);
}
}
手順
- 空の抽象スーパークラスを作成し、元のクラス群をこのスーパークラスのサブクラスにする
- 「フィールドの引き上げ(P.320)」、「メソッドの引き上げ(P.322)」、「コンストラクタ本体の引き上げ(P.325)」を適用して、スーパークラスに1つずつ移動する
- 最初にフィールド
- 引き上げが終わるたびにコンパイルしてテストする
- サブクラスに残されたメソッドについて検討する。それらに共通部分がないか、その共通部分に対して「メソッドの抽出(P.110)」を行って、「メソッドの引き上げ(P.322)」を適用できないか調べる。
- 全体の流れが共通している場合は、「Template Method の形成(P.345)を適用できるかもしれない
- すべての共通要素の引き上げが終わったら、サブクラスの各クライアントをチェックする。
- もし、共通のインターフェースだけを使っているならば、指定する型をスーパークラスの型に変更できる
インターフェースの抽出(Extract Interface)(P.341)
- 複数のクライアントが、あるクラスのひとまとまりのインターフェースを使っている
- また、2つのクラス間でインターフェースを一部が共通である
- その共通部分をインターフェースとして抽出する
- クラスの利用状況
- そのクラスのすべての責務を使用する
- クラスの責務の一部だけ使用する
- 特定の要求を受け付ける任意のクラスと協調しなければならない
- あるクラスが異なる状況において、複数の明確な役割を持つとき、インターフェースは有効
- 「インターフェースの抽出(P.341)」
- クラスの外部向けインターフェース = クラスがサーバとして提供する操作を記述したい場合
- サーバを作成したくなった場合、そのインターフェースを実装するだけで済む
- 「インターフェースの抽出(P.341)」
class Employee implements Billable {
public int getRate() {
return 0;
}
public boolean hasSpecialSkill() {
return false;
}
}
double charge(Employee emp, int day) {
int base = emp.getRate() * days;
if (emp.hasSpecialSkill()) {
return base * 1.05;
} else {
return base;
}
}
下記のようにすれば、 Employee
クラスの責務の一部だけ使っていることを示すことができる(引数を Billiable
に変更した)
interface Billable {
public int getRate();
public boolean hasSpecialSkill();
}
class Employee implements Billable {
public int getRate() {
return 0;
}
public boolean hasSpecialSkill() {
return false;
}
public int getTestValue() {
return 1;
}
}
double charge(Billable emp, int day) {
int base = emp.getRate() * days;
if (emp.hasSpecialSkill()) {
return base * 1.05;
} else {
return base;
}
}
手順
- 空のインターフェースを作成する
- 共通の操作をそのインターフェースに宣言する
- 関係するクラスでそのインターフェースの実装を宣言する
- クライアントの型宣言を、そのインターフェースを使うように修正する
階層の平坦化(Collapse Hierarchy)(P.344)
- スーパークラスとサブクラスにさほど大きな違いがない
- それらを合わせてまとめる
手順
- スーパークラス、サブクラスのどちらを削除するか決める
- 「フィールドの引き上げ(P.320)」と「メソッドの引き上げ(P.322)」、または「メソッドの引き下げ(P.328)」と「フィールドの引き下げ(P.329」を適用して、削除されるクラスのすべての振る舞いとデータを、併合するクラスに移動する
- 移動が終わるたびにコンパイルしてテストする
- 削除されたクラスへの参照を調整して、統合化されたクラスを使うようにする
- これにより、変数宣言やパラメータの型、コンストラクタ呼び出しに影響が及ぼすことがある
- 空のクラスを削除する
- コンパイルしてテストする
Template Method の形成(Form Template Method)(P.345)
- 異なるサブクラスの2つのメソッドが、類似の処理を同じ順序で実行しているが、各処理は異なっている
- 元のメソッドが同一になるように、各処理を同じシグニチャのメソッドにする。そしてそれらを引き上げる
手順
- メソッドを分解し、まったく同じか、またはまったく異なるメソッドになるように抽出する
- 「メソッドの引き上げ(P.322)」を適用して、まったく同じメソッドをスーパークラスに引き上げる
- 異なるメソッドに対しては、「メソッド名の変更(P.273)」を適用して、各処理のメソッドのシグニチャを同じにする
- これによって元のメソッドは、同じメソッド呼び出しを実行するため、同一になる。しかし、サブクラスでは各呼び出しに対して異なる処理を行う
- シグニチャを変更するたびにコンパイルしてテストする
- 元のメソッドの1つに対して「メソッドの引き上げ(P.322)」を適用する。
- 処理の異なるメソッドは、そのシグニチャをスーパークラスに抽象メソッドとして定義する
- コンパイルしてテストする
- 残りのメソッドを削除する。削除のたびにテストする
委譲による継承の置き換え(Replace Inheritance with Delegation)(P.352)
- サブクラスがスーパークラスの一部のインターフェースだけを使っている。あるいはデータを継承したくない
- スーパークラス用のフィールドを作成して、メソッドをスーパークラスに委譲するように変更した上で、継承を止める
- そのままにしておいてもよいが、コードが当初の意図と違った解釈をされ、混乱を招くかも…
class MyStack extends Vector {
public void push(Object element) {
insertElementAt(element, 0);
}
public Object pop() {
Object result = firstElement();
removeElementAt(0);
return result;
}
}
class MyStack {
Vector _vector = new Vector();
public void push(Object element) {
_vector.insertElementAt(element, 0);
}
public Object pop() {
Object result = _vector.firstElement();
_vector.removeElementAt(0);
return result;
}
public int size() {
return _vector.size();
}
public boolean isEmpty() {
return _vector.isEmpty();
}
}
手順
- サブクラスに、スーパークラスのインスタンスを参照するフィールドを作成する
- これを this によって初期化する
- サブクラスに定義された各メソッドを、委譲を行うように変更する
- サブクラスで定義されたメソッドで、スーパークラスのメソッドを呼び出しているものは置き換え不可能かもしれない
- 無限の再帰呼び出し状態になっているかもしれない
- 継承階層を壊してから、初めて置き換え可能となる
- サブクラスの宣言を削除して、委譲用のフィールドには、スーパークラスだったオブジェクトを割り当てる
- クライアントから使われるスーパークラスのメソッド用に、単純な委譲メソッドを追加する
- コンパイルしてテストする
継承による委譲の置き換え(Replace Delegation with Inheritance)(P.355)
- 委譲を使っていて、すべてのインターフェースに対する単純な委譲をたくさん書いている
- 委譲元のクラスを、委譲先のクラスのサブクラスにする
- 「委譲による継承の置き換え(P.352)」の裏返し
- 委譲先のクラスのメソッドをすべて使っているわけでない場合は、このリファクタリングはしない
- サブクラスはスーパークラスのすべてのインタフェースに常に従うべき
- 「仲介人の除去(P.160)」
- 「スーパークラスの抽出(P.336)」を適用して、共通のインタフェースを分離、その新しいクラスから継承する方法
- 「インタフェースの抽出(P.341)」
- サブクラスはスーパークラスのすべてのインタフェースに常に従うべき
class Employee {
Person _person = new Person();
public String getName() {
return _person.getName();
}
public void setName(String arg) {
_person.setName(arg);
}
public String toString() {
return "Emp: " + _person.getLastName();
}
}
class Person {
String _name;
public String getName() {
return _name;
}
public void setName(String arg) {
_name = arg;
}
public String getLastName() {
return _name.substring(_name.lastIndexOf(' ') + 1);
}
}
class Employee extends Person {
public String toString() {
return "Emp: " + getLastName();
}
}
class Person {
String _name;
public String getName() {
return _name;
}
public void setName(String arg) {
_name = arg;
}
public String getLastName() {
return _name.substring(_name.lastIndexOf(' ') + 1);
}
}
手順
- 委譲先のオブジェクトを委譲元のオブジェクトのスーパークラスになる
- 委譲用のフィールドに、自分自身のオブジェクトを代入する
- 単純な委譲メソッドを削除する
- テストする
- すべての委譲呼び出しを、自分自身のオブジェクトを呼び出すように変更する
- 委譲用のフィールドを削除する
第12章 大きなリファクタリング
ゲームの性質(P.359)
- 大きなリファクタリングは、稼働中のシステムに対して、数ヶ月から数年を必要とするものがある
- リファクタリングを行うときは、なにか別の目的が前提になっているべき
- 機能を追加
- バグを修正
- はじめから完全なリファクタリングをする必要はない
- 大きなリファクタリングには、プログラミングチーム内で合意する必要がある
なぜ大きなリファクタリングが必要か(P.360)
- 中途半端な設計判断の蓄積は、結果的にプログラムを行き詰まらせる
- リファクタリングをすることで、プログラムをどう設計すべきかについて完全な理解が、常にプログラムに反映
4つのリファクタリング(P.361)
- 「継承の切り離し(P.362)」
- 複数のバリエーションを混乱した方法で組み合わせたような、もつれた継承階層を扱う
- 「手続き的な設計からオブジェクト指向への変換(P.368)」
- 手続き的なコードに対して、どうすればよいかという古典的な問題の解決に役立つ
- 「プレゼンテーションとドメインの分離(P.370)」
- ユーザインタフェースとデータベースに対して古典的な2層アプローチを適用しているコードを見たら、ユーザインタフェースとビジネスロジックを分離
- 「階層の抽出(P.275)」
- 複雑すぎるクラスを複数のサブクラスに変換することで単純化
継承の切り離し(Tease A[art Inheritance)(P.362)
- 1つの継承階層で、2つのしごとをまとめて行っている
- 2つの継承階層を作成し、委譲を使って一方から他方を呼び出す
手順
- その継承階層で行われている異なる仕事を特定する。
- 2次元の表を作成し、異なる仕事の内容を軸に記入する
- それ以上の場合は、このリファクタリングを複数回行う必要がある
- 2次元の表を作成し、異なる仕事の内容を軸に記入する
- どの仕事が最も重要か、既存の階層に何を残し、何を別の階層に移すべきかを判断する
- 「クラスの抽出(P.149)」を共通のスーパークラスに適用して、補助的な仕事をするオブジェクトを作成し、それを保持するためのインスタンス変数を追加する
- 元の階層のサブクラスに対応して、抽出されたオブジェクトのサブクラスを作成する
- 前のステップで作成したインスタンス変数の初期化時に、このサブクラスのインスタンスを保持する
- 各サブクラスに「メソッドの移動(P.142)」を適用して、サブクラスの振る舞いを抽出されたオブジェクトに移動する
- 元のサブクラスのコードがなくなったときは、そのサブクラスを削除する
- 元のサブクラスすべてに対してこの手順を繰り返す。
- 新しい階層に対して「メソッドの引き上げ(P.322)」や「フィールドの引き上げ(P.320)」などを適用して、さらにリファクタリングできないかを調べる
手続き的な設計からオブジェクト指向への変換(Convert Procedural Design to Objects)(P.368)
- 手続き的な形式で書かれたコードがある
- データレコードをオブジェクトに変更して、振る舞いを分割し、その振る舞いをオブジェクトに移動する
手順
- レコードの種類ごとに、アクセサをもつ単純なデータオブジェクトを作成する
- リレーショナルデータベースを使っている場合、各テーブルをデータオブジェクトにする
- 手続き的なコードを、すべて1つのクラスに格納する
- そのクラスをシングルトンにするか、あるいはメソッドを static に変更する
- 長い手続きのそれぞれに対して、「メソッドの抽出(P.110)」や関連するリファクタリングを適用して分解する。
- 手続きを分解しながら「メソッドの移動(P.142)」を適用して、それぞれを適切なデータクラスに移動する
- 元のクラスからすべての振る舞いが削除されるまでこれを行う。
- もし元のクラスが純粋な手続き的なクラスだったならば削除する
プレゼンテーションとドメインの分離(Separate Domain from Presentation)(P.370)
- 問題領域のロジックを含むGUIクラスがある
- 問題領域のロジックを別のドメインクラスに分離する
手順
- 各ウィンドウに対応するドメインクラスを作成する
- 表がある場合は、その表の行を表現するクラスを作成する
- ウィンドウに対応するドメインクラスでは、コレクションを使って、行に対応するドメインオブジェクトを保持する
- ウィンドウ上のデータを調べる
- ユーザインタフェースの目的だけに使われる場合は、そのままウィンドウを残す
- ドメインロジックだけに使われ、ウィンドウ上に表示されない場合は、「フィールドの移動(P.146)」を適用して、ドメインオブジェクトに移動する
- ユーザインタフェースとドメインロジックの療法に使われる場合は、「観察されるデータの複製(P.189)」を適用して、両方の間で同期がとられるようにする
- プレゼンテーションクラスのロジックを調べる
- 「メソッドの抽出(P.110)」を適用して、ドメインロジックとプレゼンテーションに関するロジックを分離する
- ドメインロジックを独立させる過程で、「メソッドの移動(P.142)」を適用してロジックをドメインオブジェクトに移動する
- これが終わると、GUIを操作するプレゼンテーションクラスと、すべてのビジネスロジックを保持するドメインオブジェクトができあがる
- このドメインオブジェクトは、あまりよい構造になっていないかもしれない
- さらにリファクタリングをすることで改善できない
- このドメインオブジェクトは、あまりよい構造になっていないかもしれない
階層の抽出(Extract Hierarchy)(P.375)
- 仕事をし過ぎているクラスがある。少なくとも、多くの条件分岐を持つ部分がある
- クラス階層を作成して、サブクラスがそれぞれ特殊ケースの1つを表現するようにする
手順
- バリエーションを特定する
- もしバリエーションがオブジェクトの生存期間中に変化するなら、「クラスの抽出(P.149)」を適用して、その側面を別クラスに分離する
- 特殊ケースに対応するサブクラスを作成し、元のクラスに「Factory Method によるコンストラクタの置き換え(P.304)」を適用する
- ファクトリメソッドを変更して、適切なサブクラスのインスタンスを返すようにする
- 一度に1つずつ、条件ロジックを持つ一連のメソッドをサブクラスにコピーして、それらのメソッドがそのサブクラスのインスタンス用であって、スーパークラスのインスタンス用ではないと確認できるように単純化する
- メソッドの条件分岐に依存する部分と依存しない部分を分離する必要がある場合、「メソッドの抽出(P.110)」を適用する
- スーパークラスを abstract と宣言できるまで、特殊ケースの分離を続ける
- すべてのサブクラスでオーバーライドされている、スーパークラスのメソッドを削除し、スーパークラスを abstract にする
バリエーションが明確な場合に、次のような別の戦略が採用できる
- 各バリエーションに対応するサブクラスを作成する
- 「Factory Method によるコンストラクタの置き換え(P.304)」を適用して、各バリエーションに対応する適切なサブクラスを返すようにする
- そのバリエーションがタイプコードで表現されている場合、「サブクラスによるタイプコードの置き換え(P.223)」を適用する。そのバリエーションがオブジェクトの生存期間中に変化する場合は、「State/Strategy によるタイプコードの置き換え(P.227)」を適用する
- 条件ロジックを持つメソッドに対して、「ポリモーフィズムによる条件記述の置き換え(P.255)」を適用する。
- バリエーションがメソッド全体にわたっていなければ、変化する部分を「メソッドの抽出(P.110)」によって分離する
第13章 リファクタリング、再利用、現実
現実の壁(P.380)
- 誰かが「理解できない」とコメントしたり、単に伝わらなかった場合は、「われわれ」の責任
- 伝えるように一所懸命努力する責任がある
開発者が自分たちのプログラムをリファクタリングしようと思わない理由(P.381)
- リファクタリングのやり方が分からない
- 利益が長期的なものなら、何も今頑張ることもない。長期というなら、利益を得る前に、自分は今のプロジェクトを去っているかもしれない
- コードはリファクタリングするのは余計な作業だ。給料は、「新たな」機能を書くことに対して支払われている
- リファクタリングは既存のプログラムを壊してしまう
リファクタリングとは
ソフトウェアを再構築する上で、設計の治験をより明らかに示し、フレームワークを開発し、再利用可能なコンポーネントを抽出し、ソフトウェアアーキテクチャを明確にし、追加を行いやすくするよう準備する方法
リファクタリングする方法と場所を理解する(P.382)
- 自動化ツールで構造的な弱点を識別
- 引数が非常に多い関数
- 非常に長い関数
- 2つのほとんど同じ関数
- 同じ名前の変数が2つある
- プログラマが自分のコードをリファクタリングすべきであると納得する前提として、リファクタリングするところとその方法を理解する必要がある
- 自動化ツールは、プログラム構造を解析し、構造を改善するためのリファクタリングを指摘
- 自動化ツールは、構造的な解析に基づいた指摘であっても、本当に実行したいものは、プログラマが判断する
リファクタリングで短期的な利益を享受するには(P.386)
- 2つのサブクラスのコードを徐々に類似させていき、関数が同一になったら、それを共通のスーパークラスに移動
- 短期的なメリット
- テストによって共通コードの部分で見つかったエラーは「1ヶ所」だけ変更すればよくなった
- コード全体のサイズが小さくなった
- 固有な振る舞いに対する追跡と修正が簡単になった
- 中期的なメリット
- 追加が容易になった
- なにが共通か明確にすることができた
- 短期的なメリット
リファクタリングのオーバーヘッドを減らす(P.389)
- いくつかのツールや技法を使えば、リファクタリングは短時間で済む
- プログラム開発における他フェーズでの労力の現象と、空き時間でまかなって余りあるもの
- 最初は余計な作業項目とうつるかもしれないが、習慣の一部になれば、不可欠なものになる
安全にリファクタリングを行う(P.390)
- プログラマは誰でも誤りうる
- コンパイラでは補足できないエラーがある
- 継承に関わるスコープエラーは、コンパイラでは補足できない
- プログラムにありうるすべての実行経路をテストすることは。計算決定不能な問題
- テストスイートがすべての状況を補足することは保証できない
- コードレビュアーもプログラマ同様、過ちを犯す
- レビュアーは自分の業務に忙しいため、他人のコードを徹底的にレビューすることは難しい
- 筆者の安全なリファクタリングへのアプローチとして、リファクタリングツールを定義した
- 単純で退屈なチェックは大量にこなせる
- チェックしないでリファクタリングをおこない、プログラムを破壊するような問題を前もって警告してくれる
- エラーの購入を防ぐことができる
- 安全なアプローチとは、リファクタリングによりプログラムに「新たな」エラーを混入しないことを保証しようとするもの
- リファクタリングの前からプログラムにあったバグを検出して修正するものではない
- リファクタリングをすることで、そうしたバグを照らし出し、修正することが容易となる
- リファクタリングの前からプログラムにあったバグを検出して修正するものではない
現実の壁(再び)(P.394)
- プロジェクトによる懸念事項
- リファクタリング対象のコードが、数人のプログラマにより所有されている場合はどうか。
- 従来から数多く使われている変更管理の仕組み(Git, SVNなど?)の仕組みが有効かも
- ソフトウェアがうまく設計され、リファクタリングされていたら、サブクラスは十分に分離されて、多くのリファクタリングの影響はコードベースの一部分だけに限定される
- 1つのコードベースに対して、複数のバージョンやステップがある場合はどうか。
- リファクタリングがすべてのバージョンに影響があるならば、リファクタイングを適用する前に、すべてのバージョンで安全性のチェックを行う必要がある
- リファクタリングは、そのバリエーションやバージョンを統合して、新たなコードベースとするのに有用 → 以降のバージョン管理が簡単になる
- リファクタリング対象のコードが、数人のプログラマにより所有されている場合はどうか。
第15章 部品から全体へ
目標の選別に慣れる
- リファクタリングは真実や美を追求するだけのものでない
- プログラムをわかりやすく、イケていないプログラムを書き直すこと
自信がなくなったらやめる
- 自分がつくったプログラムのセマンティクスが保っていると、自分や他の人に証明できないなら、そこがやめ時
- それまでの変更はなかったことにする セマンティクス → ソースコード中で利用されている変数や文が正しく動作するかを判断する基準のこと。
引き返す
- リファクタリングしまくって、いずれかのテストが失敗したらデバッグしたくなる…
- 我慢してとりあえずテストを通すことを最優先
- もう一度テストを通すのはかなりの労力 → やり直して1つずつ修正していこう
- 我慢してとりあえずテストを通すことを最優先
デュエットで
- リファクタリングもペアでやろう
- 相棒がいれば、自分がズルをすることなく一歩一歩進むことを守らせてくれる
- 隣にいるから、すぐに会話できる
- 不吉な臭いと、それを消し去るリファクタリングを理解すれば、長年動いてるプログラムでも、秘められた新しい可能性を見出すことができる
- 新しい機能を追加する前に、ちょっと時間をとって整理してみよう
- 自信をもって整理できるなら、テストを追加しよう
- いきなり新しいコードを追加するより絶対よい
- 2つの帽子を忘れてはいけない
- 「リファクタリングするとき」と「機能追加するとき」
- リファクタリングしているときの目標は、コードの計算結果を元のままに保つこと
- 変更する事項(追加、変更するテストケース、関係ないリファクタリング、書くべきドキュメント、描くべき図など)は、書き出しておく
Java 7 で本書コードを再びリファクタリングする
before
abstract public class Price {
abstract int getPriceCode();
abstract double getCharge(int daysRented);
int getFrequentRenterPoints(int daysRented, Movie movie) {
return 1;
}
}
class ChildrensPrice extends Price {
int getPriceCode() {
return Movie.CHILDRENS;
}
double getCharge(int daysRented) {
double result = 1.5;
if (daysRented > 3) {
result += (daysRented - 3) * 1.5;
}
return result;
}
}
class NewReleasePrice extends Price {
int getPriceCode() {
return Movie.NEW_RELEASE;
}
double getCharge(int daysRented) {
double result = daysRented * 3;
return result;
}
int getFrequentRenterPoints(int daysRented, Movie movie) {
return daysRented > 1 ? 2 : 1;
}
}
class RegularPrices extends Price {
int getPriceCode() {
return Movie.REGULAR;
}
double getCharge(int daysRented) {
double result = 2;
if (daysRented > 2) {
result += (daysRented - 2) * 1.5;
}
return result;
}
}
public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String _title;
private int _priceCode;
private Price _price;
public Movie(String title, int priceCode) {
_title = title;
setPriceCode(priceCode);
}
public int setPriceCode() {
return _price.getPriceCode();
}
public void setPriceCode(int arg) {
switch(arg) {
case REGULAR:
_price = new RegularPrices();
break;
case CHILDRENS:
_price = new ChildrensPrice();
break;
case NEW_RELEASE:
_price = new NewReleasePrice();
break;
default:
throw new IllegalArgumentException("不正な料金コード");
}
}
public String getTitle() {
return _title;
}
double getCharge(int daysRented) {
return _price.getCharge(daysRented);
}
int getFrequentTenterPoints(int daysRented) {
return _price.getFrequentRenterPoints(daysRented, this);
}
}
import java.util.Enumeration;
import java.util.Vector;
public class Customer {
private String _name;
private Vector _rentals = new Vector();
public Customer(String name) {
_name = name;
}
public void addRental(Rental arg) {
_rentals.addElement(arg);
}
public String getName() {
return _name;
}
public String statement() {
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
// この貸し出しに関する数値のみ表示
result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
}
// フッタ部分の表示
result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) + "frequent renter points";
return result;
}
public String htmlStatement() {
Enumeration rentals = _rentals.elements();
String result = "<H1>Rental Record for <EM>" + getName() + "</EM></H1><P>\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
// この貸し出しに関する数値のみ表示
result += each.getMovie().getTitle() + ": " + String.valueOf(each.getCharge()) + "<BR>\n";
}
// フッタ部分の追加
result += "<P>You owe <EM>" + String.valueOf(getTotalCharge()) + "</EM><P>\n";
result += "On this rental you earned <EM>" + String.valueOf(getTotalFrequentRenterPoints()) + "<EM> frequent renter points<P>";
return result;
}
private double getTotalCharge() {
double result = 0;
Enumeration rentals = _rentals.elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result += each.getCharge();
}
return result;
}
private int getTotalFrequentRenterPoints() {
int result = 0;
Enumeration rentals = _rentals.elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result += each.getFrequentRenterPoints();
}
return result;
}
}
public class Rental {
private Movie _movie;
private int _daysRented;
public Rental(Movie movie, int daysRented) {
_movie = movie;
_daysRented = daysRented;
}
public int getDaysRented() {
return _daysRented;
}
public Movie getMovie() {
return _movie;
}
double getCharge() {
return _movie.getCharge(_daysRented);
}
int getFrequentRenterPoints() {
return _movie.getFrequentTenterPoints(_daysRented);
}
}