じぶん対策

日々学んだことをアウトプットして備忘録にしています。

積読解消 「レガシーコード改善ガイド」を読んでリファクタリングの知識をつける

はじめに

自チームで担当しているプロダクトでは常にリファクタリングを意識した開発を行っています。
なかにはかなりレガシーなプロダクトもあり、その改善への指針として書籍「レガシーコード改善ガイド」を購入していました。
去年の秋頃に買ったんですがしばらく放置していたため、年明けのこのタイミングで積読の解消をしていきます。

対象書籍

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

概要

書籍には、ざっくり以下の内容が書かれています。C++/Javaのコードがサンプルとして使用されています。
具体的なコードの修正方法のような技術的な部分だけでなく、「良い設計とは」のような設計論、ペアプログラミングのような方法論についても触れています。
今回はテストに対する考え方の部分を中心に自分の考えと比較しながらのメモを残していきます。
具体的なコードの修正方法についてはぜひ書籍を手にとってみてください。

  • 仕様がわからないコードの分析方法、修正方法
  • レガシーなコードを疎結合な設計に部分的に改善する方法
  • 良い設計についての原則とその例

読書メモ

そもそもソフトウェアの変更とは?

ソフトウェアに変更が必要となるケースには以下のような理由がある。

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

変更を行いながら既存の振る舞いを維持するのはとても難しく、大きなリスクを伴う。
以下3点を考慮する必要がある

  • どんな変更を行わなければならないか
  • 変更が正しく行われたことをどうすれば確認できるか
  • なにも壊していないことをどうすれば確認できるか

変更の難易度が高いからといってコードの変更を避けると全体の見通しが悪くなる。そのため将来的に更に変更の難易度が上がる。 また、コードの理解にかかる時間も増えてしまう。

所感
->近年のエンジニアの転職事情を考えると属人化したコード管理は避けたい。
->変更が容易で理解も容易(部分ごとに関心事が分離されている)コードの価値が高い

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

コードの変更は難易度が高い作業だが、フィードバックを得ながらの作業によってその難易度やエンジニアのストレスを低減できる。
テストを書くことでコードの振る舞いの大部分を固定し、意図した箇所しか変更していないことを確認する。

結合テストも大事だが、だからといって単体テストを省略するといくつか問題がある。なによりテスト頻度が落ちるのが良くない。

  • エラー発生箇所の特定が難しくなる
  • 実行時間が長い
  • テストケースを動かすための値の準備が大変

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

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

テストの最大の障害は依存関係なので依存関係を整理できる設計(ex.クリーンアーキテクチャ)への理解が必要。

所感
->C/C++においてはプリプロセッサを利用してテストが書ける(Cの経験はあったがプリプロセッサをテストに使用する考え方を知らなかった)
->結局テストしやすい設計を初めから意識する必要があるのでは???

テストによる時間の節約

コードが書かれる機会より読まれる機会が多い。
一切変更がない、バグも潰しきったコードならテストの効果は最小限になってしまうがそんなプロジェクトは殆どない。
最終的にテストコードの整備は開発作業全般を早めることになるため、ほとんどの開発組織にとって重要なトピックになる。
コードは自分の家であり、その中で暮らさなければいけない。日々きれいにしていくべき。

スプラウトメソッド(Sprout Method)

具体的な改善手法としていくつか紹介されているうちの一つ。 スプラウトとは、「発芽」のような意味合い。
古いコードに対してテストを書くことは難しいが、新しく追加する要件に対してテストを書くことは容易。
特に独立した1つの機能としてコードを追加する場合や、まだメソッドのテストを整備していない場合に推奨される。

  1. 変更が必要なコードを洗い出す
  2. もしその変更が、メソッド中の1つの場所で一連の命令文として実現できるなら、必要な処理を行う新しいメソッドを呼び出すコードを書いて、それをコメントアウトしておく
  3. 呼び出されるメソッドで必要となるローカル変数を特定し、それらを新しいメソッド呼び出しの引数にする
  4. 新しいメソッドから呼び出し側のメソッドに値を返す必要があるかどうかを決定する。必要があるなら、戻り値を変数に代入するように呼び出し側を変更する
  5. 新しく追加するメソッドをテスト駆動開発により作成する
  6. 呼び出し側のコメントを外して、新しく追加↓メソッドの呼び出しを有効にする

短所
- 元のメソッドとクラスは放置される

長所
- 古いコードと新しいコードを明確に区別できるようになる

こんな感じのレガシーコードに対する改善テクニックがいくつか紹介されている。

所感
->大きなコードを改善する際の最大の障害は既存のコードであり、一度にリファクタリングするのはかなり難しいのでリファクタリングによって一時的に多少形がいびつになろうとも日々きれいにしていくほかない。
->ラップメソッドやラップクラスを使って一時的にコードが汚くみえたとしても将来のために依存関係を整理し、きっかけを作る事が重要。設計の観点からは理解しがたい手法かもしれないが、明確な新しい責務と古い責務を分離することがより優れた設計へ向かう唯一の方法。
->人は汚い箇所にゴミを捨てたくなる。右に倣えでコードを書かない勇気が必要。

オブジェクト指向によるコードの複雑化

オブジェクト指向の悪しき歴史(継承等)からどう脱却するか。
テストの書きづらさやコードの複雑さは継承やオーバーライドから来ているパターンが多そう。
グローバル変数を使用するためのsingletonパターンの例から、一般的に言われる「グローバル変数は悪」の例とテストの書きにくさを実感できる。

テストコードは本番コードと同じルールに従う必要はない(変数をpublicにするとか)が、かんたんに理解でき、変更できるものでなければならないため、きれいであるべき。

所感 ->LaravelにおけるDI等で当たり前のように書いていたコードがどういった問題を避けることができ、以下にテストが書きやすくなるかを実感できる。 ->例えば、コンストラクタのパラメータ化とか。コンストラクタ内でオブジェクトが生成されており、生成の依存関係がない場合には簡単にコンストラクタのパラメータ化ができる。

->あまりキャリアが長くなくて、かつ恵まれた環境にいたため、目にすることのなかったレガシーなコードの例とその対処法を知ることができた。

変更をどのように進めるべきか

変更する場合にどのメソッドをテストすればよいか - 変更内容を検討する - 何に影響するかを調べる - 影響を受けたものがさらに何に影響するかを調べる

この影響範囲の調査の方法
影響を受ける変数と戻り値が変わる可能性のあるメソッドについてスケッチを書く
このスケッチが単純であればあるほどソフトウェアとして良い構造

privateをキーワードを適切に使用して不要な影響範囲をなくすこと、なにがmutableでなにがImmutableなのかは言語仕様によって異なるため、使用する言語についてよく知ることが必要

ときにカプセル化と依存関係を排除しテストでコードを保護することが対立することがあるが、その場合はテストによる保護を優先する。
結果的に将来のカプセル化を強めるために役立つことが多い。

一箇所にたくさんの変更が必要な場合、関係するすべてのクラスの依存関係を排除するべきか。

いくつかの変更を一度にまとめてテストできる場所を見つけることができれば、より粒度の大きなテストを行うことができる。
変更のためのテストだけでなく、該当箇所を更にリファクタリングするためのテストも手に入る。
ただ、単体テストの代わりに使うのではなく、単体テスト整備のための最初の一歩とするべきである。

仕様化テスト

ソフトウェアが実際にどう動いているかをテストコードとして表現することで変更を検知したり、リファクタリング時の振る舞いの固定化使用する。 作成手順は以下

  1. 変更する部分のためのテストを書く。コードの振る舞いを理解するために必要と考えられるテストケースをできるだけたくさん書く
  2. テストを書いたあと、変更したい具体的な事柄について調べ、それに関するテストを書く
  3. 機能の抽出や移動をしようとする場合、既存の振る舞いや振る舞い同士の関係を検証するテストを個々に記述する。それにより、移動対象のコードが実行されること、及びそのコードが適切に関係し合っていることを検証する。その後でコードを移動する。

試行リファクタリング

コードを変更する場合、変更箇所や影響箇所について深く理解する必要がある。
その手法の一つとして試行リファクタリングという手法が存在する。

ブランチを切って、あらゆる方法を用いてリファクタリングする。
そしてそのコードをチェックインすることなく破棄する。
最終的に破棄してしまうため一見無駄に思えるかもしれないが、短い時間で効果的にコードについて学ぶことができる。

所感

幸せなことに、書籍に紹介されているほどひどいコードに実際に出会ったことはないですが、良い設計のために留意すべきことがより具体的に理解できました。

具体的な例を見ることで、実際の業務等でのリファクタリングの作業イメージを持つことができました。
また、設計についてもテストの容易性の観点や依存関係の複雑さ観点から改めて考えるきっかけになりました。
書籍自体は多少内容が古い部分もありますが、設計等本質的な部分は現在でも十分参考にできると感じています。
古いと感じている箇所としては、具体的なコード修正に関する箇所で、現代においてはIDEやツールで解決する問題が結構登場します。
また、全体的に説明を他の章に移譲している箇所が多く、書籍としては読み進めにくい部類でした。

特に印象に残ったワード

設計等についての記述等から、特に印象に残った記述についてまとめます。

  • 良い設計はテスト可能であり、テスト可能でない設計は悪い設計である
  • 書籍全体を通して推奨されているテスト駆動開発は一度に一つのことを行うための手段であり、プログラミングとは、一度に一つのことを行う技術である。

課題

書籍を読み進めるにあたって、あまり同意できない箇所があり今後の課題としてまたの機会に調査してみようと思います。

  • sealedやfinalはテスト容易性を下げるため控えめに使う