Rのつく財団入り口

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

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

こちらのエントリの続きです。 iwasiman.hatenablog.com

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

 よくアレなコードの小噺で出てくるいわゆる神クラス、ゴッドクラスがすでに堂々と鎮座している場合にどうするかの話。
 まずは状況を悪化させないためにどう作業するか決める。機能追加ではまずスプラウトクラスかスプラウトメソッドのテクニックを使って追加分は分離。そして特効薬はやはりリファクタリングで、単一責務の原則:SRPに基づいて分割していくこと。
 本書では構文解析をする大きな RuleParser クラスをリファクタで最終的に6つに分割してく例を挙げています。

クラスの責務の把握については訓練が必要だとあり、以下のようにステップを分けて解説しています。

  1. メソッドを分類して似ている名前、スコープで分けていく。ここでグルーピングしたものが分割の単位になったりする。
  2. スコープがprivateやprotectedで隠蔽された内部メソッドがたくさんある場合、たいてい移動したりして取り出せる。
  3. 変更可能な決定事項を探す。同じAPIの呼び出しなど実は同じ処理をやっているなら、一旦メソッドに抽出すると分類がより簡単になることがある。
  4. 内部的な関係を探す。ごく一部のメソッドでしか使われていないインスタンス変数があったら、これもクラス分割のサイン。メソッドと変数名を○で囲い、中で呼んでいる変数/メソッドに矢印でつなげた「機能スケッチ」で整理して探っていく。例としてホテル予約の巨大な Reservation クラスを分割していく手順が載っている。
  5. 主要な責務を探す。クラスの責務を一文で説明してみて、違ったら抽出して分割していく。ここでインターフェース分離の原則(ISP: Interface Segregation Principle)が出てくる。クライアントはインタフェース経由で大きなクラスにアクセスするとよい。たいてい機能の一部しか使っていない。
  6. 経験則が役に立たなかったら、「試行リファクタリング」で試してみる。
  7. 現在の作業の集中する。たとえ成果が小さくても、責務の抽出に成功したりして前へ進んでいる。

 その他の技法としては本を読んだりデザインパターンを学んだり、人のコードを見て学ぶことが挙げられています。まあこういうリファクタリングは現場で実際にやって慣れてこそスキルが上がる面がありますがだいたい同じようなことを言っています。参考文献としてはここでも『オブジェクト指向における再利用のためのデザインパターン』『リファクタリング』が挙げられています。

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

 調べていくと中の方で実は同じようなことをしているクラスをどう改善していくかの話。
ここではサーバーにコマンドを送るJavaのクラス2つを例に上げて、一部づつ共通処理を抜き出したり親クラスに引き上げたり、最終的に大きな親の Command クラスとだいぶシェイプアップした小クラス2つになる例を上げています。ステップごとにサンプルコードが乗っていてとても分かりやすいです。
 格言系としてはここで「開放/閉鎖原則」(Open/Closed Principle :OCP) =コードは拡張に対しては開いていて、修正に対しては閉じていなければならない、が登場。たいていの場合、重複を取り除いていくとコードは自然とこの原則に従うと述べています。

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

 なんとなく予想がつきますが何百行、何千行であまりに長く複雑で触りたくないメソッドを「モンスターメソッド」と称し、どう立ち向かっていくかの話。
 このモンスターメソッドには以下の種類があるそうです。

  • 箇条書きメソッド:単に処理を並べただけでインデントがない、ながーいメソッド
  • 錯乱メソッド:インデントが深すぎて理解できないメソッド

 例が載っているのですが、かの『Code Complete』のCODING HORROR例を思い出すなかなかにモンスターなメソッドです。
 自動リファクタリングツールで分割する他、手作業で行う場合は以下のポイントがあるとしています。

  • 処理が終わったかどうか分かる検出用変数を一時的に持って、テストメソッド側ではこの値を assertに使うなどして判定する。
  • 理解できる小さな処理があったらメソッドに抽出して整理していく。
  • 変えるとまずそうな部分もまずテストコードを書いて、リファクタの後で動きが変わらないのを確認。「依存関係の落ち葉拾い」と呼んでいる。
  • モンスターメソッドをそのまま全部別クラスに移動して対処。「メソッドオブジェクトの取り出し」と呼んでいる。
  • 条件部分と処理部分をそれぞれ別メソッドに切り出す。逆に一緒のメソッドに切り出したほうが良い場合もある。
  • 小さい処理が本来別クラスにあるべきとわかっても、まずは同じクラス内でメソッドに抽出、他の部分のリファクタリングが進んでから別クラスへ移動する。

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

 プログラミング周り全般の作業で大事なのは時間よりも集中して作業することだ...というのは様々な文献で言われていますが、本書でも同じような意見です。
「超集中編集 (Hyperware Editiong)」の状態をテストやペアプログラミングで促進するとよい。没入したフロー状態で行うこと。テスト→リファクタ→テストで確認して細かい単位でフィードバックが得られると疲れない...など。
 また、リファクタに取り掛かってからあちこちやることが増えて目移りして進まない場合は、「プログラミングとは一度に1つのことを行う技術である」の呪文を繰り返しながらひとつづつ解決していくとよいとあります。人間は一度にマルチタスクでやっていくと効率がどんどん下がるので一旦整理してシングルタスクで順次片付けていこう…というのはタスク管理手法やToDo管理ツールやライフハック系の諸々でもよく見かけますが、まあそういうことですね。

  • シグネチャの維持:新メソッドに移動するときはまずは引数の数や型は最初は同じにして、注意深くコピーペーストしていく。
  • コンパイラ任せ:コンパイラの型チェックでわざとエラーを起こしてそこを変更していく。ただしメソッドを消してもオーバーライド元だったスーパークラスの同名メソッドが呼ばれるケースは注意。
  • ペアプロミング:2人でやると間違いに気づきやすいので良い。

参考文献としては古い本ですが『ペアプログラミング』が挙げられています。最近の日本だと「モブプログラミング」も注目されていますね。

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

 これまた正直なタイトルですが、2部の最後はこんな時どうするかの話。
 プログラミングに楽しさを知った最初の頃の気持ちを振り返ってみよう。向こうで新しい開発をやってるチームが楽しそうに見えても、「隣の新規開発の芝はそれほど青くない」。成功する鍵はやりがいを見い出すことだ。コミュニティと交流を持ってみよう。物事をより良くする機会だと捉えよう。これを機会にテスト駆動開発をやってみよう。気晴らしに仕事と関係無ないコードを書いてみよう。
 不毛なレガシーコードの砂漠もリファクタが徐々に進んでコードを制御でき始めると、「優れたコードのオアシス」が現れてくる。そして楽しくなってくるよ…と締めくくっています。うーん、エモいです。

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

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

 本の7割を超えたところで第3部はこの25章だけ。これまでのまとめとして、今まで導入で紹介してきた改善手法をすべて名前をつけ、24通りのテクニックとして紹介しています。本書の内容が理解できた人にはこの25章こそが実践時に参照するメインになるのではないでしょうか。
 かの『リファクタリング』とも関連し被るところがあるのは本書でも述べています。例はC/C++/Javaですが、1箇所だけRubyもあります。何かの足しになると思うので英語版タイトルと共に以下にまとめてみます。

25.1 パラメータの適合 (Adapt Parameter)

 面倒な引数 (JavaHttpServletRequest など) をインターフェースに変える。テスト用クラスでは単に文字列を扱い、本番用では中に HttpServletRequestをメンバで持ったラッパークラスにするなど。

25.2 メソッドオブジェクトの取り出し (Break Out Method Object)

 長い長いメソッドを、別クラスのインスタンスメソッドに切り出す。元のメソッドの引数が別クラスのコンストラクタの引数になるイメージ。

25.3 定義の補完 (Definition Completion)

 C/C++専用。どこかのヘッダーファイルで関数やメソッドを宣言したら、別のソースファイルで実体を定義。これを本番・テストで別のソースファイルで切り替える。

25.4 グローバル参照のカプセル化 (Encapsulate Global References)

 主にC++グローバル変数をクラスの中に閉じ込めて、呼び出し元からインターフェース経由で参照。閉じ込めたクラスは本番用とテスト用で別の動きをさせる。

25.5 静的メソッドの公開 (Expose Static Method)

 インスタンス生成が難しいクラスのメソッドをテストしたい場合。テスト対象メソッドの全部あるいは一部をstaticに変えて定義するとテストできる。

25.6 呼び出しの抽出とオーバーライド (Extract and Override Call)

 テストしたいメソッド中で、メンバ変数が何か別のやっかいな処理を呼び出していて依存関係がある場合。この処理呼び出し部分をprotedtedな別メソッド化して、テスト時はテスト用サブクラスでこのメソッドをオーバーライドする。

25.7 Factory Methodの抽出とオーバーライド (Extract and Override Factory Method)

 クラスのコンストラクタ内でメンバ変数の初期化処理を直接書くと、テスト時に回避が難しくなる。例では protected makeTransactionManager() など別のファクトリーメソッドに切り出しておく。テスト時はサブクラス化してこのファクトリーメソッドをオーバーライドする。

25.8 getメソッドの抽出とオーバーライド (Extract and Override Getter)

 上のテクニックがが使えないC++の場合。ある厄介なオブジェクトがあったら生成ロジックをすべて抽出してget()メソッドに入れて、使用箇所を全部置き換える。get()メソッドは遅延get(=最初に判定してNULLのときだけインスタンスを作る)にする。テスト時はサブクラスでこのget()メソッドをオーバーライドする。

25.9 実装の抽出 (Extract Implementer)

 例はC++。元クラスのメソッド名をコピーした本番用クラスを作り、元クラスのpublicメソッドは親クラスに引き上げて抽象メソッド化。public以外のメソッドはインターフェース化。そして本番用クラスで実装する。

25.10 インタフェースの抽出 (Extract Interface)

 厄介なオブジェクトがあったらメソッド引数の場合はインターフェースで宣言、メソッド中で使っているところを全部インターフェース経由に変える。厄介なオブジェクトはそのインターフェースを実装し、それとは別にテスト用に Fake...クラスでそのインターフェースを実装。するとテストメソッドではこの Fake... クラスのオブジェクトさえあれば対象メソッドを動かせる。

25.11 インスタンス委譲の導入 (Introduce Instance Delegator)

 staticメソッドの集合のユーティリティ系クラスで、使うと問題があるstaticメソッドがある場合。委譲用のインスタンスメソッドを一旦作り、その中で問題のstaticメソッドを呼ぶようにする。
問題メソッドの呼び出し元では、インスタンスメソッド経由に一括で変えていく。こうすると委譲用インスタンスメソッドの中で振る舞いを変えられる。

25.12 静的setメソッドの導入 (Introduce Static setter)

 Singletonパターンでインスタンスが常に1つになっているが、テスト中は2つ以上作りたい場合。
 SIngleton クラスに setInstance() 的なメソッドを追加して唯一インスタンスを変えられるようにする。テストで必要ならSingletonのサブクラスを作ったりする。SIngletonの使い過ぎはよくない。

 オブジェクトの入れ替えができないCでこれをやりたい時。ターゲットのメソッド/クラスと同じメソッド/クラスのダミー関数や擬装クラスを作り、リンクでそちらを参照させる。Javaでも環境変数CLASSPATHを変えると同じことができる。

25.14 コンストラクタのパラメータ化 (Parameterize Constructor)

 クラスのコンストラクタ内でメンバ変数のオブジェクトを生成したら、コンストラクタの引数経由でこのオブジェクトを直接渡せる別コンストラクタも作っておく。テストでは後者のコンストラクタに切り替えて使う。

25.15 メソッドのパラメータ化 (Parameterize Merthod)

 テストしたいメソッド内でオブジェクトをnewしていたら、メソッドの引数で渡せるようにする。

25.16 パラメータのプリミティブ化 (Primitivize Parameter)

 問題があるメソッド内の中間処理をテスト用に別メソッド化する。多くの場合、別のスプラウトクラスにしたほうがよい。

25.17 メソッドと変数の引き上げ (Pull Up Feature)

 改善すべき質の悪い依存関係などと関係ない処理をしているメソッドやメンバ変数があったら、abstractな親メソッドに型やスコープはそのまま引き上げる。
 テスト対象がシンプルに改善したら、Testing...クラスとしてテスト用クラスを作り、同じ親を継承、テスト化に立ち向かう。
 親をabstractにするのは、必ず子クラスを作って使っていることを明確にするため。(具象クラスにすると、アプリ内のどこでもインスタンス化されていない場合に使われていないクラス=削除して良い、と見えてしまう)

25.18 依存関係の押し出し (Push Down Dependency)

 テスト対象クラスをabstractに変更、問題ない処理はそのまま残す。継承した子クラスを作り依存関係で問題があるメソッドとインスタンス変数一式をこちらに、下方向に押し出す。テスト用のクラスはこの子クラスをさらに継承した孫クラスで作り、依存で問題がある部分をオーバーライドして対処する。

25.19 関数ポインタによる関数の置き換え (Replace Function with Function Pointer)

 Cなど手続き型言語で依存を排除したい場合。置き換えたい関数と同じ名前で関数ポインタを定義、その後元の関数の名前を_productionなどに変更。実装しているソースファイル内で、元関数のポインタを初期化、関数の本体部の名前を新しい方に変えていく。このテクニックを無理に使うよりは、使用言語をC++にアップグレードした方がよい。

25.20 getメソッドによるグローバル参照の置き換え (Replace Global Refernce with Getter)

 テスト対象メソッド内でグローバルなオブジェクトを作ってしまうようなstaticなSingletonクラスがあったら、そのオブジェクトを取り出す処理をprotectedなget()メソッドに押し出し、戻り値はインスタンスでなくインターフェースにする。
 テスト用サブクラスではそのget()メソッドをオーバーライド、Fakeな別クラスを戻り値で返すようにすると、テスト用にどんなロジックでも書ける。

25.21 サブクラス化とメソッドのオーバーライド (Subclass and Override Method)

 依存関係を分離したいテスト対象メソッドがあったら、まずオーバーライドできるようにスコープを変え、テスト用サブクラスでメソッドそのものをオーバーライドする。ほとんどなんでもできて強力。

25.22 インスタンス変数の入れ替え (Supersede Instance Variable)

 「Factory Methodの抽出とオーバーライド」のテクニックが使えない場合。C++だとコンストラクタ内での仮想関数呼び出しが許可されていないため。
supersedeXXX()としたメソッドで以前のインスタンスを破棄、新しい値をセット。テストではこのメソッドを使う。

25.23 テンプレートによる再定義 (Template Redefinition)

 C++専用。置き換えたいクラスをテンプレートに変換して別の名前にし、typedef文を活用、テスト時は置き換えたい新しい型でテンプレートをインスタンス化する。

25.24 テキストによる再定義 (Text Redefinition)

 ここだけ動的言語の活用例としてRuby。テスト用の別スクリプトの中で元のクラスをrequireし、テスト対象のクラスを同じクラス名で、問題のあるメソッドだけ空にするなどして再定義。するとメソッド自体が再定義されて書き換わるので、テストが便利になる。

 最後は付録としてリファクタリングの基本、そして用語集が並び、470ページに及ぶ本書は幕を閉じます。

気になったところ

★長い本なのでそれなりに根気が入ります。僕も3週間ぐらいかけてゆっくり読みました。といっても各章はけっこう短く分割されているので(えらく短い章もありますが)読みやすいと思います。

★欠点ではないのですがやはり原著が出たのは2004年、時代を感じます。CやC++固有の話も多く、僕も普段使わないので理解しきれなかったところもありました。もちろん今でもCやC++で頑張ってる人もいるでしょうが、2018年現在だとやはり情報が古いところもあります。
 2004年というと2000年台にJavaオブジェクト指向UML記法が注目を浴びてJDK1.4から5.0の頃、本書の一節にあるようにJavaと.NETが二大帝国を築いて覇を競っていた頃です。アメリカでは既にレガシーなシステムも多かったのでしょうか。
 この頃日本語訳する話もあったそうですが、日本でレガシーというとまだCOBOLで作られたシステムだろうということで見送られ、翻訳されたのが2009年。日本では最初は2004年からRailsが出てRubyも注目を浴び、PHPも2004年から5になってオブジェクト指向になり、軽量級言語が勢いを増し、2005年にAjaxが登場してJavaScriptが徐々に復権UMLが面倒臭がられたりアジャイルが注目されたり、Java/.NET中心から徐々に世の中が変わっていった時代です。2000年代後半だともう開発が終わって本番稼働中のエンタープライズJavaシステムで、中身は設計がまずくて掘り起こすとレガシーコードが潜んでいる事もけっこうあったのではないでしょうか。そうした時代背景と共に読むと面白いと思います。
 また、本書のテクニックの幾つかあるようにCやC++だとどうしても乗り越えられない壁もあり、だからこそオブジェクト指向言語は進化し、改善されてきたのだなと改めて思います。

その他感想

★翻訳が上手いのか元の本からそうなのか、著者のマイケルさんの文章はジョーク満載の砕けた文章でもなく技術系の硬い文章でもなく、柔らかくて読みやすいですね。文中にあるように、ともすればモチベーションが下がりがちなレガシーコードを前にした仕事をする上で、開発者の気持ちに寄り添った、あくまでコード改善の現場に立った立場が貫かれています。

★文中に多く出てくるコードサンプルはコード内のコメントはついてないのでクラス名やメソッド名の英語から想像するしかないのですが、じっくり読むとよく出来ています。このへん名著『Code Complete』なんかと似た感じですね。
段階ごとにコードが改善されていく様子を記した例も多く、言語そのものやオブジェクト指向リファクタリングを学びたい人にも参考になると思います。

iwasiman.hatenablog.com

★文中のテクニックで「コンパイラまかせ」(Lean on the Compiler)が時々出てきます。要はリファクタでコードを変更してわざとコンパイルエラーを出させてそこから影響範囲を知って直していく当たり前のことですが、考えてみればこれも立派なテクですね。近年は動的言語のほうが注目されていますが、コンパイラーって偉大だなと改めて思いました。

★共通部品とかフレームワーク的な基盤部分の実装でなく機能側の開発をする時はリストをList型でなくArrayList型で書いちゃったりあまりインターフェースを意識しないこともあるのですが、こうして見るとテストの上ではインターフェースのありがたみがよく分かります。

★本書ではクラスや処理の関連などを「影響スケッチ」などと名前をつけて解説しています。要は面倒なフォーマット指定やルールはなくて簡単なメモの延長レベルで良いから図で整理していこうということなのですが、このへんアジャイルエクストリーム・プログラミングなどの流れっぽいですね。
 2000年代にオブジェクト指向が注目され必要以上にもてはやされた頃はUML記法がまるで究極の図解方法のように持ち上げられたこともあったのですが、クラス図・シーケンス図(場合によってはユースケース図なども)以外は面倒臭がられて結局UMLの全部はあまり使われなかった歴史もあります。やはり図解はルールに囚われずに自分たちにあった方法でやっていくのが良いのだなと思います。

★以前読んだ『現場で役立つシステム設計の原則』の中ではDDD寄りのアプローチでのコード例として、コンストラクタでメンバ変数をセットしたらあとはそのインスタンス変数しか使わずメソッド引数は使わない、getterメソッドは基本禁止…などのちょっと過激なルール例が出てきますが、本書は明らかに逆を行っていますね。 iwasiman.hatenablog.com

 それどころかstaticメソッドをインスタンスメソッドに変えてしまう、privateをprotectedに変えてカプセル化を壊してでもテストする…など、オブジェクト指向原理主義者の人(推定)が見たら卒倒しそうな手法も平気で載っています。時にオブジェクト指向の基本に反してでもテストを武器にレガシーコードに立ち向かう…という現場の実践的な立場が貫かれています。
 格言の「ソフトウェア開発に銀の弾丸はない」、どんな原理原則も絶対ではなく、その時々の状況に合わせて自分たちで考え、何を優先し採用するか自分たちで決めていくのがひとつの真理なのだなと改めて思います。

★本書を読んだ上で、例えばJavaなんかでクラスを新規作成するとき、将来別の誰かがテストコードを書いたり拡張するかもしれない時の注意点を改めてちょっと考えてみると…

  • クラスの名前やメソッドの名前は重要。(名前重要)
  • クラスの役目はひとつにする。大きすぎるクラスは分割する。(単一責務の原則:SRP原則)
  • (本書にはこのへんの話は出てこないですが) あとから見る人のためにコメントはちゃんと書く。(意図を表す、PIE原則)
  • コンストラクタ内でメンバ変数生成や何かとの接続など長い処理をする時は、initialize()メソッドなどに一旦外だしする。
  • クラスとクラスの結びつきは少なくする。1クラス内のメンバ変数、メソッド引数、戻り値に登場する型の種類は少ないほうが良い。(依存性の排除、疎結合デメテルの法則)
  • 同じ処理を2回書かない。内部の共通メソッドや親クラスのメソッド化、あるいは別クラスに処理を委譲する。(DRY原則)
  • メソッドの内の処理が長かったら別メソッドに分けて短く保つ。(複雑さに分割で対応する、KISS原則)
  • 将来子クラスで拡張しやすいような作りにする。(開放・閉鎖原則)
  • メンバ変数もsetter/getter経由でアクセス可能にし、子クラスで書き換えられるようにする。
  • メソッド内で引数のオブジェクトやメンバ変数が重い処理をするなら、型はインターフェース経由で渡すと良い。(抽象に依存すべき、→いちおう依存性逆転の法則:DIP)
  • テスト時の障害になりそうな大きな処理は特に外だしや入れ替え可能にすることを意識する。(DBアクセス、ファイルアクセス、他システムとの通信、印刷や画面表示、別レイヤーにまたがる処理など)

 ...というように、たいていオブジェクト指向の基本やプログラミング原則に繋がっていく訳ですね。メンバ変数がやる処理を入れ替えられるようにする...というのはより高度になると昨今はいろんなフレームワークが備えている、依存性の注入(DI)の考え方にも繋がっていきます。

まとめ:レガシーコードと戦うだけでなく、レガシーコードを生み出さないためにも役立つ本

 2018年現在からすると古いところもあり、『リファクタリング』と被るところもあるのですが、よく名前が上がる名著だけあって時代を超えて役立つ本でした。目の前に厄介なレガシーコードを抱えている人の他にも、

  • 新規開発や追加開発だけど将来の保守性も考えてちゃんとしたコードを書いていきたい方
  • リファクタリング手法を学びたい方
  • テスト駆動開発や、テストしやすいコードを学んで行きたい方
  • オブジェクト指向で書くと何が嬉しいのかテストの立場から理解したい方
  • いろんなプログラミング原則に従うと何が嬉しいのかテストの立場から理解したい方

などなど、よりよいコードを書いていきたい人全てに役に立つ本です。
 そういえばこの前仕事で書いたコードはそれなりに良くやれてるはずだけど、もっと良くできるところはないかな…と思い出させてくれた本でした。

おまけ:リンク集

訳者陣代表のウルシステムズ 平澤章さんのスライドが内容を一通りカバーしています。

www.slideshare.net

こちらもSlideShareでもうひとつ。

www.slideshare.net

Java/C#でなく動的言語PHPで適用したらどれだけいけたかというヤフーの資料。こちらも興味深いです。

www.slideshare.net