レガシーコード改善ガイド
情報
原著は2004年9月発売
本書は、システム保守の現場でありがちな、構造が複雑で理解できないような
コードに対する分析手法・対処手法について解説します。
つまり、「コードを理解し、テストできるようにし、リファクタリングを可能にし、
機能を追加できるテクニック」を紹介している書籍です。
例に使用されている言語
Java
C++
C
動機
baserCMS 5開発において、CakePHPを2.xから3.xにアップデートするためコードが書き直しになるが、既存コードをテスタブルに書き換えていくのに役立てるため baserCMSの内製プラグインにテストを追加する動きがあり、その参考にするため
まえがき
要求の変更に耐えられない設計は、そもそも悪い設計
要求を変えてくる顧客のせいにしない
腐敗を防ぐだけでは不十分
腐敗を逆戻りさせる方法が本書のテーマ
はじめに
私にとって、レガシーコードとは、単にテストのないコードです
きれいなコードだけでは不十分
レガシーコード改善は外科手術のようなもの
美しいコードや設計といった美的判断は保留にし、優れた設計原則を念頭に置きながら少しずつ改善する
第1部 変更のメカニズム
第1章 ソフトウェアの変更
ソフトウェア変更の4つの理由
1. 要件の追加
2. バグの修正
ソフトウェアで最も大切なのは「振る舞い」
契約や品質管理の要請から要件追加かバグ修正か別々に管理する必要がある場合があるが、その議論によって、振る舞いが変更されるかどうか、という重要なことが隠されてしまう
振る舞いの変更かどうか判断する有効な方法としてコードを変更する必要があるならとみなす方法がある
3. 設計の改善
振る舞いを変えずに設計を改善する方法がリファクタリング
4. リソース利用の最適化
機能は変えず、時間やメモリなどのリソースを変えること
変更の際に既存の振る舞いを変えずに保つことが重要
変更を行った時に何が起こるか整理することも良いがそれ以上に重要
リスクを避けるため変更の回避することには問題がある
新しいクラスやメソッドを作らないことで既存のクラスやメソッドは肥大化する
頻繁に変更をしていないと腕がなまってしまう
日に日に強くなる恐怖をもたらす
第2章 フィードバックを得ながらの作業
システム変更の2つの方法
Edit and Pray
注意深く作業するため一見プロらしく見えるが安全性が注意に比例するわけではない
Cover and Modify
テストで保護する
振る舞いの大部分の固定のために、従来の用語では「回帰テスト」と呼ばれていた、変更を見つけるためのテスト レガシーコードを扱う際はシステムレベルの回帰テストだけでなく、単体テストが非常に重要
本書で言う単体テストは手続き型では関数、オブジェクト指向ではクラス単位
大規模なテスト
問題
エラー箇所の特定
実行時間
カバレッジ
頻繁なテストを避けてしまいがちになる
優れた単体テストの条件
実行が速い
問題箇所の特定がしやすい
実行に0.1秒もかかる単体テストは、遅い単体テストである
例:3000クラス * 10テスト * 0.1秒 = 1時間近く
tommy.icon データベースとやり取りしたりネットワーク通信をするテストは単体テストではないと書いてあるけどその辺はモック使えということかな?
レガシーコードを扱う時にはたいてい、変更を容易にするために依存関係を排除する必要がある
tommy.icon インターフェイスの使い方がずっとよく解ってなかったけどようやくつかめてきた。テスタビリティが上がるんだな。
最初のリファクタリング段階から非常に保守的に作業することが秘訣
レガシーコードの変更手順
1. 変更点を洗い出す
2. テストを書く場所を見つける
3. 依存関係を排除する
4. テストを書く
5. 変更とリファクタリングを行う
第3章 検出と分離
クラス間の依存関係を排除する2つの理由
1. 検出 (Sensing)
コードの計算した値にアクセスできない時に、それを検出するために依存関係を排除する
2. 分離 (Separation)
コードをテストハーネスに入れて実行することすらできない時、分離するために依存関係を排除する
協調クラスの擬装
ソフトウェアを分離する方法は様々だが検出のための主な手段はこれ1つだけ
テスト用にメソッドを用意し、テストではそのクラスの型のインスタンスとして扱う
メソッド呼び出しを事前に設定し、呼び出し後に検証する
テストを書く時には「分割して統治する」ことが肝要
個々の単位についてテストを書くと理解しやすい小さな単位が出来上がる
第4章 接合モデル
接合部 (seam) とは、その場所を直接編集しなくても、プログラムの振る舞いを変えることのできる場所である。 接合部で振る舞いを置き換えることでテスト時に依存関係を取り除ける
接合部の種類
プリプロセッサ接合部
CやC++におけるマクロや#includeを利用するもの
リンク接合部
テスト環境においてインポートで探しに行く場所をテスト用コードの置き場に書き換える
オブジェクト接合部
OO言語でおそらく一番役に立つもの
引数で本番とは違うオブジェクトを渡したり、呼び出しているメソッドをオーバーライドしたりする
どの接合部も許容点 (enable point) を持つ 許容点ではどの振る舞いを使うかを決定できる
OO言語では他に良い選択肢がない場合以外はオブジェクト接合部を使う
第5章 ツール
tommy.icon この辺は情報が古いかもしれないけど自動リファクタリングツールを信用しすぎないというのは頭に入れておきたい
第2部 ソフトウェアの変更
第6章 時間がないのに変更しなければなりません
テストを整備することに時間がかかり、最悪再び変更するまで何年もかかるかもしれないが、一般的に変更箇所は集中する
テストコードの整備によりコードの変更はより簡単になり、開発作業全般を速める
コードはあなたの家であり、あなたはその中で暮らさなければならない
今、時間をかけるか、あるいは将来、時間をかけるか
(スプラウトとは「発芽させる」、「新芽」の意)
手順
コードの変更の際に、その変更が一連の命令文として実現できる場合、それを行うメソッドの呼び出しを先にコメントアウトした状態で書いておき、TDDによりメソッドを作成し、呼び出しのコメントを外す 独立した1つの機能として追加する場合や、メソッドのテストを整備していない場合に適用する
長所
古いコードと新しいコードを明確に区別できる
短所
元のメソッドとそのクラスに愛想をつかしたと言っていることになる
手順
コードの変更の際に、その変更が一連の命令文として実現できる場合、それを行うクラスの生成とメソッドの呼び出しを先にコメントアウトした状態で書いておき、TDDによりメソッドを作成し、呼び出しのコメントを外す 作成する状況
クラスにまったく新しい責務を追加したい場合
機能追加したいクラスがテストハーネスの中でテスト可能な状態でない場合
長所
コードを直接書き換えるより確信を持って変更を進められる
C++の場合、既存のヘッダーファイルの修正が不要
短所
仕組みが複雑になる
2つの形
元のメソッドと同じ名前のメソッドを新しく作り、古いコードに処理を委譲する
元のメソッドの処理の前後に新しい振る舞いを追加したい場合に適用
どこからも呼び出されていない新しいメソッドを追加
新しい要件を追加しつつ、接合部を導入する
弱点
追加する機能が既存の機能のロジックの前か後ろに行うものでなければならない
新しい名前を考え出さなければならない
長所
既存のメソッドの長さが変わらない
新しい機能を独立させられる
短所
不適切な名前を付けがち
方法
ラップしたいコードを呼び出している箇所が多い場合に有効
別のクラスにその機能を置いてオブジェクトを受け取らせる
使用場面
1. 追加振る舞いが完全に独立しており、既存のクラスを汚染したくない場合
2. クラスが巨大化し過ぎている場合
見苦しいコードは人に改善が無駄だと信じさせてしまうが、小さな改善を続ければシステムは見違えるようになり、人も変わることを著者は経験から知っている
tommy.icon 小さなステップの積み重ねだな。特にレガシーコードが相手だと大事になりそう。
第7章 いつまで経っても変更作業が終わりません
クラスやモジュールを独立してコンパイルできるようにすることでフィードバックが素早く得られるようにし、開発のスピードを上げる
依存するクラスのインタフェースを抽出することでコンパイル時間を小さくする
依存先も実装クラスを変更しても再コンパイルの必要がない
アプリケーション構造で明示的に表現したい場合はパッケージでグルーピングする
tommy.icon 依存先をインタフェースにするメリットはその実装クラスが1つしか無い場合だとコンパイル時間の削減以外にあるんだろうか
システム全体のビルド時間は少し増えるが必要なものだけの再コンパイル時間は減る
第8章 どうやって機能を追加すればよいのでしょうか?
まずはコンパイルを通すことを考え、時間が経っても一般化したい場合はその時に行う
必要とするコードを単にコピーし、新しいメソッドとして修正、あとから重複を取り除く方法は非常に強力
テストがあることで簡単に重複を取り除くことができる
TDDではコードを書いているかリファクタリングをしているかのどちらか1つのことだけに集中できる
機能追加の前に変更したいクラスをテストで保護する
継承による機能追加
継承の問題として複数の機能を別々のサブクラスとして追加した場合、一度に1つの機能しか利用できない
テストを書き、まずはサブクラス作成で解決してテストを通し、リファクタリングという流れ
「クラス名の変更」はリファクタリングの中でも最も強力
サブクラスのオブジェクトは、どんな場合においてもスーパークラスのオブジェクトの代わりに使えなければならない
LSPを守るための経験則
1. 可能であれば具象メソッドをオーバーライドしない
2. 具象メソッドをオーバーライドするなら、その中でオーバーライド対象のメソッドを呼べる
具象メソッドをオーバーライドすると、使う側の振る舞いを変えてしまい、利用者に分かりにくくなる
スーパークラスから継承した具象メソッドをオーバーライドするクラスがない階層を著者は「正規化された階層」と呼ぶ 第9章 このクラスをテストハーネスに入れることができません
テストハーネス内でのクラスのインスタンス化が簡単であれば本書はずっと薄いものになっていた
テストハーネスでのオブジェクト生成をまずは単にやってみて難易度を把握する
インタフェースの抽出
テストのためのクラスはルールが異なるためメソッドが単にnullを返したり変数がpublicであったりしても良い
Nullを渡す
オブジェクトが生成しづらいパラメータを必要とする場合、Nullを渡すことを考える
例外のある言語であれば、それが使われた場合に例外を投げ補足してくれる
コンストラクタのパラメータ化
新たなコンストラクタを作成し、テストではそれを呼ぶようにすれば、クラスの呼び出し側を書き換える必要はない
コンストラクタに隠れた依存関係は他にも手法があるが、著者はコンストラクタのパラメータ化を好んで使う
インスタンス変数の入れ替え
コンストラクタの内部でオブジェクトを生成し、別のオブジェクト生成に利用している場合などに使う
グローバル変数は他の場所の変数にアクセスしたり変更したりしているかどうかわからない
Singletonへの対処方法
オブジェクトの生成は他のオブジェクトを必要とし、さらにそのオブジェクトも必要とする玉ねぎのようになっている
コンストラクタに渡すパラメータの中で本当に必要なものを特定し、「Nullを渡す」や「インタフェースの抽出」、「実装の抽出」といった対策をとる
「インタフェースの抽出」は素晴らしいものだがクラスとインタフェースの関係がほとんど1対1になると設計は混乱する
第10章 このメソッドをテストハーネスで動かすことができません
privateメソッドをテストしなければならない場合、publicにすべき
publicにするか悩んでしまう場合はそのクラスが多くのことを行いすぎている
時間やリスクにより責務を切り離す余裕がない時は対象privateメソッドをprotectedに変更し、サブクラスを作り対象メソッドをpublicにする
制御不可能なライブラリ(finalを指定されているクラスなど)に対してはそのスーパークラスをサブクラス化したり、インタフェースを抽出してAPIをラップすることで対策する
コマンド: オブジェクトの状態を変更するが値を返さない
クエリー: 値は返すがオブジェクトの状態を変更しない
コマンドまたはクエリーのいずれかであり、両方にすべきでないという原則
最も重要な理由は理解しやすさ
tommy.icon GUIのクラスのリファクタリングの仕方、参考になった
第11章 変更する必要がありますが、どのメソッドをテストすればよいのでしょうか?
影響の調査
影響を受ける変数と戻り値が変わる可能性のあるメソッドの楕円を書き、値が変わり得るものに向かって矢印を描く
単純であれば理解、保守しやすいコードとなる
11.2 前方向の調査
tommy.icon 変更を行う際の影響スケッチの書き方として参考になる
テストする場所を見つける必要がある場合、変更の影響が何かを把握し、どこで影響を検出できるかわかったら、その中からテストを書く
影響を制限することでコードの理解を助ける
カプセル化とテストによる保護が対立する場合、著者はテストによる保護を優先する
カプセル化は目的ではなく手段
第12章 1カ所にたくさんの変更が必要ですが、関係するすべてのクラスの依存関係を排除すべきでしょうか?
特定の変更による影響を検出出来るプログラム上の場所
一般的に、安全性と、テストの準備の容易さにより、変更点のすぐそばにある割り込み点を選択することは良い考え
影響スケッチの中で集約されている場所
幅広い変更を担保するテストを書くことができる
影響スケッチを利用してより良いカプセル化を実現することが可能
第13章 変更する必要がありますが、どんなテストを書けばよいのかわかりません
自動化したテストはバグを見つけるためのものではなく、ゴールを仕様化するか、既存の振る舞いを維持するもの
仕様化テスト (characterization test) 振る舞いを維持するために必要なテスト
現在の振る舞いをそのまま文書化する
テストハーネスで対象コードが失敗する表明を書き、その結果から実際の振る舞いを確認し、その振る舞いを期待するようにテストを変更する、を繰り返す
その時点でバグを見つけるためではなく、現在の振る舞いとの相違という形で現れるバグを将来見つけるため
ブラックボックステストを書くわけではない
コードを調べながらコードの振る舞いを理解できたと満足できるまでテストを書く
変更に起因する問題を今あるテストで検出できると確信できるまでテストを追加する
第14章 ライブラリへの依存で身動きが取れません
ライブラリへ強く依存しすぎない
第15章 私のアプリケーションはAPI呼び出しだらけです
責務を分割し、APIに依存している部分を明らかにし、設計し直す
2つのアプローチ
APIをラップ
APIが比較的小さい
依存を完全に分離したい
テストがなく、APIを通じたテストが不可能なため、テストを書けない
責務をもとに抽出
APIが複雑
メソッド抽出ツールがあるか、手動で安全に行える
第16章 変更できるほど十分に私はコードを理解していません
コードを理解するための方法
メモを取ったり、スケッチを描く
UMLの文法に従う必要はない
印をつける
印刷して行う
コードをチェックアウトし、あらゆるリファクタリングを行ったあと破棄する
使用していないコードの削除
第17章 私のアプリケーションには構造がありません
アーキテクトはチームに加わって日々一緒に仕事をする必要がある
チームの全員がアーキテクチャとは何かを知り、関心を持つことがアーキテクチャを保つ鍵
システムのストーリーを話す
説明役がシステムについて知らないと仮定した相手にシステムのアーキテクチャを説明する
簡潔に伝える
そうすることでシステムの最も重要なことは何か考え、単純化、抽象化することができる
CRCはClass, Responsibility, Collaborationの略
何も書かないカードを使い、オブジェクトとその相互作用を伝える
会話の吟味
会話とコードの間に共通する部分がない場合、理由を考える
第18章 自分のテストコードが邪魔になっています
テストに関するクラスの命名は人間工学的な視点が重要
第19章 私のプロジェクトはオブジェクト指向ではありませんが、どうすれば安全に変更できるでしょうか?
古い関数に新しいコードを追加するよりも、新しい関数を導入することを優先すべき
少なくともその関数に対するテストを書くことができる
第20章 このクラスは大きすぎて、もうこれ以上大きくしたくありません
大きなクラスの問題
把握の困難による混乱
作業計画の調整の難しさ
テストが大変
責務
クラスの「主要な目的」について議論することで明確にできる
経験則
1. メソッドを分類する
名前の似ているメソッドをアクセス属性とともにグループ化する
2. 隠蔽されたメソッドを調べる
private、protectedメソッドが多い場合、別のクラスに取り出せる可能性がある
3. 変更可能な決定事項を探す
特別なAPIやデータベースのアクセスなどがハードコードされているなら変更できないか
4. 内部的な関係を探す
インスタンス変数とメソッドの関係を探す
機能スケッチ (feature sketch)
1. 変数を表す円を書く
2. メソッドを表す円を追加する
3. メソッドから変数に対して矢印を引く
まとまりからクラスを抽出する
大きなまとまり同士を接続する小さな線、すなわち絞り込み点が見つかる場合がある
5. 主要な責務を探す
クラスの責務を1文で説明してみる
単一責務の原則の違反
インタフェースレベルの違反
インタフェースを分離する
実装レベルの違反
責務を別のクラスに委譲させる
6. 試行リファクタリングを行う
他のすべての経験則が役立たない場合
7. 現在の作業に集中する
行おうとしている変更により責務がわかる場合がある
tommy.icon ここはかなり勉強になった
tommy.icon 本全体で一番響いた箇所かも
クラスの分割にはリスクも伴うので必要に応じて分割する
単一責務の原則のインタフェースレベルでの導入は難しいためまずは実装レベルから始める
第21章 同じコードをいたるところで変更しています
重複部分を洗い出して取り除くことで重複の排除が本当に有効か判断する
小さな重複から始める
アプリケーションが大きな箱だとすると振る舞いごとに1つの取っ手がある状態
第22章 モンスターメソッドを変更する必要がありますが、テストを書くことができません
モンスターメソッドの変種
箇条書きメソッド
ほとんどインデントされていないメソッド
錯乱メソッド
インデントされた条件文などの大きなセクションから構成されるメソッド
テストなしで自動リファクタリングを行い場合、リファクタリングツールだけを使い、手動による文の並び替えなども行わない
その後でテストを整備し、手作業での変更を検証する
手作業によるリファクタリング
検出用変数の導入
理解している部分の抽出
抽出の結合カウント
入力、出力の変数の数の合計
カウントの小さい抽出の方が安全なため先に行う
依存関係の落ち穂拾い
メソッドの主目的である重要な部分に対してテストを書き、テスト対象になっていない部分を抽出する
戦略
骨組みメソッド
条件部分と処理部分を別々に抽出する
処理シーケンスの発見
条件部分と処理部分をまとめて抽出する
まず制御構造を明確にするため骨組みメソッドを作成し、コードが明確になると感じた場合、処理シーケンスを探すようにする
第23章 どうすれば何も壊していないことを確認できるでしょうか?
「プログラミングとは一度に1つのことを行う技術である」
第24章 もうウンザリです。何も改善できません
隣の新規開発の芝は、実はそれほど青くない
やりがいを見出すことがレガシーコードで成功する鍵
第3部 依存関係を排除する手法
第25章 依存関係を排除する手法
パラメータの適合
標準インタフェースへの依存関係を完全に排除するためパラメータをラッピングする
全体
tommy.icon 時々出てくる手術と同じできれいな設計にするという美的感覚を脇にやらなければならない時があるという例え解りやすい
tommy.icon あわせて読みたい本
tommy.icon 気軽にコンストラクタを作ってしまうけど、テストにおけるインスタンス化の難しさが分かった
tommy.icon インタフェースもっと使っていきたいと思った
tommy.icon テストフレームワークでテストダブルを用意する方法が色々整っている現在だと違った書き方になるものもあるのかなと思った tommy.icon 他のオブジェクトを利用することで生じる依存性についてもっと考えていかなければと思った
tommy.icon 影響スケッチや機能スケッチは実践すると良さそう
tommy.icon 第20章が一番響いた
tommy.icon テストを書くためにいかに依存性を排除するかということに尽きるのかなと思った
メモ
取り上げられそうなトピック
影響スケッチ
スプラウトクラス、スプラウトメソッド
関連リンク