スポンサーリンク
概要
「レガシーコード改善ガイド」を読んだので感想等を書こうと思います。
テストの無い、変更が困難なコードに対してどのようにコードを追加したり、リファクタリングしていけば良いかが書かれている本です。
スポンサーリンク
スポンサーリンク
書籍概要
どんな本?
本書は、システム保守の現場でありがちな、構造が複雑で理解できないような
コードに対する分析手法・対処手法について解説します。
つまり、「コードを理解し、テストできるようにし、リファクタリングを可能にし、
機能を追加できるテクニック」を紹介している書籍です。本書には、以下のことが記載されています。
●仕様が分からないコードの分析方法
●仕様が分からないコードの修正方法、またテストコードの追加方法
●コードの修正で、疎結合な設計に部分的に改善する方法また、本書には、以下のことは記載されていません。
●COBOLなどで記述されているメインフレーム上のアプリケーションの改修方法【 対象読者】
●現行のシステムが仕様が分からず保守作業に悩む、保守担当者
●現行のシステムの修正作業は可能であるもののデグレーションに悩む、保守担当者
●疎結合な設計手法を知りたい技術者
- マイケル・C・フェザーズ 著、ウルシステムズ株式会社 翻訳、平澤 章 翻訳、越智 典子 翻訳、稲葉 信之 翻訳、田村 友彦 翻訳、小堀 真義 翻訳
- 2009年07月13日 発行
- 472ページ
- 定価4,620円(税込)
スポンサーリンク
スポンサーリンク
内容のまとめと感想
既存のコード(レガシーコード)に対してのテストの追加、リファクタリングの技法を中心にまとめた本です。
著者の定義による、レガシーコードとはテスト(xUnitによるテストコード)が無く、安全に機能追加や変更ができない(祈って修正する)状態のコードを指します。
コードの分析方法や分類など、どういった考え方でコードのテストを追加しつつ、リファクタリングしていけば良いかが説明されています。
コードはJavaとC++が使用されています。
私自身はJavaもC++もあまり使った事がないのですが、Javaのコードの方はC#などの静的型付け言語の経験ががあれば、違和感無く読む事ができました。
C++に関しては私が苦手といった点や、C++といった言語特有の場合の説明で使用されている点が多く感じて、ほとんど飛ばしてしまいました。
比率的にはJavaが7~8割くらいだと思うので、オブジェクト志向の静的型付け言語を読む事ができれば価値はあると思います。
多くのエンジニアが、長年使われてメンテナンス性が悪いコードを触るといった経験をされていると思いますが、そういった際にどうやって取り組んだら良いかといった事が学べるかと思います。
(ただ、実際のケースで既存のレガシーコードにユニットテストを追加しつつ、コードをばっさりとリファクタリングできるのかといった事は実現性が難しい問題ですが・・・)
良かった点
JavaとC++による実際のコードを使って、どのようにリファクタリングをしていけば良いか説明されていて、イメージがしやすくなっています。
レガシーコードに対する改善という話にはなっていますが、新規でコードを作る際にも役立つと思います。
例えば、何も考えずにざっくりとコードを書いて、それを後からリファクタリングをしてコードをまとめていくといった事はよくやると思いますが、そういった際にもコードをどのように整理していくか?といった方法を考えるのに役立ちます。
出版されていて10年以上経っていて、コード自体やツールの説明に一部に古く感じる場所などはありますが、基本的な考え方を学ぶには問題ないと思います。
気になった点
翻訳本においては宿命なのかもしれませんが、読みにくかったり冗長に感じる部分が結構ありますね。
また、本の構成として「XXに関してはYYの章で説明する」といった、何度も章やページの参照させる記述が多く、読んでいてめんどくさくなる点が多くありました。
もう少し構成とか、書き方を工夫して読みやすくしてくれると助かるなぁ・・・というのが正直な感想です。
その他には、「XXXの場合どうしたら良いか?」といった質問や問題ケース毎に説明を書いているせいか、前のページで似たような内容や方法を見たような気がして、冗長と感じる点が多くありました。
スポンサーリンク
読書ノート(個人的なまとめ)
第1部 変更のメカニズム
ソフトウェアの変更のメカニズムに関して書かれているブロックです。
何故、プログラムの変更において単体テストが重要なのかに関して説明がされています。
レガシーコードにおいて、このような単体テストを導入する際に一番の問題となる、依存関係とその解消策が書かれています。
この辺りはオブジェクト指向プログラミングの王道的な処理の委譲とDIなどによるクラスの抽象化に関係する話で、結局のところこういった現在では当たり前の作法というのはテストをしやすくするためにあるのだなとわかります。
- ソフトウェアの変更
- ソフトを変更する理由は、機能追加、バグ修正、リファクタリング、最適化がある
- 既存の振る舞いは殆ど残ったまま、新しい振る舞いを追加していく事が殆ど
- 既存の振る舞いを変えずに保つことは困難
- 変更を行う方法
- 編集して祈る(注意深く変更する。非常にリスクが高く、高スキルが必要)
- 保護して変更する(テストで保護する。問題があったらすぐ気づけるフィードバックを用意する)
- 開発中に素早いフィードバックが得られることで安全にリファクタリングが得られる
- 単体テストのメリット
- エラー箇所の特定が容易
- 実行時間が短い
- カバレッジ
- 大規模なテストでは埋められないギャップを埋めてくれる。
- 優れた単体テスト
- 実行が早い(0.1秒かかるなら遅い)
- 問題箇所の特定しやすい
- DBやネットワーク、ファイルに依存するテストは遅いので単体テストでは無い。ただし価値はあるので切り分けておくこと。
- レガシーコードにおける単体テスト
- 依存関係が複雑で実施が難しい。これを排除する必要がある。
- 偽装オブジェクト
- 依存関係のあるクラスに対してインタフェース作り、差し替え可能にする
- さらに進化させて、呼び出し内容をチェックするモックオブジェクトもある
第2部 ソフトウェアの変更
様々なユースケースにおいて、どのようにレガシーコードに手を加えていけば良いかを説明しているブロックです。
「時間がないのに変更しなければなりません」といった、わかりやすいテーマごとに説明がされていて面白いです。
テーマ数が20を超えて非常にボリュームがあり、かなり読み応えがありますね。
具体的なコードやクラス図が多く含まれていてわかりやすくなっているのも特徴ですね。
- 時間がないのに変更しなければなりません
- 保守性を上げるために追加で必要になる作業(依存関係の排除、テストを書く)の時間に価値はある?
- ほとんどの場合には最終的に時間の節約となる
- スプラウトメソッド、クラス
- 仕様の追加部分を既存メソッドに実装せずに、別のメソッドやクラスに切り出して実装する。(既存メソッドはその結果のみを使用する)
- クラスであれば、対象のクラスからコンストラクタで渡す(IFで)
- 少しの新機能のためにクラスを追加することは別にばかげたことではない
- ラップメソッド
- 既存メソッドには何も手を加えず、新しいメソッドを作る
- 既存メソッド、新しいメソッドをそれぞれ呼び出す別のメソッドを用意する
- 既存のメソッドの処理の前後に処理を追加する方法でのみ使用可能
- ラップクラス
- 既存クラスをインタフェースとして抽出する
- 同じIFを実装したクラスを用意し、元のクラスをコンストラクタで受け取り、元のクラスの処理の前後に追加したい処理を入れる
- デザインパターンのデコレーターパターンに該当する
- どうやって機能を追加すればよいのでしょうか?
- TDD(テスト駆動開発)による機能追加が良い
- 失敗するテストケース作成 -> コンパイル -> テストPASSさせる -> 重複を取り除く -> 以下繰り返し
- TDDでの大きな価値は、1度に1つの事に集中できること。(コードを書いているかリファクタリングしているかのどちらか)
- このクラスをテストハーネスに入れることができません
- テスト対象が簡単にオブジェクトを生成できない、コンストラクタが副作用を持つ、処理が実装されている等でテストがそもそもできない場合
- コンストラクタで渡されるクラスが副作用や依存性を持つ場合には、そのクラスのIFを抽出して、擬似オブジェクト(モック的なもの)を作成可能にする
- nullを渡しても動くならnullを渡すのも手(ただし、あくまでテストでのみnullを渡すべきで、本番コードではそいうのはNG)
- シングルトンパターンへの対応
- インスタンスを設定可能なテスト用のメソッドを追加+コンストラクタをpublicにしてしまう
- publicにするのが許されないならば、コンストラクタをprotectedにして継承で差し替え可能にする
- もしくはシングルトン自体の実装を全てIFでやり取りするようにする。
- このメソッドをテストハーネスで動かすことができません
- テスト対象のメソッドがテスト時に動かすのが難しい(privateメソッド、パラメータが複雑、副作用を持つなど)
- 深いprivateメソッドをテストしたい => publicメソッドにすることを検討すべき
- publicメソッドにする弊害(例;他のメソッドとの呼び出し順に依存する)があるならば、別のクラスに切り出してpublicメソッドとしてテストすべき
- テスト対象のメソッドがテスト時に動かすのが難しい(privateメソッド、パラメータが複雑、副作用を持つなど)
- 変更する必要がありますが、どのメソッドをテストすればよいのでしょうか?
- 変更による影響を考慮してどのようにテストを書いたら良いのか?(呼び出し元や先に影響を与える場合など)
- 影響スケッチによる影響の可視化(変数とメソッドを図にして、変数の変更でメソッドの戻り値がかわるものを矢印で結ぶ)
- メンバ変数の更新方法の変更による影響調査:変数を起点として戻り値に変更を与えるメソッドを影響スケッチする
- メソッドの変更による影響調査:メソッドの内で変更するメンバ変数を起点として、そのメンバを参照するメソッドを洗い出す
- 変更による影響を考慮してどのようにテストを書いたら良いのか?(呼び出し元や先に影響を与える場合など)
- 1箇所にたくさんの変更が必要ですが、関係する全てのクラスの依存関係を排除すべきでしょうか?
- 1箇所でまとめてテストできるポイントを探す(もしくは実装する)
- privateメソッドをテストするのではなく、publicメソッドからまとめてテストする(privateのテストはリファクタリングを困難にする)
- メンバ変数の更新方法の変更による影響調査:変数を起点として戻り値に変更を与えるメソッドを影響スケッチする
- メソッドの変更による影響調査:メソッドの内で変更するメンバ変数を起点として、そのメンバを参照するメソッドを洗い出す
- 割り込み点(多くのメソッドの集約される場所)
- 影響スケッチで関係を示し、呼び出しのルート的な場所を見つける
- その場所を起点にテストすれば、全てのメソッドに関してテストが可能
- 1箇所でまとめてテストできるポイントを探す(もしくは実装する)
- 変更する必要がありますが、どんなテストを書けばよいのかわかりません
- レガシーコードでバグを見つけるためにテストを書くことは基本的にしない(非効率)
- 仕様化テストにより、テストコードとして仕様を明らかにしていく事が効果的
- 仕様化テスト:失敗するテストを書く -> 失敗時に本来の結果を確認する -> その結果を期待値にしてテストをパスさせる
- 仕様化テストにバグを検出する効果はない
- 1:仕様を明らかにするドキュメントとしての効果
- 2:将来的な振る舞いの変更を検知するため(=>リファクタリングするためのガードとして使用する事も可能)
- もっとも良い仕様化テスト:実行パス内の全ての値変換処理を実行するもの
- このクラスは大きすぎて、もうこれ以上大きくしたくありません
- 小さなクラスに分割を行うもしくは、前述のスプラウトクラスで新機能を実装する
- 単一責務の原則に沿ってクラスを単一責務にする
- 責務をどのように見つけて分割すれば良いか?
- メソッドを分類する => 似た名前のメソッドを探しだしてグルーピングして抜き出せないか検討する
- 隠蔽されたメソッドを調べる => privateやprotectedメソッドが別のクラスとしてpublicメソッドとして公開できないか?
- 内部的な関係を探す => インスタンス変数とメソッドの関係を探し、一部のメソッドでしか使われていない変数があれば、一緒に別のクラスに切り出すか検討(調査方法としては前述の影響スケッチが効果的)
- クラスの責務を1文で説明してみる
-
- 複数の責務があれば、別のクラスに責務を移譲
- 責務の違反では、実装レベルの違反とインタフェースの違反がある
- 実装レベルの違反:実装が全て同一クラス内にあるもの。責務に応じて分割する。
- インターフェースの違反:実装は別のクラスに委譲されているが、publicインタフェースがたくさんある
- インタフェースの分離の原則に沿って分割を行う(クライアントが全てのIFを使用することは滅多にない。使用されるメソッド群ごとにまとめたクラスを実装する=> Facadeクラス的な実装でXXXControllerみたいな上位のクラスを作る
-
- 同じコードをいたるところで変更しています
- 重複コードだらけの対策
- 抽出した重複部分を取り除いていく
- 基底クラスを作って重複部分を基底クラスに移していく
- 継承先で情報が変わるものはAbstractメソッドとする
- 情報として可変のもので同じ振る舞いならばArrayやListの塊としてまとめて基底で定義できないか検討
第3部 依存関係を排除する手法
レガシーコードにおいてテストを可能にしたり、リファクタリングを行うためのクラス間の依存関係を減らすための手法が書かれています。
ただ、この章で説明されている内容の多くは前のブロックで説明されているものの繰り返しで、正直冗長で退屈でした。