技術的負債の返済 – レガシーコードをリファクタリングで救うには

レガシーコードをうまく手なずけて、もう一歩成熟させるにはどうすればいいのでしょう?この投稿では、大規模なレガシーウェブアプリケーションと格闘してきた私が学んだことを紹介します。

レガシーコードはリファクタリングで救出可能

耳寄りなお知らせがあります!リスたちは毎年何千本もの木を植えてくれています。まあ自分たちが隠したドングリのありかを忘れてしまった結果ですけどね。そしてもうひとつ。あなたのプロジェクトも救出できるのです。

ボスから任されたプロジェクトがどんなに醜い泥まみれのレガシーコードだったとしても、そこには必ず道があります。道は曲がりくねっていて、木陰にはモンスターが待ち構えていることでしょう。それでも、一歩一歩着実に歩めば先に進めます。

恐れない

確かに、好き好んでそんな恐ろしい冒険に立ち向かうわけではありません。コードの沼への入り口には「ここにドラゴンがいる」という血まみれの看板。入ったが最後、今後数年は後遺症に悩まされそうです。モンスター退治のために沼に入って石化されてしまった農民の仲間入りをするよりは、腹ごしらえをしてそのへんの草原を散歩でもしていたいものです。
murky-forest.png
しかし残念ながら、ボスは有無を言わせずあなたの背中を突き飛ばして、沼に押し込みます。そしてボスは、安全なところで町を見守りながら、沼の持ち主である公爵とランチを楽しむのです。

抜本的な対策を施さない限り、この事実は変わりません。しかし、たった一本の泥道があれば、この沼を草地に変えることができるのです。

技術的負債 – どうしてこうなった?

警告の看板を越えてあちこちつついてみたあなたは、きっとこんなふうに感じるでしょう。いったいどこのどいつがこんなことをした?誰かがこんなざまにしたっていうことだよね。マジですか?こんなひどいコードを書いた無能はどこのどいつですか?

おそらくその感覚は正しいでしょう。沼に押し込められた人たちは、どんなに自信を持っているように見えても、何をしているのかを把握していないものなのです。

ただ、無能であることだけが理由ではありません。沼での作業の原則は、よく技術的負債というメタファーで表されます。

コードの癌

どんな開発プロジェクトであっても、どこかで手っ取り早い抜け道を使う場面はあります。とにかく先に進めたいという場合は、少々見苦しいハックであってもとりあえずは許されるでしょう。こうして負債が積みあがります。そのコードは、やりたいことはこなすものの今後の保守のことがまったく考えられておらず、ただ単に作っただけのものです。

そうして徐々に、プロジェクトのコードは壊れていきます。自分がさわるはめになるまでは、誰もそのコードのことを気にかけません。しかし、いつか誰かが、そのコードに手を加えることになります。それはおそらくあなたでしょう。

この原則は、あらゆるプロジェクトにあてはまります。もしウチは違うというなら、あなたはまだ全力を出し切っていないのでしょう。その隙に競合他社が手っ取り早い抜け道を使い、新しい機能を先にリリースしてしまいます。そして、大事なお客様が奪われてしまうでしょう。隣の部屋のパーティーに参加したお客様たちは、カクテルバーでロングアイランド・アイスティを楽しんでいます。いっぽうあなたは、一年がかりで手入れしたたった1平方メートルの草地でただ立ち尽くすだけ。確かにその草地は美しく手入れされているのでしょうが、作業はあまり進んでいません。
party-swamp-color.jpg
どんな健全なプロジェクトであっても、何らかの技術的負債を背負うものです。しかし、破産(コードのメンテナンスに手間がかかりすぎて手に負えなくなる状態)を回避するためには、どこかで負債を精算しなければいけません。

沼のオーナーに代わって負債の返済を強要されるのが、恐る恐る立ち入ったあなたの役割です。負債の返済のことをリファクタリングと呼ぶことがあります。これは、保守や拡張をしやすくなるようにコードを少しずつ変更しつつ、変更前とまったく同じ挙動を保つ作業のことです。

顧客の説得

沼のオーナーである公爵は、追加投入すべきモンスターについて口出ししてきます。顧客はいつだって、何か新しい機能を追加して欲しいと思うものです。言われた機能の締め切りは先週で、ほんとうなら二週間前にはすべて完成させておくべきものでした。あなたはまず、もうすでにできあがっていると考えている部分についても作り直しの必要があると、公爵に説明して納得してもらわなければいけないでしょう。

ここが重要です。負債の清算をはじめるように顧客を説得できなければ、技術的負債が膨れ上がって崩壊点に達してしまいます
mayhem.jpg
顧客もあなたも、求めるものはまったく同じはず。つまり、誰も苦しむことなく楽しく仕事を進めて、みんなが生計を立てていけるようにしたいということです。そのためには、プロジェクトを安定させる必要があります。ちょっとコードをいじっただけで全体が壊れてしまうような心配をせずに済むようにしましょう。

自由への戦い

あなた自身のためにも、まずは状況を把握する必要があります。いちばんいい方法は、目前に差し迫る危機とその対策について説明して、沼のオーナーにあなたの意図を伝えることです。

  • 技術的負債の意味を説明して、負債がかさむと開発スピードが半減してしまうことを話します。バグを見つけるのに時間がかかるし、バグを修正したと思ったら別のバグを仕込んでしまうし、新機能の追加にも余計な時間がかかってしまうといったことを、顧客に認識してもらわなければいけません。
  • 明確な短期目標を念頭に置いたリファクタリング。新しい機能を追加するだけなら一週間でできます。一方、リファクタリングに一週間かけた後なら、同じ機能を一日で追加できます。後者なら、将来別の機能を追加することになっても簡単に対応できるでしょう。短期的には後者のほうが高くつきますが、長い目で見れば後者のほうがずっと安上がりです。
  • 自分のためだけではなく顧客にとって最善なことを考えているのだと伝え、お互いが気にしていることが実は同じであるということを説明します。

オーナーを味方につけてしまいさえすれば、最大の課題はクリアできたことになります。これで、プロジェクトの軌道修正を自由にできるようになりました

新しい沼を作ろうとしない

ひどい状態からだって元に戻せます。新しく作り直そうなんて考えないでください。ゼロから書き直したものがうまくいくことはめったにありません。ほとんどは、リリースにすら至らないでしょう。

沼に出くわす人が出なくなるようにとなんとか公爵を説得できても、今度は「そんなにひどいなら、新しく作り直してくれないか」といわれるかもしれません。あなたもちょっと魅力を感じるかもしれませんね。あの最新ツールを使えば間違いなく、今よりもずっとよいものが作れるでしょうし。でも、やめましょう
caution.png
ゼロから書き直すリスクは、私の知る限りでは次のようになります。

  • 大きな赤いボタン参考):新しいコードはまだ一度も本番環境で動かしていないものです。そして、本番環境で初めて動かしたコードは、必ずといっていいほどどこかで問題が発生します。
  • データの移行:旧システムからエクスポートしたデータを取り込みつつ新たに入ってくるデータにも対応するのは大変で、どこかでおかしくなるかもしれません。
  • 新たなミス、そして古いミス:コードを書き直していると、バグを仕込んでしまったり以前のバグを復活させてしまったり、細かい機能や暗黙の機能を見落としてしまったりといったことが発生しがちです。こういった問題が発生するのは、システムが動き始めた当時はまったく問題のなかったところあることが多いものです。ビジネス的な価値を顧客にまったく届けられないまま、時間だけが過ぎていきます。
  • 業務の移り変わりに追従する:別のプロジェクトに移ってしばらくたったころに、以前のシステムを最新の業務要件にあわせて改修することになるかもしれません。新しいコードは、旧システムの機能を踏襲するだけではなく、旧システムにその後追加されたさまざまな機能にも対応しなければいけません。

やり直すのではなく、いま手元にあるものを改善しましょう

問題の可視化

あまりいい気はしないでしょうが、沼に潜むモンスターたちはあなたの目と鼻の先にいます。ドラゴンの吐く息はあなたの腕の毛を焦がし、キノコ岩に住むノームは向こうずねを蹴とばしてくることでしょう。

こういった邪魔者たちは、知らないうちにプロジェクトを破滅に導きます。それを防ぐためには、モンスターに立ち向かう必要があります。そのためには、何がおかしいのかをいつでも確認できるようにしなければいけません。

  • エラーを可視化します。毎週少しずつでも時間をとって、頻発するエラーの対応をしましょう。最終的にグラフがフラットになり、うまくすればエラーを根絶できるでしょう。
  • 環境を監視します。これは、ボトルネックや致命的な障害を見つけるためには不可欠です(次のパラグラフをごらんください)。

errors.png
これで、いつどこがおかしくなったのかに気づけるようになります。手遅れになってからじたばたするのではなく、見つけた時点で応急処置を施せるようになるでしょう。

いちばんつらい相手との戦い

次に、理想的な世界におけるそのシステムのあるべき姿を把握することが大切です。あなたが望む究極の草原について、隅々まで思い描きましょう。
perfect-meadow.jpg
忘れないように、これを手近な泥地に木の枝で書き残しておきましょう。理想の世界に向けて少しずつでも歩み続ければ、フェンスの向こう側を見通せるようになるでしょう。

ここでポイントになるのが、監視ツールから得られる各種の情報とヴァルハラへの憧れを組み合わせて、どの問題から対処するかの優先順位を決めることです。最大の問題を解決するのは現実的なゴールに見えないかもしれません。でも、手をつけられる障害からリファクタリングを進めれば、道は開けます。

目の前を飛び回る妖精たちが気になるかもしれませんが、妖精は無害です。そんなのを相手にする暇があったら、オーガをやっつける方法でも考えましょう。

コードは自分のもの

問題になっているところを解決することが大切です。ただ、だからといって細かいことはどうでもいいというわけではありません。細かい部分についても、同じくらい重要だといえるでしょう。
swamp-camping.jpg
ボーイスカウト・ルール日本語)として知られている「キャンプ場を去るときは、自分が来たときよりも美しくしておくこと」は、沼で生き残るための最優先ルールです。常に整理整頓を心がけ、ごみを残して立ち去らないように注意していれば、だんだんきれいな環境になっていきます。いつの日か、みなさんが待ち望んでいた元通りの草原にいることに気づくでしょう。

そのためには、ボーイスカウト・ルールに従う姿勢が大切です。きれいなコードを目指して努力しましょう。

  • あなたが気をつけなければいけません。そのコードはあなたのものであり、他の人が触る場合にもあなたと同じように扱ってもらう必要があります。 汚れや不注意を許してはいけません。
  • チーム全体でも気をつけなければいけません。プロジェクトを立て直す途中でどんなトラブルが発生しようとも、 せっかくのあなたの苦労を誰かが台無しにするようならうまくいきません。
  • 規律を守ることが不可欠です。あなた(や、他の誰か)が少しでもコードをほったらかしにしてしまうと、もはやどうにもならなくなってしまいます。
  • 少しずつ歩み続けます。もちろん、正しい方向に向かって。完璧を目指すより、少しでも進むことのほうがずっと重要です。
  • ちょっとした勝利が勢いを生む。すばらしいパッチが見られるようになってくると、それに関連するパッチもきれいにしたくなるものです。

ライブラリの構築

文化的成熟度を示すよい指標のひとつが、単位面積あたりの図書館ライブラリの数です。プロジェクトを文化的に成熟させるためにも、ライブラリを作るのはとてもよいことです。
library.jpg
どんなに足場の不安定な沼地でも、いくぶんマシな場所はところどころにあることでしょう。うまく書けているコード片を見つけたら、それをライブラリ化しましょう。そうすれば、再利用できるようになります。

いまあなたが救おうとしているコードには、何かおかしなところがあるのは間違いありません。標準やベストプラクティスの類をまったく無視していたりすることでしょう。それに従ってさえいれば、開発者たちはもっと楽に生きられたのです。それをなおしてくれる人はどこにもいません。あなたのすべきことはたったひとつ。これから新たに書くコードだけでも、標準やベストプラクティスに沿ったものにすることです。

業界標準のコンポーネントやモジュールがあれば、それを使いましょう。古いコードをいじるときにはその当時のやりかたに従わざるを得ないこともありますが、だからといって新しいコードでもそうしなければならないというわけではありません。

手に入れた道具を用いたリファクタリング

道具がそろったら、既存のコードにリファクタリングを施して、より使いやすくすることができるようになります。ずっとそれにかかりっきりになる必要はありません。古い書きかたのコードをいじっていてつまづいたときに、それを直せばいいのです。少しずつ、少しずつ。そのうちすべてが新しい書きかたに置き換えられて、旧来のコードはどこにも見当たらなくなるでしょう。

そこらじゅうに散らばっている依存関係さえなければ特に問題はないということもよくあります。そんな場合に使えるよい方法は、依存関係を含むコードをメソッドの外に出して、メソッドへのパラメータとして渡すようにすることです。そうすればコードが少しだけ自己完結型に近づき、後で移動させたり分割したりしやすくなります。

そして、コードが自己完結型になれば、テストできるようになります。

自信を得るためのテスト

泥の中を動き回るオーガのほうが、鎖につながれたオーガよりもずっと危険です。システムを手なずけるためのいちばんいい方法は、最も重要な部分(つまり、最も危険な部分)を鎖につないでしまうことです。テストを用意しましょう。できれば、自動化してビルドのたびに実行できるようにしておくことが望ましいでしょう。
http://blog.intracto.com/hubfs/Blog/refactoring-legacy/chain.jpg
システムの振る舞いをチェックするためのテストを自動化すればするほど、何かを変更することになっても自信を持って取り組めるようになります。たとえどこかがおかしくなってもテストがすぐにそれを見つけてくれて、本番環境に投入する前に修正してしまえるからです。

概要レベルのテスト

テストを書き始めるはじめの一歩として、そのシステムにおける重要なシナリオの受け入れテストを用意するという手があります。一般的なeコマースシステムなら、チェックアウト手続きなどがその対象になるでしょう。注文を受け付けることができなければ、得られるはずのお金を失ってしまいます。もし注文を受け付けた後の処理で問題が発生したら、顧客に通知を出す前に何か対応できるかもしれません。このようなテストを用意しておけば、うっかりミスで大きな問題を引き起こしたりすることがなくなります

詳細レベルのテスト

詳細レベルのテストとしてはユニットテストが使えます。依存関係をメソッドの外に出すアプローチを少しずつ進めていくと、明確な入出力を持つコードができあがります。この状態になれば、ユニットテストでそのコードの機能をカバーできるようになるでしょう。入力値と、そのコードを実行したときに期待する結果が定まれば、そのコードが期待どおりに振る舞うかどうかをユニットテストで確認できます。

何でもかんでもテストしようとはしないこと

すべてのコードを行単位で隅から隅までテストしようとするのはコスト的に難しいでしょうし、そもそもそんな必要もありません。理屈のうえでは、システムのあらゆる側面について正しいことが確信できればすばらしいことでしょう。しかし、すべてをテスト可能にした上でテストを書いてメンテナンスし続けることを考えると、どこかの段階でそのコストが得られるメリットを上回ってしまいます。自明ではないビジネスロジックと、(たとえ単純なコードであっても)誤動作が致命的な問題につながりかねない部分のテストを書いておくくらいがちょうどいいのではないかと思います。

これで、危険なモンスターの大半を鎖でつなぐことができました。沼はかなり安全になり、想定外のことも起こりにくくなってきましたね。

隔離と置換

ここまでに紹介した戦略パターンのすべてについてうまく適用できるテクニックが、昔ながらの「隔離と置換」です。

しばらくすると、内部的な挙動や副作用がはっきりしないせいで、リファクタリングを少しずつ進めていくことが難しいようなコードに遭遇することもあるでしょう。そんな場合は次の手順で進めましょう。

  1. やっかいな部分を隔離して、別のメソッドやクラスにします。
  2. 依存関係を外に出して、パラメータとして受け取るようにします。
  3. 副作用を伴うロジック(データベースへの保存など)を、段階的に切り離します。
  4. そのロジックを隔離して、別のメソッドにします。
  5. ユニットテストを追加して、そのロジックの振る舞いをカバーできるようにします。
  6. ロジックを書き直します。ユニットテストがパスすれば、少なくとも旧バージョンの挙動と同じように動いていることはわかります。
  7. 旧バージョンと新バージョンのロジックを、同時に本番環境で動かします。この時点では旧バージョンがまだ使われている状態です。
  8. 新バージョンと旧バージョンの出力の違いをログに記録します
  9. ログを精査して違いが出た原因を調べ、その違いがなくなったときにパスするような新しいユニットテストを書きます。
  10. 新旧の両バージョンが同じ動きになるまでこれを繰り返します。
  11. 安心して、旧バージョンのロジックを新バージョンに切り替えます

このテクニックを使えば、モーターを交換するときにクルマを壊してしまう心配がなくなるし、そもそも内部的に何かが変わったことすら気づかないでしょう。

自分の説得

ここまでに紹介した心構えやツールは、災害から守るためにシステムの舵を取るためのうまい方法であることがわかっています。
dragon-yin-yang.png
最初の頃の私は、レガシーコードに立ち向かう作業にフルタイムで長期間かかわることを躊躇していました。しかし今では、いろんな居意味で開発者として成長するためのよい経験だったと考えています。

  • 悪いコードを見つけられるようになります。大半の時間をその対応に費やしてきたのですから、何が悪いのかを見抜けるようになります。新しいプロジェクトにかかわることになったとしても、ここで説明したパターンを意識することになるでしょう。今のコードが将来の自分自身を苦しめることになるかもしれないとわかっているからです。
  • 基本に戻ります。私の場合は、システムのあらゆる側面について、質問をしたり自分で確かめたりする必要がありました。最近のフレームワークがいろいろな課題を肩代わりしてくれることをありがたく感じるでしょう。
  • システムとともに成長します。手がけているシステムが成長するにつれて、新たな関門が現れます。システムを使う人が増えれば増えるほど、対処すべきボトルネックが見えてきます。さらにスケールアップする必要も出てくるでしょう。
  • 人はみな、新たなレガシーをこつこつと生み出し続けるものです。海がゴミだらけになる前にどうすればいいのかを知っていることは、持つべきスキルのひとつです。
  • 楽しくてやりがいがあります。歩んできた道をふと振り返ったときに、今のシステムが数年前にはじめて見たときとは見違える姿になったことに気づくでしょう。大きな達成感を得られます。

meadow-cow.jpg

この(少なくとも分量だけは)充実した投稿のまとめに代えて

沼の中を恐る恐る歩きながら、誰かに助け出されてディズニーランドに連れていってもらえることを夢見るのもいいでしょう。でも、自分の手で事態を何とかすることもできます。沼のオーナーとして、自分のルールに従わせていけばいいのです。どんな沼地でも、底には硬い地面があります。あなたにもきっと、見つけられるでしょう。

エピローグ:

数年後、かつては沼だった草原を散歩するあなた。向こうのほうでは公爵が、かつては「ここにドラゴンがいる」だった看板をビールの広告に書き換えている。

満面の笑みをうかべながら。