2011-01-10

レガシーコードをライブで扱う際のポイント試案

twitter で TDDBC Hokuriku (2010) のレガシーコード改善を Coding Dojo で行った際の Ruby チームは比較的うまくいってたけど、あれって○○な流れだっけ的な話をしているうちに気になってることをまとめておこうと思い立ったので、できるだけ書き出してみる。

何かのきっかけになれば嬉しい。

素材(レガシーコード)のポイント

  1. まず動くこと
  2. 触ったことがあること1
  3. ある程度でいいので機能別に書かれていること
    • オブジェクト指向であるとなお良い(使える技が増える)
  4. 小規模であること
    • ただし完全に単機能だと余地が少ないのでテストを足しにくい
    • 外部 API 依存しまくりの場合は単なるレガシーコード改善とはまた別なテクニックの習得に繋がってよいかも
  5. 自動実行できるテストがないこと :-)

1 については「えっ」て思うかもしれないけど、放置してるものは依存ライブラリの関係や、そもそも動かし方がよく分からない(覚えてない)など、動かないことがよくあるので、素材を事前に用意できるならこの部分をチェックしてあるとだいぶ話が早い。

文字列処理、数値処理などテストしやすい機能が独立していると改善は早いけど、あまりきれいに分離されていると分離するまでに使える技をおみまいできないので、悩ましいところかも。

ただライブで扱う際はどうしても時間的制約が大きいのであまりに難しい題材を扱うのはまず無理だと思う。

事前準備のポイント

素材を用意する人は

  • コードの読み込み
  • 動作テスト

を入念に行っておくのがオススメ。

特に自分で書いたわけではない(オープンソースなどの)レガシーコードを相手にするのは難易度高めなので事前準備が大切になってくる。

この部分は TDD の知識の有無、経験値は関係なく準備できる。古典的な「読ん」で「動かす」でよい。できれば「こうしたい」という要求が挙げられるとよいけれど、それがイベント内で達成可能なゴールになるとは限らない。

これはそういうもんだと思っておくとあまりガッカリしないで済む。なんでもそうだと思うけど、達成可能なゴールの設定にはある程度経験が必要。

レガシーコード改善手順を復習

レガシーコードをレガシーコードでなくすこと自体が目的ということはそれほど多くないと思う。継続してメンテナンスが必要だけど修正ポイントを見つるのが大変、リグレッションテストのコストが大きすぎて大変、てゆーか影響範囲が判断不能、といった事情があるはず。

そこでテストを付加していくわけだけど、その手順を書き起こすと大きく三つ、

  1. テスティングフレームワークに載せる
  2. characterization test から徐々に TDD を回す
  3. 修正、機能追加やリファクタリングを目指す

に分けられると思う。

サイボウズの記事のようにユニットテストよりも Selenium レベルのテストを重視するという方針ならそもそも話が変わってきてしまうんだけど、基本的には TDD を回すと言った場合はある程度「ユニットテスト可能」な状態を指すと思うし、Selenium 重視になるとテストデータの投入の自動化とかそもそも Web アプリじゃなかったらどうするのとか、クリアすべき課題がさらに増えてしまうので、ユニットテストを付加していく方向で考える方がイベント時の手順として汎用的に使えるだろう。

レガシーコード改善チーム編成のポイント

レガシーコードの改善をイベントでやろうとする場合、

  • ペア以上の人数で一つの素材に取り組むチームを作る2

そのチーム内に

  • TDD に慣れている人3
  • コードに習熟している人

の両方を置く

という形がベストだと思う。どちらが欠けてもイベントの限られた時間内でゴールに到達するのは難しくなるだろう。

理想的には上に挙げた人が一人ずつのペアを作れると最も速度が出て効果的に学習できるのだが、そんなに大量に TDD エキスパートが揃うとは思えないので、チーム内に一人ずつを目指すのがよいと思う。

ゴールは素材とイベントの規模とチーム編成次第

さて準備が整ったところでレガシーコード改善に実際に取り組むわけだけど、個人的には先に挙げた 3つのどのステップをゴールに設定してもよいのではないかと考えている。

どこをテスティングフレームワークに載せるのか

レガシーコードすべてを一度にテストに載せるのは多くの場合で無理なので、特にどの部分をテストして、修正、機能追加、リファクタリングに結びつけるかを考える。この際、ややこしい依存がいくつもある部分を選んだ場合はテストハーネスに載せるまでの苦労を実践的に味わえる。

現実の「どうにかしたい」レガシーコードの中にはそういうとてもややこしいことになってしまっているものも数多くあると思う。とにかく片っ端からそうしたコードをやっつけていきたいので、まずはここを強化したいと考え、あえてテスト開始の難しい素材でやるという選択もアリだと思う。

というかそういう素材しか見つからない場合だってあり得る。その場合はテスト可能な状態を作るだけでも良しする判断もあるんじゃないか。2〜3時間から半日程度の規模ならここまでで終えてしまうケースも増えると思う。

例えばコンストラクタが頑張りすぎてて依存オブジェクトが多い場合はまず依存オブジェクトを初期化する処理をそれぞれ別メソッドに分ける、外部のライブラリをあちこちで呼んでいる場合は wrapper にまとめる4、本当に果たすべき一つの責務を見つけ出す、継承してテスト用クラスで override して依存を分離する、など身につけたいテクニックはたくさんあるわけだが、多くの場合、一時的にはコードが膨らんでしまうし、この作業に掛かる時間を事前に読み切るのは難しい。

逆にすべてを体験したい場合には

ややこしい依存のないコード、あるいは依存を分離しやすいコードを選ぶ

ことになる。

もう一つ、テスティングフレームワークに載せる部分は修正、機能追加、リファクタリングを行う部分なので、2番目以降のステップに繋げやすいという要素も重要になってくる。

例えば機能としては独立しているのでテスティングフレームワークに載せるのは比較的容易だが、外部あるいはブラックボックスの API 呼び出しだらけとかメソッドがちゃんと入出力を持つ関数として機能せずに副作用だらけで動くようなものは TDD の入門素材としてはあまり適していない。(無理じゃないけど。)

TDDBC Hokuriku 2日目の Coding Dojo Ruby チーム は比較的うまくレガシーコードの改善が進んだのだが、このときの決め手の一つは t_wada

「パーサがありますね! パーサいいですね! パーサにしましょう!」

だったと思う。この決断は TDD への習熟から生まれてきたものだろう。

characterization test から徐々に TDD を回す

レガシーコードはテストコードのないコードなので、当然機械的に確認可能な仕様が存在しない。仕様書はあるかもしれないが、それが現在の動作と合っている保証はない。そこでまず現状把握のためのテストを書く。これを Characterization Test と呼ぶ(『レガシーコード改善ガイド』第13章)。テストするのはもちろん変更を加えたい部分とその周辺。

これがすんなりうまく行くようならしめたもの。その後の修正、機能追加、リファクタリングに集中できる。

しかしたぶんそんなにうまくいかない。そのメソッドが想定通り動く条件をすぐに満たせるかどうかが分からない。インスタンス生成はうまくいってもメソッドの呼び出し時にはいきなり例外が上がる可能性もある。また、目的のメソッドは副作用を基本にしたメソッドかもしれない。そうなると検出用変数の追加など、テストなしで加えなければいけない変更が出てくるかもしれない。

目的のメソッドをテストできるようになったらようやくそこがスタート地点。境界値など一般的なテストを書いて仕様を確認していく。

「現場」においてはこの段階は次のステップへの繋ぎなのでゴールにはならないのだけれど、これもイベントにおいては状況次第かなと思う。次のステップへ進むために遠回り(目的とは異なるメソッドのテストや stub out など)が必要な場合も考えられる。そうなってしまうとどこでその遠回りが終わるかの予想が難しいこともあり得る。

修正、機能追加、リファクタリングを目指す

修正、機能追加を行うためにはその部分をテストでガードしましょうというのが TDD の基本である。(それ以外の部分もガードできないと本当はダメだけど、そこは置いておく。)ということでガードできたところからプロダクトコードをいじり始める。ただし、レガシーコードをいじる場合、

影響範囲が閉じていて簡単に変更できるなんて平和なことは少なく

  • 影響範囲は小さいがメソッドは巨大で if や case の嵐
  • 一つのメソッドで多くのインスタンス変数やメソッドを呼び出している

なんてことがザラにある。要するに

レガシーコードはクラスやメソッドの仕事が多すぎる傾向にある。

方針としては

仕事(責務)を一つに減らすことが理想

と言えるが、これは恐らくそれほど簡単には実現できない。つまり、リファクタリングをゴールにする場合は結構時間が掛かることを覚悟しなければいけない。

だから修正や機能追加をゴールにする方が時間設計上は楽だと思う。

それでもリファクタリングしたいという場合、「リファクタリングのための準備」くらいで我慢しなければいけないこともあり得ると覚悟しておこう。例えば

  • 不要なコードは削除する
  • メソッド丸ごと別クラスに分離する
  • if, case の嵐は実際の処理を別メソッドに分けて、override できるようにする

など。

上の文字だけを見ると単純な話のように見えるけど、これはこれでちゃんと準備して施術する必要があり、かつ効果は予想以上に大きい。場合分けすると複雑で長くなってしまうコードも、override を前提にすれば個々の処理はそれほどでもない場合は多いし、独立クラスに追い出してしまうと結果的にコードの長さが長くなったとしても、可読性が高く短い処理に分割しやすくなる。

また試行リファクタリングも効果的。これは実際には捨ててしまうことを前提に理想的なコードを組んでみることで、クラスやメソッドが本来果たすべき責務を明らかにするために行う。ただしこれを踏まえて作業できるとは限らない。非常に根の深い問題を抱えている場合は試行リファクタリングで見つけたような気がした理想的な状態をいったん捨てて5、地道に取りかかれるところだけを作業しなければいけないこともあり得る。実際にできあがったコードだけを成果として捉えてしまうとこれはとても寂しいが

できないことが分かる

ことも成果である。

まとめ

個人的には TDD のキモの一つは依存の分離だと思う。テストしにくい依存はどんどん分離してテスト対象の動作に集中して回転を上げていく。これができるからこそ TDD はリズムが良く、小さなミスの発見、小さな設計変更、小さな達成をくり返すことができる。

これはレガシーコードを相手にする際により大きな意味を持つ。レガシーコード特有の問題ではないが、レガシーコードは往々にしてブロック6が大きすぎ、それぞれの責務が曖昧で名前もぼんやりしている傾向にあり、依存ライブラリを含めて本番と同じ動かし方しか想定していないので分離の難しい傾向にある。だから TDD で書き始めたコードよりも

大胆な依存分離テクニックと発想、そのための調査

が大事になってくる。

自分にはそれほど多くのレガシーコード改善経験があるわけではないが、レガシーコードとの戦いに有用な依存分離テクニックは伝統的なオブジェクト指向の教科書的な方針とぶつかることがあることは知っている。しかし多くの場合で教科書を補足する原則には反していない。つまり、実はオブジェクト指向の周辺知識がとても有効な武器になる。

レガシーコードとの戦いはつらい。だがレガシーコードを切り刻んで TDD 可能なコードに改善する作業も楽ではない。これは総力戦だと思う。レガシーコード改善のためには TDD への習熟も必要だし虫食いの知識を補強する必要もある。そしてこれをイベントで扱う場合は「楽しかった!」「勉強になった!」と簡単に言えないケースも想定しておくことが大切だと思う。

最後のまとめの発表では「ここまでしかできなかった」という気持ちにならないこと。「この課題はここが難しかったがここまで進むことができた」と締めくくろう。

あ、『レガシーコード改善ガイド』はオススメですよ。これを読むと

勇気が手に入る。

諦めていたレガシーコードともう一度戦ってみようと思える。同時に、何が大事なのか、捨ててよいものは何かを考える契機にもなるし、戦って負けてもそれはそれで別な方法で再戦しようと思えるようになる。

自分が段階的なゴール、段階的な成果を前提にこのエントリを書いているのもこの本と TDDBC Hokuriku (2010) の影響だと思う。

ありがとう。

参考

  1. 読むだけでなく、ここをこうしたい、と思える程度にはコードをいじったことがないとテストを付加して何を行うのかが見えてこない 

  2. 一人で学ぶよりその方が早い 

  3. できればレガシーコード改善の経験のある人 

  4. この場合は元のレガシーコードにはまず触らず、wrapper だけを独立して TDD で作り、のちにこの wrapper と統合するのがよいだろう。 

  5. つまり書き上げたコードだけでなく理想的な設計と思えた形も両方捨てる 

  6. if, case, メソッド、クラスなどなど 

About

例によって個人のなんちゃらです

Recent Posts

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Document SVG AsciiDoc Pandoc DocBook Develop Jekyll macOS Node.js Vite Heroku Transformer AI Data Cloud Wasm