レガシーコード改善ガイド

test

2019-10-13 13:27:13

レガシーコード改善ガイド

  • 第1部 変更のメカニズム
    • 第1章 ソフトウェアの変更
    • 第2章 フィードバックを得ながらの作業
    • 第3章 検出と分離
    • 第4章 接合モデル
    • 第5章 ツール
  • 第2部 ソフトウェアの変更
    • 第6章 時間がないのに変更しなければなりません
    • 第7章 いつまで経っても変更作業が終わりません
    • 第8章 どうやって機能を追加すればよいのでしょうか?
    • 第9章 このクラスをテストハーネスに入れることができません
    • 第10章 このメソッドをテストハーネスで動かすことができません
    • 第11章 変更する必要がありますが、どのメソッドをテストすればよいのでしょうか?
    • 第12章 1ヶ所にたくさんの変更が必要ですが、関係するすべてのクラスの依存関係を排除すべきでしょうか?
    • 第13章 変更する必要がありますが、どんなテストを書けばよいのかわかりません
    • 第14章 ライブラリへの依存で身動きが取れません
    • 第15章 私のアプリケーションはAPI呼び出しだらけです
    • 第16章 変更できるほど十分に私はコードを理解していません
    • 第17章 私のアプリケーションには構造がありません
    • 第18章 自分のテストコードが邪魔になっています
    • 第19章 私のプロジェクトはオブジェクト指向ではありませんが、どうすれば安全に変更できるでしょうか?
    • 第20章 このクラスは大きすぎて、もうこれ以上大きくしたくありません
    • 第21章 同じコードをいたるところで変更しています
    • 第22章 モンスターメソッドを変更する必要がありますが、テストを書くことができません
    • 第23章 どうすれば何も壊していないことを確認できるでしょうか?
    • 第24章 もうウンザリです。何も改善できません
  • 第3部 依存関係を排除する手法
    • 第25章 依存関係を排除する手法
  • 付録A リファクタリング
  • 付録B 用語集

はじめに

p.6

  • レガシーコードとは
    • テストのないコード
      • テストのないコードはコードの良し悪しが検証できない

第1部 変更のメカニズム

第1章 ソフトウェアの変更

1.1 ソフトウェア変更の4つの理由

p.3

  • 要件の追加
  • バグの修正
  • 設計の改善
  • リソース利用の最適化

p.4

  • ソフトウェアで最も大切なのは「振る舞い」
    • 振る舞い
      • UIの変更を伴うもの

第2章 フィードバックを得ながらの作業

p.12

  • ソフトウェア万力
    • テストコード
      • コードの振る舞いを固定する万力

2.1 単体テストとは

p.14

  • 単体テスト
    • 個々のComponentに大して個別に行うテスト
    • Component
      • システムの振る舞いの面から原始的な単位
      • ex. 手続き型のコードなら関数

p.17

  • 以下に当てはまるものは単体テストではない(=振る舞いの原始的単位ではない)
    • データベースとやり取りする
    • ネットワークを介した通信をする
    • ファイルシステムにアクセスする
    • 実行するために特別な環境設定を必要とする(環境設定ファイルの編集など)

2.4 レガシーコードの変更手順

p.21

  • 変更点を洗い出す
  • テストを書く場所を見つける
  • 依存関係を排除する
  • テストを書く
  • 変更とリファクタリングを行う

※詳細は本書の後続の章

第3章 検出と分離

  • N/A

第4章 接合モデル

4.2 接合部とは

p.36

  • 接合部
    • その場所を直接編集しなくても、プログラムの振る舞いを変えることのできる場所
    • 依存関係の排除

4.3 接合部の種類

p.38

  • プリプロセッサ接合部
  • リンク接合部
  • オブジェクト接合部

p.41

  • 許容点
    • どの接合部も許容点を持つ
    • どの振る舞いを使うか決定できる

第5章 ツール

  • N/A

第2部 ソフトウェアの変更

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

  • テストを書いておけば「最終的に」時間の節約になる
    • 「最終的に」
      • エラーの補足、次の変更作業時、コードリーディング...etc

6.1 スプラウトメソッド

p.67

  • スプラウトメソッド
    • 新しく追加するテストを伴うメソッドのこと
    • 既存のコードと決別して、独立した機能として実装

6.2 スプラウトクラス

p.71

  • スプラウトクラス
    • スプラウトメソッドのクラス版
    • クラスの変更が困難なとき、新しいクラスを作成し、呼び出し側でインスタンス化して直接呼び出す
    • クラス間の関係が複雑化するが、変更に対して確信を持てるようになる

6.3 ラップメソッド

p.76

  • ラップメソッド
    • 例を見る限りだど、要はメソッドの分割では?
    • 既存のメソッドの名前を維持しつつ、処理を新しく作る別のメソッドに委譲する

6.4 ラップクラス

p.80

  • ラップクラス
    • 継承またはDIで処理をラップ

第7章 いつまで経っても変更作業が終わりません

7.3 依存関係の排除

7.3.1 依存関係逆転の原則(Dependency Inversion Principle: DIP)

p.93
  • インターフェースに依存する場合は、依存は小さい
    • インターフェースを変更しなければコードを変更する必要がない
      • インターフェースの変更は実装コードの変更に比べると少ない

第8章 どうやって機能を追加すればよいのでしょうか?

8.1 テスト駆動開発(TDD)

8.1.12 重複を取り除く

p.104
  • テスト駆動開発をレガシーコード
    • テスト駆動開発
      • 一度に1つのことだけに集中できる
        • コードを書いているか、リファクタリングをしているかのどちらか

8.2 差分プログラミング

p.105

  • 差分プログラミング
    • 既存クラスの共通項をベースに新しいクラスを作り出すこと

p.112

  • Liskovの置換原則(The Liskov Substitution Principle: LSP)
    • 派生型のクラスは基本型のクラスと置換可能でなければならない
      • ex. クラスAのインスタンスがクラスBのインスタンスに依存しているとき
        • クラスBのサブクラスクラスB'を作成するときは、
        • クラスAにクラスB'を渡しても正常に動作しなければならない
        • →スーパークラスの設計が問題なくできれていれば達成可能

第9章 このクラスをテストハーネスに入れることができません

  • クラスのオブジェクトを簡単に生成できない
  • そのクラスを含むテストハーネスを簡単にビルドできない
  • 使用しなければならないコンストラクタが悪い副作用を持つ
  • かなりの量の処理がコンストラクタで行われ、その内容を検出する必要がある 

9.1 いらただしいパラメータ

  • 引数の依存関係は複雑でオブジェクト生成が難しい
    • テストが用意されてないクラスのインスタンス生成ができるかどうかは、実際にテストを書いてみて必要なものがなにかをコンパイラに教えてもらえば良い
    • DBや外部サーバーとのコネクション等
      • インターーフェース抽出で擬装オブジェクトをつくる

p.122

  • テストコードvs.本番コード
    • テストコードが本番コードと同じルールに従う必要はない
      • テストコードはきれいに(書きやすく)保つべき

p.123

  • Nullを渡す
    • あるオブジェクトが生成しづらいパラメータを必要としたら、代わりに単なるNullを渡すことを考える
      • Nullポインタエラーを検知できる言語においては、例外を補足してくれる
        • そのオブジェクトが必要になった時点で必要なオブジェクトを生成し、パラメータとして渡すように変更すればよい

p.124

  • Nullオブジェクトパターン
    • プログラムでNullを使用することを避ける方法
      • 呼び出し側で明示的なエラーチェックが不要になる
        • 呼び出し側で操作成功を確認する必要がない場合は便利
          • 使用には注意が必要

9.2 隠れた依存関係

  • ex. コンストラクタの初期化リストの中でnewを使ってオブジェクトを割り当てている
    • 解決策
      • コンストラクタのパラメータ化 
        • getメソッドの抽出とオーバーライド
        • Factory Methodの抽出とオーバーライド
        • インスタンス変数の入れ替え

9.3 複雑な生成

  • ex. コンストラクタ内で複数のオブジェクトを生成していたり、多くのグローバル変数にアクセスしていたりするとき
    • 解決策
      • Factory Methodの抽出とオーバーライド
      • インスタンス変数の入れ替え

9.4 いらただしいグローバルな依存関係

  • シングルトンデザインパターン 
    • システムの中でクラスのインスタンスを1つだけにしたい
      • 現実世界をモデリングした結果、実際には1つしかないものである場合
      • 2つ存在すると重大な問題になる場合
      • 2つ作ると多くのリソースを使いすぎる場合

第10章 このメソッドをテストハーネスで動かすことができません

p.151

  • メソッドのテストを書くために必要な作業
    • メソッドにテストからアクセスできない
      • メソッドがprivateになっている場合や可視性に問題がある場合など
    • メソッド呼び出しに必要なパラメータの生成が困難
    • メソッドのに悪い副作用(DBの更新など)がある
    • メソッドを使ういくつかのオブジェクトについて事前に検出を行う必要がある

10.1 隠れたメソッド

p.152

  • クラスのメソッドがprivateだったとき
    • publicメソッドを通じたテストが可能か検討する
      • publicメソッドを通じたテストを行うことで実際のコードと同じ方法によるテストが保証できる
    • 上記が可能でない場合
      • そのprivateメソッドはpublicにすべき
        • 大抵の場合はそのクラスが多くのことをやりすぎている、再設計が必要であることを意味している
  • privateメソッドをpublicメソッドにすることについて悩む点
    • メソッドが単なるユーティリティで、呼び出し側が気にかけるものでない
      • →そのメソッドを別クラスに移せないか検討する
      • →クラスのインターフェースに余分なpublicメソッドがあることは許容可能
    • 呼び出し側がそのメソッドを直接使った場合、そのクラスの他のメソッドの結果に悪影響を及ぼす可能性がある
      • →そのメソッドを別クラスに移し、移動先のクラスでpublicにする
  • 良い設計とはテストが可能であり、悪い設計はテストが不可能

10.3 検出できない副作用

p.162

  • コマンドとクエリーの分離(Command/Query Separation)
    • メソッドはコマンド、またはクエリのいずれかであり、両方にすべきではない
      • コマンド
        • オブジェクトの状態を変更できるもの、値を返さないメソッド
      • クエリー
        • オブジェクトの状態を変更しないもの、値を返すメソッド
    • 副作用を引き起こすメソッドかどうか理解しやすい設計

第11章 変更する必要がありますが、どのメソッドをテストすればよいのでしょうか?

p.167

  • 仕様化テスト
    • 既存の振る舞いを明らかにするテスト
      • 変更作業の前に現在の振る舞いを保護するために行う

11.1 影響の調査

p.171

  • 影響スケッチ
    • 影響を受ける変数と、戻り値が変わる可能性のあるメソッドについて図式化したもの
      • 更新されるものは楕円で囲み、それによって実行時に値が変わり得るすべてのものに向かって矢印を書く
    • 影響スケッチの構造が単純であれば、コードが十分に構造化されている、はず

11.3 影響の伝播

p.179

  • 影響が伝播する基本的な3つの方法
    • 呼び出し側によって使われる戻り値
    • パラメータとして渡されるオブジェクトの変更
    • staticデータ、あるいはグローバルなデータの変更
  • 影響を調査する場合の著者の経験則
    • 変更対象のメソッドを特定する
    • そのメソッドに戻り値がある時は、そのメソッドを呼び出すコードを調べる
    • そのメソッドが何らかの値を更新するかどうか調べる。更新する場合、その値を使うメソッドと、さらにそのメソッドを使うメソッドを調べる
    • インスタンス変数やメソッドの利用者になり得るスーパークラスとサブクラスも確認する
    • そのメソッドに渡されるパラメータを調べる。変更仕様としているコードがパラメータや戻り値のオブジェクトを更新していないかを調べる
    • 特定したメソッドのいずれかでグローバル変数やstaticデータを更新していないか調べる

11.6 影響スケッチの単純化

p.185

  • 影響とカプセル化
    • カプセル化
      • 依存関係を排除する手法はカプセル化を壊す
      • カプセル化はコードを調査するために役立つ

第12章 1ヶ所にたくさんの変更が必要ですが、関係するすべてのクラスの依存関係を排除すべきでしょうか?

p.187

  • 上位レベルのテストはリファクタリングに役立つ
    • 単体テストを整備するための最初の一歩とすべし

12.1 割り込み点

p.188

  • 割り込み点
    • 特定の変更による影響を検出できるプログラム上の場所で、テストかく部分を決める判断材料となる
    • 大抵の場合は変更しようとしているクラスのpublicメソッドになる
    • 変更点に近い箇所のpublicメソッドを選ぶと良い

p.195

  • 絞り込み点
    • 影響スケッチの集約された箇所
    • 少数のメソッドに対するテストにより多くのメソッドの変更を検出できる場所
    • 割り込み点の中で最もテストを書くべき場所である

第13章 変更する必要がありますが、どんなテストを書けばよいのかわかりません

  • レガシーコードでバグを見つける
    • 開発チームが一貫して正しいコードをかけるようにすることが有効
      • 自動化テスト
        • 直接的にバグを見つけるのが本来の目的ではない
          • 達成したいゴールを仕様化するか、既存の振る舞いを維持するためのもの
          • ソフトウェア万力
            • 変更が必要なときに、変更したい部分のコードを支えながら改善する

13.1 仕様化テスト

p.200

  • 仕様化テスト
    • 振る舞いを維持するためのテスト
      • 現在の振る舞いを文書化するものであって、「こうするべき」「こうしていると思う」といった仕様そのものではない。
    • 手順
      • テストハーネスの中で対象のコードを呼び出す
      • 失敗するとわかっている表明を書く
      • 失敗した結果から実際の振る舞いを確認する
      • コードが実現する振る舞いを期待するように、テストを変更する
      • 以上の手順を繰り返す
    • 現在のシステムの振る舞いをベースにテストが通るようにテストかくようなイメージ?
    • 仕様化テストを書く
      • 予期しないことを見つけたら、それを明確にすることに価値がある(バグなのか、仕様なのか)
  • メソッドの使用規則
    • レガシーシステムでメソッドを使う前にテストがあるかどうか調べておくこと
      • テストがなければテスト書く

13.2 クラスの仕様を明らかにする

あるクラスが何をすべきか特定したいときに役立つ経験則

  • 特に入り組んだ処理を探す
    • コードを理解できない場合は、検出用変数(デグレ検出用のグローバル変数)を導入する。
  • クラスまたはメソッドの責務を特定できたら、失敗する可能性のある事柄の一覧を作る
    • その失敗を引き起こすテストを作成できるかどうか検討する
  • テストに与える入力値を検討する
    • 特別な値を与えるうと何が起きるか検討する
  • オブジェジェクトのライフサイクルを通じて、常に成立する条件があるかを確認する(不変条件)
    • 不変条件を検証するためのテストを書いてみる

13.3 狙いを定めたテスト

p.207

  • 条件分岐のテストを書くとき
    • 分岐を実行せずに、テストをパスしてしまう可能性がないか考える
      • 確信が持てない場合は、検出用変数かデバッガを使う

13.4 仕様化テストを書くための経験則

  • 変更する部分のためのテストを書く
  • 変更したい具体的な事柄について調べ、それに関するテストを書く
  • 機能の抽出、移動をする場合、既存の振る舞いや振る舞い間の関係を検証するテストを個々に書く

第14章 ライブラリへの依存で身動きが取れません

  • ライブラリのクラスへの直接呼び出しをコード内に分散させてはいけない
    • ライブラリの変更(依存)を避ける

第15章 私のアプリケーションはAPI呼び出しだらけです

p. 219

  • API呼び出しが多いシステムのテストパターン
    • APIをラップする
      • APIをラップすることでAPIへの依存を排除することができる
        • APIが比較的小さいとき
        • サードパーティのライブラリへの依存を完全に分離したいとき
        • テストがない、あるいはAPIを通じたテストが不可能なためテストを書くことができないとき
    • 責務を元に抽出する
      • 責務を元にAPIから必要なモノを抽出する
        • APIが複雑であるとき
        • 安全なメソッド抽出をサポートするツールがあるか、手動での抽出を安全に行う自身がある時

第16章 変更できるほど十分に私はコードを理解していません

16.1 メモを取る/スケッチを取る

p.224

  • コードを読む際に絵を書いたり、スケッチを取る

16.2 印を付ける

p.225

  • コードを印刷して印を付ける
    • グルーピングしたり、線を引いたりする

16.3 試行リファクタリング

p.226

  • テストコードを忘れてコードをいじりながらコードを理解していく
    • 変数を移動したり、メソッドを抽出したり、リファクタリングをしてみる

第17章 私のアプリケーションには構造がありません

  • チームがアーキテクチャを理解していない
    • システムの複雑性故に、全体像を把握するのに時間がかかる、または全体像が存在しない
    • 緊急の差し込み対応に追われ続け、全体像を見失っている
    • →アーキテクチャの退化

p.239

  • アーキテクチャの設計方針や発見したことについてコミュニュケーションを取るz

第18章 自分のテストコードが邪魔になっています

p.243

  • 本場コードとテストコードを一緒に並べることで、ディレクトリ構造を上下したりする余計な手間がかからない
    • 本番コードとテストコードを分離する場合は理由を明確にする
      • ファイルサイズ
        • 本番コードとテストコードを一緒に配置することによるファイルサイズの増化を許容できるか
          • 商用製品である場合など許容できないとき
            • コードの参照しやすさを検討しつつ、分離するして配置することを検討する

第19章 私のプロジェクトはオブジェクト指向ではありませんが、どうすれば安全に変更できるでしょうか?

p.259

  • 手続き型言語はオブジェクト志向言語ほど多くの選択肢を持たないが、手続き型のレガシーコードを改善することは可能
    • 絞り込み点を探して、リンク接合部を使って依存関係を必要なだけ排除し、コードをテストハーネスにいれる
    • 後継となるオブジェクト志向言語あれば移行することを推奨する

第20章 このクラスは大きすぎて、もうこれ以上大きくしたくありません

p.262

  • 単一責務の原則(single responsibility principle:SRP)
    • すべてのクラスは単一の責務を持つべき
      • クラスはシステム内で単一の目的を持つべきであり、クラスを変更する理由は1つだけにするべきである

20.1 責務の把握

p.265

  • 経験則その1 メソッドを分類する
    • 名前が似ているメソッドを探す
      • アクセス属性と一緒に書き出し、まとめられそうなものを探す
  • 経験則その2 隠蔽されたメソッドを調べる
    • privateメソッドとprotectedメソッドに注意する
      • そのようなメソッドが多い場合はそのクラスから別クラスを取り出せる可能性がある
  • 経験則その3 変更可能な決定事項を探す
    • すでに決定済みの事項を探す
      • データベースへのアクセスや他オブジェクトの利用方法など何かを行うための何らかの方法として、ハードコードされているものがあるか、それらが変更される可能性が考慮できるか
  • 経験則その4 内部的な関係を探す
    • インスタンス変数とメソッドの間の関係を探す
      • ex. あるインスタンス変数が一部のメソッドでしか使われていないのでは?
  • 経験則その5 主要な責務を探す
    • クラスの責務を1分で説明する
  • 経験則その5 他のすべての経験則が役立たない場合、試行リファクタリングを行う
    • クラスの責務を見つけ出しづらいときは試行リファクタリングを行う
  • 経験則その6 現在の作業に集中する
    • 今やるべきことは何か

p.278

インターフェース分離の原則(Interface Segregation Principle:ISP)
  • システム内での依存関係を弱めるために、インターフェースを定義することで情報隠蔽(カプセル化)を行う

第21章 同じコードをいたるところで変更しています

p.289 どこから始めるか判断する

  • 複数のリファクタリングを行い、重複を取り除く際、最初に手をつける場所によって構造が変わってくる
    • ex. メソッドの分解
      • 分解パターンによって構造的に変更がない場合、文脈に合うパターンを採用する
        • 小さなことから始めていく

21.1 最初のステップ

p.303

  • 直交性
    • 非依存性
      • コードの元々の振る舞いを変更する際、変更する必要の箇所が1つだけだった場合、直交性がある、という
  • コードの重複を徹底的に取り除いた先に設計が姿を現す
    • →優れたコードには重複が少ない、というよりほぼないような気がする

p.304

  • 開放/閉鎖原則(Open/Closed Principle:OCP)
    • コードは拡張に対しては開いていなければならず、修正に対しては閉じていなければならない

第22章 モンスターメソッドを変更する必要がありますが、テストを書くことができません

  • N/A

第23章 どうすれば何も壊していないことを確認できるでしょうか?

  • N/A

第24章 もうウンザリです。何も改善できません

  • N/A

第3部 依存関係を排除する手法

第25章 依存関係を排除する手法

  • N/A

付録A リファクタリング

  • N/A

付録B 用語集

  • N/A

About the author

Image

bmf san @bmf_san
A web developer in Japan.