Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【感想】レガシーコード改善ガイド 【前編】

レガシーコードに立ち向かうための改善ガイド

 こちらもIT関係でよくおすすめ書籍に必ず顔を出す名著のひとつ。大型本で472ページとがっつり分厚く、2009年とちょっと古めですが今でも役立つ本です。
 レガシーコードというと何年も保守されて継ぎ接ぎだらけになったコード、中が謎で誰も触らずに放置されてきたコード、設計書と整合がとれておらず仕様がよく分からないコード、メンテしにくいコード、COBOLなど古い言語で書かれたコード、JavaStrutsなど古いフレームワークで書かれたコードなど状況に応じて様々な(殆ど全ての場合マイナスな)意味を持ちますが、本書ではテストで保護されていない扱いにくいコードをレガシーコードと定義し、ソースのリファクタリングやテスト方法、調査や対処の手法など、レガシーコードに立ち向かうための様々な事柄を記しています。著者はマイケル・C・フェザーズ氏。

 ちなみにぐぐると感想記事などでも一緒によく出てくる『レガシーソフトウェア改善ガイド』は名前も表紙も似てるのでシリーズかと一見勘違いしやすいですが、実は2016年刊行で違う人の書いた全然別の本なんですね。
 この『レガシーコード改善ガイド』はあくまでレガシー「コード」にフォーカスしてクラスやメソッドをどうリファクタで改善して行くかの話。『レガシーソフトウェア改善ガイド』はよりスコープが広く、方法論やインフラ、マネジメントや人間系の話も扱った本となっています。

レガシーソフトウェア改善ガイド (Object Oriented Selection)

レガシーソフトウェア改善ガイド (Object Oriented Selection)

 各章のタイトルは下に出てくるように現場の切実な状況を表していたり手法に名前をつけていたり、なかなか面白いですね。このへん『SQLアンチパターン』と通ずるものがあるかもしれません。 iwasiman.hatenablog.com

以下、各章の要約や感想などを。

第1部 変更のメカニズム

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

 リリースして動いているソフトウェアを変更する理由には要件追加、バグ修正、リファクタ、最適化の4つがある。そして一般的に人間はレガシーなコードの変更を避けがちだがそれは賢明ではない…と話は始まります。

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

 そして有名な、編集して祈る(Edit and Pray)はダメで保護して変更する(Cover and Modify)が良いという話。本書ではテスト用コード類一式を「テストハーネス」と呼び、このテストハーネスで対象のレガシーコードを保護し、フィードバックを得ながら徐々にリファクタリングしていくのが良いのだ、としています。
 単体テストは実行が速く問題箇所の特定がしやすいテストが望ましく、0.1秒程度で実行できるもの。DBとのやりとり、ネットワーク、ファイルシステムなどは除外してよいと定義しており、なんでも実際の動き通りに動かすのではなくてロジック部分にフォーカスする考え方なんですね。コードの変更手順は変更点を洗い出す、テストを書く場所を見つける、依存関係を排除する、テストを書く、変更とリファクタリングを行うの5ステップ。

 そしてテストし、リファクタリングしていく上での最大の問題は「依存関係」。お馴染みのクラス図でメソッドの引数を減らし、クラス間の関係を表す線を減らしていく方法を基本的な例を取り説明しています。この「依存関係の排除」こそが本書全体を通しての最大のテーマであり、様々なテクニックが紹介されています。

第3章 検出と分離

 コードの計算結果にアクセスできない時にテストコードからそれを検出するために依存を排除する方法、そもそもテストが実行できない時には依存を分離して排除する方法。
 ネットワーク管理をするクラス NetworkBridge を題材にとり、擬装オブジェクト (Fake Object) として本物の代わりに処理をするクラスを紹介しています。画面表示をする本物クラスがDisplay、擬装オブジェクトがFakeDisplayで実際の大変な画面表示の代わりに標準出力するだけで、これらの呼び出し側では型はインターフェースにしてどちらでも呼べるようにする、という方式ですね。
 この擬装オブジェクトを進化させたのがモックオブジェクト (Mock Object) で、こちらはメソッド呼び出し後に検証を指示できるものとしています。差がいまいちよく分からなかったのですが、今ならxUnitなどでの定義された言葉としてのモックを指すのが普通でしょうか。

第4章 接合モデル

 テストの容易性に配慮した設計として、クラス間で分離した設計が重要であるという話。

  • 接合部 (Seam):その場所を編集しなくてもプログラムの振る舞いを変えられる場所
  • 許容点 (Enabling Point):どのふるまいを使うか決定できる場所

 ここでは例がC++ですが、テスト対象メソッド内でどうしても実行できない厄介な内部のメソッドをテスト用の実装クラスで再定義し、振る舞いを変える方法を紹介しています。
 接合部の種類としては以下があります。2018年現在だと、3つ目の「オブジェクト接合部」がメインになるでしょうか。

  • プリプロセッサ接合部:CとC++専用、プリプロセッサを活用して振る舞いを変える。
  • リンク接合部:Javaで、クラスパスの値を変えて、クラス名が同じだがテスト用の別クラスを読み込んでテストに使う。
  • オブジェクト接合部:一番使える方法。インターフェースで定義したメソッドがテスト対象として、継承したクラスを変えればメソッドの内容も変わるのでテスト用の子クラスでオーバーライドして活用する。テスト対象メソッドはstaticはやめてインスタンスメソッドに変更し、スコープがprivateの場合もprotected に変更する。

第5章 ツール

 自動リファクタリングツールやモックオブジェクトのライブラリもあるよという話。単体テストハーネスとしてはJUnit、そして本書の作者さんが作ったC++用のCppUnitLiteというツールが紹介されています。英語版の本書執筆時の2004年ごろの話なのでこのへんは情報が古いですね。
 参考文献としてはかの有名なマーチン・ファウラー本の『リファクタリング』が登場、この本は度々名前が出てきます。

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

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

 タイトル通りの厳しい状況下で、いつ時間をかけてどうしていくと良いかという話。

スプラウトメソッド:

 要件が追加されたら既存メソッドのど真ん中に処理を追加するのでなく、新しいメソッドに書いて既存メソッドからは呼ぶだけにする。元のレガシーなメソッドと区別がし易い。新しいメソッドにたくさん値を引数で渡す必要がある(=依存関係がある)時に苦しくなる。

スプラウトクラス:

 新規要件部分を新しいクラスの新しいメソッドに書いていく。完全に独立してテストできる。クラス作成の原則(単一責務原則のSRPとか)に従わずに作っていった場合、クラスが増えて仕組みが複雑になることもある。

ラップメソッド:

 例えばあるメソッドの pay() の振る舞いを変えたい場合、元の pay()private dispatchPayment() などとリネームして退避、新規に pay() メソッドを作ってその中から古い dispatchPayment() を呼ぶようにする。既存機能と新しい機能を明確に独立させられる。新メソッドに変な名前をつけてしまいがち。

ラップクラス:

 例えば元のクラス Employee と同じインターフェースを持ったラップクラス LoggingEmployee を追加、その中のメンバ変数に元クラスの Employee を持って各メソッドの処理で元クラスの処理を内部で呼んだり新規処理を追加したり、元クラスをまるごとラップで包み込む。GoFデザインパターンの Decorator パターン。既存クラスに変更がまったく入らずに済む。

 スプラウト(Sprout) は英語の新芽で、スーパーに行くと売ってるかいわれ大根の仲間みたいなやつですね。別冊フレンドの少女漫画でもこのタイトルのがあったような。
要はクラス名やメソッド名は適当でもいいからとにかく旧レガシーコードと引き離してテストで保護されたクリーンなコードの王国の保護下に移動させて動かすという考えですが、このへんは実戦でもすぐ使えそうです。

 参考文献としてはかのGoFデザインパターン本、そして古いですが『ケント・ベックSmalltalkベストプラクティス・パターン』が挙げられています。

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

 これまたタイトル通りの心が折れそうな時にどうするとよいかの話。

  • 適切な名前の付いた小さいパーツに分けていくと理解しやすい。
  • 何かの実行にすごく時間がかかるなら、本書のテクニックを使って(DBアクセスはダミーの値を返すなど)、短い時間で単体テストできる仕組みを作る。
  • 依存を徐々に排除していくとビルドも早くなる。また、極力具象クラスに依存せず、インターフェースや抽象クラスに依存するようにする。『依存性逆転の法則』 (Dependency Inversion Principle: DIP )
     インターフェースとパッケージが増えると全体ビルドの時間は増えてしまうが、必要なものだけの再コンパイル時間が劇的に減っていく。

 『Code Complete』などでもよく出てくる、「分割して複雑さに対処していく」ということですね。本章の「依存性逆転の法則」にあるように、インターフェースと抽象クラスを使ったテクニックは本書を通じてよく出てきます。
 参考文献としてはちょっと古いですが『アジャイルソフトウェア開発の奥義 第二版』が挙げられています。

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

「一般的に、獣に対しては隠れるよりも立ち向かうほうが良い対処法です」と読者を勇気づけ、タイトルのような状況でもコードをテストで保護しながら進む方法を述べています。

テスト駆動開発

 TDDの一番基本の、最初は失敗するテストケースを作ってから実装を追加、テストを通過、重複があったら削除してまた…と段階的に進める方法。例がステップごとに書いてあって分かりやすい。
 テスト駆動開発は一度に1つのことに集中して、コードを書くかリファクタかに分離できるのがメリット。レガシーコードの場合は古いコードと分離して作業できるのが良い。

差分プログラミング:

 新機能の差分の分をどんどん継承のサブクラスで追加していく古典的な方法。継承を使いすぎるので90年代からはあまり使われなくなった。
 『Liskovの置換原則』(The Liskov Substitution Principle: LSP) :サブクラスは常にスーパークラスの代わりに使えなければならない、に違反しがちである。

 参考文献にはケント・ベック本の『テスト駆動開発』が挙げられています。

テスト駆動開発

テスト駆動開発

  • 作者:Kent Beck
  • 発売日: 2017/10/14
  • メディア: 単行本(ソフトカバー)

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

 タイトル通りの、テストしづらい厄介なクラスをどうテストしていくかの話。

テスト対象メソッドに重い処理をする引数がある:

 例では謎のあちこちと接続しに行きそうで重そうな RGHConnection 型クラスを取り上げ、インスタンス生成ができるかはまず実際にnewして試して確認。対象のインターフェースを抽出し、このインターフェースを実装した擬装クラス(例ではFakeConnection)をテストのときは使うようにして工夫する。
 Validator系のクラスにはNullを渡す方法もある。Nullを表すクラスを作るNullオブジェクトパターンもある。これは呼び出し側で操作が成功したか気にしなくて良い場合に有効。

隠れた依存関係:

 コンストラクタ内でメンバ変数をnewするなど重い処理を別メソッドの initialize() に追い出すパラメータ化で対応。他にget()メソッドをオーバーライドするなど。

複雑な生成:

 中で処理をするインスタンスを外側からset()メソッド経由で変えられる「インスタンス変数の入れ替え」テクニックを使う。

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

 Javaならグローバル変数はないですが、アプリケーション全体で常に唯一の値がテスト環境では邪魔になるなら、Singletonクラスは継承したサブクラスで setInstance() のようなメソッドをオーバーライドし、インスタンスの入れ替えで対応する。

恐るべきインクルードの依存関係:

 C++で、フェイクのヘッダファイルをインクルードして工夫する。

玉ねぎパラメータ:

 コンストラクタで面倒なクラスを引数に取らないと進めない場合。JavaならNull渡し、インターフェースを定義して別のサブクラスを擬装オブジェクトで引数に渡すなど。C++だと言語仕様でまた変わる。

別名のパラメータ:

 テスト対象コードの引数に面倒な OriginationPermit クラスがいて、テスト対象メソッドの中で validate() メソッドで余計な処理をするなら、それを継承した FakePermit クラスを代わりに引数に渡し、こちらの validate() では常に true を返すなど。

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

 テスト対象メソッドにアクセスできなかったり、パラメータが面倒だったり、扱うオブジェクトに検出が必要だったり、メソッドを実行すると何か副作用がある場合のテクニック。やってほしくない副作用の例として「DB更新」や「ミサイル発射」と、こっそりギャグを入れています。

隠れたメソッド:

 スコープが private でアクセス出来ない場合。テストしなくてはいけないなら、protected や public に変えてしまう。(!)
 テストを通すことやレガシーコードの根本原因を取り除くほうが優先である。

言語の便利な機能:

 テストメソッドの引数がC#で sealed になっていて継承クラスが作れない場合。元の親のインターフェースを継承したクラスを自前で作って代用する、API系のクラスはラッパークラスを作って対応する。

検出できない副作用:

 テスト対象メソッドがイベント受け取りとロジックとGUI表示など、複数のことを一度にやっている場合。リファクタリングでメソッドを分離、クラスを分離したりして責務を分割、その後テスト可能にする。

 ここで言語仕様のJavaなら final、C#なら sealed はレガシーコード改善の上では「いらない」と言っているのが面白いですね。 格言系では『コマンドとクエリーの分離』(Command/Query Separation): オブジェクトの状態を変更するコマンドと、値を返すだけのクエリは別メソッドにする を例に出し、大事なのはまず理解しやすさなのだと論じています。

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

 他のメソッドに影響が出る可能性がある時、どう調べていけばいいかの話。

  • 影響の調査:重要な変数と重要なメソッドを丸で囲んで書いて矢印で繋げる「影響スケッチ」を書いてみる。フリー書式で良いので図にしてみる。
  • 前方向の調査:調査対象のクラスを利用するすべてのクラスを対象として、影響スケッチで書いてみる。どこかに関係のあるクラスが隠れているかもしれない。
  • 影響の伝搬:影響は対象メソッドの戻り値、引数で渡されたオブジェクトの変更、staticなデータやグローバルなデータの変更の3種類である。これらに着目して対象メソッドの周囲を調べていく。
  • 影響調査のためのツール:言語が用意している「防火壁」の仕組みを理解する。 Javaならprivate変数、C++は変数宣言に mutable がついていたら変更できてしまうなど。

 依存関係の排除でオブジェクト指向カプセル化が崩れることがあるのは認めています。しかしカプセル化よりなぜカプセル化が重要なのかの理由のほうが大事、テストによる保護の方が優先ならそうすべきで、カプセル化は理解のための手段なのだと断言しています。
 こうした、時にオブジェクト指向の原則周りに反してでもテストでレガシーコードに立ち向かっていく姿勢が貫かれており、とても実践的です。

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

 巨大なレガシーシステムで、とてもそんなことはできない場合どうするかの話。  特定の変更による影響を検出できる場所を割り込み点(interception point)として、これを影響スケッチの図と共に調べていきながらテストをするとよいと述べています。
 やはり一度に対応は無理なので少しづつテストで保護しながらリファクタリングを進めること、また図で整理する、ということですね。

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

  • 仕様化テスト(characterization test):仕様書もない場合、コードが実際にどう動くのかを振る舞いを確認するテストメソッドを書いていく。つまり現実装を仕様と定める。
  • クラスの仕様を明らかにする:特に入り組んだ場所を調査したりテストコードを書きながら、そのクラスの役割や仕様を理解していく。
  • 狙いを定めたテスト:リファクタリングで処理の分割や局所化を行ったら、そこを集中してテストする。

 満足な仕様書のないコード、保守フェーズに入って機能改修が続くうちに仕様書メンテが追い付かず仕様と実際のコードが食い違っちゃう話はありがちですが、アメリカのレガシーシステムでも同じなのだなと思います。この「仕様化テスト」という用語は日本ではあまり見ないですが、潔いですね。

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

 優れた設計のための言語機能はテストコードのために対立することもある。様々なテクニックで対抗していくのだという話。
 本書の執筆時点は2004年、世界はJavaと.NETで二分されている...と書いているあたりに時代を感じます。

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

 言語付属のライブラリなどを使ってAPI呼び出ししかしていないクラスは、構造を改善することが難しかったりテストが難しい事が多いのでどうするかという話。
 JavaMailを使ってメールの送受信や管理をしているメールサーバー的な1つのクラスを例に挙げています。最終的には責務を元に抽出して受信クラス、作成クラスや送信クラスに分割してテスト可能にしていくのですが、ステップ毎に改善されていくサンプルコードを示していて分かりやすいです。

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

 これまた正直なタイトルですが(笑)、こんな時どうするかの話。

  • メモを取る/スケッチを描く:UMLのような大層な記法に従う必要はなく、とにかく図にしてみる。
  • 印を付ける:クラスの責務をグループ化して色を分ける、コードブロックに矢印を書いて関係をたどる、抽出したいメソッドを丸で囲っていく、変更対象と影響を受けそうなところに印をつけて区別していく。
  • 試行リファクタリング:あくまで試しにリファクタリングしてみて、うまく行かなかったらリファクタしたコードは捨てる。
  • 使用していないコードを削除する:一見時間の浪費に見えるが、分かりづらいコードを調べていくときには整理になって役立つ。バージョン管理システムで戻せるので大丈夫。

 なんとなくコードの修正を一旦始めたらもう最後までやってコミットしなければという意識があるので、ローカル上で「試行リファクタリング」するという方法は新たな発見でした。

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

 oh...という感じのアプリケーションですが、やっつけ仕事でアーキテクチャがない、チームがアーキテクチャを理解していない場合どうするかの話。
 昔からこうした場合はアーキテクトという役割をチーム内に用意するのがセオリーだが、この人は日々一緒に仕事をしている人でコードが分かってる人、できる人でないとダメよとあります。やはり肩書きだけのアーキテクトは役に立たないんですね。

  • システムのストーリーを話す:設計について質問してみて、答える人も簡潔に答えながら実際はどうなんだっけと理解をしなおす。例として「JUnitアーキテクチャはどうなってますか?」の問答が載っている。
  • 白紙のCRCCRC: Class, Responsibility, Collaboration。クラス、責務、協調。1980年代に流行ったという記法。これを単に何も書いてない白いカードにカード=1インスタンスで書いていって話し合って理解する。
  • 会話の吟味:設計に関する会話に耳を傾ける。経験者でも意外なところで見落としたり新しいアイデアが出たりする。

 会話しながら答えを見つけていったり、このへんアジャイルっぽいアプローチです。

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

実際にテストを書いていくときの命名ガイドです。

  • テストクラスは末尾に Test を付ける。例:DBEngineTest
  • テスト用の機能側オーバーライドクラスなどは先頭に Testing を付ける。例:TestingCheckingAccount
  • 偽装オブジェクトは先頭に Fake を付ける。例:FakeTransaction
  • テストクラスは機能側コードのsource/srcディレクトリと同位置にtestディレクトリを作り、その中にパッケージごとに入れていく……

人間工学的に、一覧を並べた時に人間の目で見分けやすくするのが良いと述べています。
末尾に Test をつけたテストクラスなど、これは今でもxUnit系でも標準ですね。

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

 手続き型言語の場合、依存関係を排除するのが難しいがどうしていくかの話。

  • C言語でテストが困難な関数が中にある場合:「リンク接合部」のテクニックで、同じ名前で何もしない擬装関数をインクルードする。
  • 名前をTESTINGなどとしたマクロ定義で、困難な関数呼び出しを無効にする
  • 新しい振る舞いを追加するときは既存の関数の中でなく新しい関数に書く。長い長い関数にしないで中を分割し、TDDのアプローチでテストしていく。オブジェクト指向のほうが、取れるテクニックの数が多く、長所を活用できる。

 2018年現在から見ると古い話ですが、やはりオブジェクト指向への進化が発生したのはこの辺にも理由があるのだなと改めて思います。

(以下、続く)