覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

「レガシーソフトウェア改善ガイド」 第2部

「レガシーソフトウェア改善ガイド」の第2部 コードベース改良のためのリファクタリング で気になったところの抜粋。

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

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


3. リファクタリングの準備

  • 急進主義者(Iconoclast)は、レガシーコードを憎悪する革新派の開発者である。コードベースの全ファイルを直すまで満足しない。
    「書き方がヘタなコードを見るのは我慢できない」というのだが、「書き方がヘタなコード」の定義は、「誰か他のやつが書いたコード」と、ほとんど同じであることが多い。
  • コードレビューはコーディングミスをチェックする技術だと思われがちだが、他にも以下のような重要なメリットがある。
    • レビューアにとってコードレビューは自分の知識をシェアする機会である
    • コードを書いた人はコードレビューの場で、チームの他のメンバーに対して自分が何を書いたのかを知らせることができる
  • レガシーアプリケーションの本格的なオーバーホールや完全なリライトを考えているのなら、それ専用に時間とリソースを配分する必要がある。
  • ほとんどのリファクタリングは、価値(value)、難度(difficulty)、リスク(risk)の3つの軸により、いくつかのカテゴリーに分類できる。
    • リスクは、しばしばリファクタリングの対象であるコードに依存しているコードの量に比例する。コードの依存関係が大きければ大きいほど、その変更によって予期せぬ副作用が発生するリスクが大きい。
  • ビジネスに投資するステークホルダーの立場から見ると、リライト(書き直し)には、これといって期待できる要素がない。
    一般に、書き換えられたシステムの働きは、古いシステムのそれと、ほとんど同じになるはずだから、まったくメリットが見当たらないと評価される大きなリスクがある。
  • 既存のシステムの書き換えには、独自のリスクがある。それはリグレッションのリスクである。
    既存のソフトウェアには、そのシステムのすべての仕様が(ビジネスのルール全部を含めて)プログラムのソースコードにエンコードされている。
    これらのビジネスルールをひとつ残らず見つけて、それらを忠実に新しいシステムに移植できると保証できなければ、そのシステムの振る舞いは、書き換えの結果として変わってしまう。
  • 技術者は、ソフトウェアプロジェクトを新規にゼロからセットアップするときのオーバーヘッドを、過少に見積もりやすい。
    すでに軌道に乗ったプロジェクトで仕事をしているときは、オーバーヘッドの多さを本当に認識することがないからだ。
    けれども、開発を始めるときは、ビルドファイルやロギングユーティリティやデータベースアクセスするコードやコンフィグレーションを読みやすくするユーティリティなど、面白くもないボイラープレートコードを山のように書かなければならない。
    • ボイラープレートコードを書く面倒を減らす手段として、あなたが置き換えようとしているコードベースから一部のコードを借りることができるかもしれない。
      けれども、そのコードを後で削除またはリファクタリングする計画が必ず必要だ。
      あなたはリライトを行うつもりで、実は非常に効率の悪いリファクタリングを行ってしまったのだ。
  • オーバーヘッドを念頭に置き、必要な仕事の量を甘く見積もりがちな開発者の傾向を計算に入れても、やはりリライト(書き直し)は必ず見積もりを超過する。
    • 平均的なタスクに必要な作業時間を少なめに見積もっていて、その誤差がタスクの数だけ積算されたのだ。
    • 大規模なリファクタリングプロジェクトもやはり見積もりが難しく、したがって超過も発生するが、主な違いとしてリファクタリングではインクリメンタルな作業の恩恵を受けやすい。
      リファクタリングが見積もりを超過して、途中で開発を止めると判断しても、おそらくコードベースに何らかの有益な改善が行われている。
      完全な書き直しは、完了するまで何の価値もないのだから、いったん始めたら、たとえ予定を超過しても最後まで苦労を続けなければいけない。
  • リライト(書き直し)のメリット
    • ゼロから書くことによって、既存のコードから余計な影響を防ぐことができる。
      既存コードのパラダイムの中でコードを書いていると、その周囲にあるコードの設計と実装から良くも悪くも制約を受ける。
    • いったんコードが書かれてしまった後でテスタビリティを追加するのは非常に困難なので、レガシーコード用にテストを書くのは開発者にとって限度を超えた時間と努力を要する仕事になりかねない。
      いっぽう、ゼロから書き直すのであれば、テスタビリティを最初から設計に組み込むことができる。
      「あなたのコードを好きなだけテスタブルにできる」という自由は間違いなく有益である。
  • リファクタリングにある程度の時間と労力を注ぎ込んでみたけれど、品質に顕著な改善が得られないときに限り、完全なリライトを考慮し始めるべきである。
  • リライト(書き直し)を行う場合、インクリメンタルに実行できないか検討してみる価値がある。
    • リライトを分割する方法の一つは、レガシーソフトウェアを理論的なコンポーネントに分割してから、それらを一つずつリライトするものだ。
      このアプローチを使えば、リファクタリングとリライトの境界が厳密ではなくなることが注目点である。


4. リファクタリング

  • 失効コード
    • 失効コードとは必要なくなったのにコードベースの中に残っているコードのこと。その不必要なコードの削除にはいくつものメリットがある。
      • 読むべきコードの量が減るので、コードが理解しやすくなる。
      • 使われていないコードを誰かが修正あるいはリファクタリングして無駄な時間を費やす可能性を減らす。
      • (おまけとして)コードを削除するたびに、プロジェクトのテストカバレッジが上がる。
    • コメントアウトされたコードは、無意味な雑音であり、周囲のコードを読みにくくするだけ。
  • 破綻しやすいテスト
    • テストケースが壊れやすくなる一般的な原因のひとつは、あまりに細かいレベルでユニットテストを行っているから。
    • 一般に、このようなテストを書く必要はない、本来はコンポーネントが互いに提示している振る舞いだけをテストすべきであって、内部状態をテストすべきではない。
  • nullの使いすぎ
    • null参照やヌルポインタを使うのは、どんな言語でもごく一般的なバグの元。
  • 不必要な「ミュータブル」(mutable)
    • オブジェクトをイミュータブル(変更不可能)にすれば、開発者がプログラムの状態を追跡管理しやすくなる。
  • テストなしにリファクタリングするときは、コードレビューによってその埋め合わせをすることができる。


5. リアーキテクティング

  • リアーキテクティングとは
    • リファクタリングを行うときは、例えば一部のクラスを別パッケージに移動することもある。それに対して、リアーキテクティングでは、それらをメインのコードベースから別ライブラリへ移すことがある。
    • 1個のアプリケーションを複数のコンポーネントモジュールまたは独立したサービス群に分割する理由は以下3つを達成するため
      • モジュール化による品質
      • 優れた設計によるメンテナビリティ(保守容易性)
      • 独立による自立性
  • Gradleに切り替えることで得た主なメリット
    • マルチモジュールプロジェクトのために設計されている
    • Gradle DSL
    • プラグイン
  • フロントエンドとバックエンドを分ける
    • フロントエンドをバックエンドから分離することの主な利点は「関心の分離」(separation of concerns)。
    • ビジネスロジックをプレゼンテーションから切り離すために、フロントエンドとバックエンドを分離したら、開発チームも同様に切り分けるのが合理的である。
    • フロントエンドとバックエンドを分離することに決めるのなら、そのAPIについて「約束破りの変更」(breaking change)は絶対にしないというのが重要な暗黙の前提。
  • サービス指向アーキテクチャ(SOA)
    • アプリケーションが数多くの異なるサービスに分離されるので、アプリケーション各部を要件に応じてスケーリングしやすくなる。
    • SOAを実行する際の運用とアーキテクチャに関する難関
    • 運用のオーバーヘッド
    • レイテンシ
    • サービス発見
    • トレース/デバッグ/ロギング
    • サービスのホットスポット
    • データの断片化
  • マイクロサービス
    • SOAの特別なケース。疎結合、文脈の境界、開発者の自主独立と責任感に、特に重点が置かれている。
    • それぞれのサービスは、それ自身が持つドメインモデルにおける「コンテキスト境界」の役割を果たす。
  • ネットワーク上で発生する通信は、さまざまな形で失敗する可能性がある。サービス間にネットワーク呼び出しを追加するとまったく新しいタイプのエラーが導入される。


6. ビッグ・リライト

  • いったん実装を始めると、既存のソフトウェアには(実装にも、仕様にも)、あらゆる種類の隠れた特殊ケースや、不可解な抜け穴があり、その全てを調査し文書化する必要が生じる。
    リライトの場合、その仕事の大部分がレガシーソフトウェアの謎めいた振る舞いを解き明かし、それをどう扱うのが最良の策かを議論するために費やされてしまう。
  • 人々は既存のソフトウェアにあったバグや欠点に慣れてしまい、特徴のように思いがちだから、もしそれらを忠実に再現しないと熱心なユーザを失望させるリスクがある。
    さらに、あなたの新しい実装によって独自の新しいバグが導入されることは、ほぼ確実である。
  • プロジェクトの範囲を文書化する
    • どんなリライトを行うか決めたらその事実を、プロジェクトの範囲を定める詳細とともに、はっきりと文書化しておくことが肝心。
    • 新機能について - もし追加するのなら、それらをリストにする。それぞれの機能について、不可欠かそうでもないかを記す。
    • 既存の機能について - 既存のソフトウェアにある機能のうち、削除する予定ももはあるか?
    • タイミングか機能完備か - 決まった日付までにリリースすることと、予定の機能を完備した製品をリリースするのとどちらが重要か?
    • 段階的なリリースについて - 複数のリリースで徐々に機能を足していく計画はあるか?
  • もし可能なら、段階的リリースのアプローチが推奨される。
    小規模なリリースを繰り返しながら、それぞれのリリースで機能を少しずつ追加していくのだ。
    そのほうが、プロジェクトの最後に「すべてかゼロか」のビッグバン的なリリースを行うよりもリスクが低い。
  • 過去から学ぶこと
    • 既存のコードは以下の理由により尊重する必要がある
      • 何年もの間に、バグ修正、性能のための最適化、特殊なケースの処理などが蓄積されている。
        注意していないと、リライトではそれらを失ってしまう。
      • コードは既存のソフトウェアの振る舞いを正確に定義しているから、新しいソフトウェアがどう振る舞うべきかを決めるとき、有益なリファレンスとなり得る。
    • 既存の設計から無意識の影響を受けやすいことに注意し、安易に流されないよう積極的に抵抗すべきである。
      正当な理由がないのに新しい設計が古い設計を真似していないか、その兆候を常に監視し同じ問題をまったく別の方法で解決できないか、しばし考えてみるべきだ。
  • アプリケーションにデータベースがあるなら、新しいDBを作るか、新旧両方の実装で同じデータストアを共有させるか、どちらかを選ぶ必要がある。



関連エントリ