巨大な(あるいは、汚くて邪悪な)コードの泳ぎ方

ロンドンへの飛行機(11時間)で暇だったから書いた文章。

自分でゼロからすべてのコードを書けるときはテストファーストでいいけど、アンドキュメントな実験的なライブラリを利用する際や、巨大なプロジェクトの一部としてコードを書く際は、テストファーストよりもとにかくコードを書きまくって挙動の変化を確かめるほうが有用な時がある。

まあ多分どっかでこういうのはハウツー化してあるんだろうけど、自分ルールが固まってきたので、メモっておく。

目的を設定する

トップダウンに読むには、コスパが悪いことが多い。とにかく「アレする」「コレする」という目的を定義して、そのためにその周辺領域からボトムアップに読むことにしよう。

エンドポイントを追う

巨大なプロジェクトに放り込まれた最初の段階では、エンジニアは本当に無力だ。

最初にやることは、自分が処理を挟むべき位置を見つけることだろう。 まずはファイル名や関数名を読んであたりをつける。臭ってる箇所のコードを読んで「らしさ」を嗅ぎ取る。ここらへんは正直センスだと思う。そのコードベースに精通すればするほどみつけるのが早くなる。

ゲームなどのGUIがあるアプリならば、表示されている文字列でgrepしてみる。ビューが引っかかるか、そのビューに文字列を与えるコントローラが引っかかるはず。外部から読み込まれる場合はちょっとつらいが、そもそも外部から読み込まれるものと直感できる場合は追う必要はない。

ここかな?と思った場所でデバッガを突っ込む。起動すれば成功。だめなら上に戻る。

返り値や副作用を確認

テストがあればテストを読む… のだが、最初の段階で何をテストしているかわからないケースが多々ある。その言語のテストを読み慣れていないか、そもそもテストの品質が低いとか、いろんな理由がある。

自分の場合は、grepでその関数を呼びだす位置や、引数を確認する。 またその関数が起こす副作用と、どういう返り値を返すと戻り先でどういう処理が行われるか、直接ハードコードしてみて確かめる。

とにかく気合で実装する

上の操作で何が行われるか50%ぐらい理解したら、プリントデバッグで与えられたコンテキストや状態を確認しながら、とにかくコードを書きまくる。 汚くてもいい。とにかく目的の状態や出力を作ってしまう。ついでに書きながらあとで修正すべき点をTODOコメントで撃ちまくる。

テストがある場合は、自分のコードが追加された段階で、テストが通ることを確認する。もしテストが落ちたら、そのテストは自分が通したい挙動と相反しないか?を考える。そもそも仕様変更ならば元のコードを破壊するはずだから、破壊したということは自分の意図は達成されている。テストを書き換えて、この時点で一度コミットするが、自分は wip about ~ というそのままコミットできない文字列にしている。あとで直せという圧力を自分にかける。

この時点ではだいぶ綱渡りな実装になっている。(この時点でコミットする奴はどうしようもないクズだ)

分割とリファクタリング

※ こっから要はREG/GREEN/REFACTORINGで、目新しいことはなにもない。

処理の塊に名前をつけて関数にする。もし頻出パターンで、意味性を損なわないならば、共通ルーチンにする。この意味性というのが大事で、無関係で似ているだけのものを一箇所で処理すると、いずれポリフィーズムが失われifの羅列になっていく。

テスト可能性や、凝集性を意識する。同時に出現する変数を一箇所にまとめる。

もし、ある種のエンドポイントで綺麗にするのが難しい処理の塊が出現したら、それはきっと本質的に難しい操作だと思うことにしている。(もちろん、自分の実力不足でそう見えているだけのときもある)

そうなってしまうとリファクタリングは困難で、関数には「こいつを触ると危ないぞ」というニュアンスの名前をつける。できるだけ小さい関数のスコープに押し込んで、副作用を可能な限りゼロにし(それができないから難しいことが多いのだが)、なぜそうなのかというコメントを大量に書く。

たとえば、自分は setupGlobalInstances という関数を書いたことがあるが、GlobalInstancesが何かを長々と書いていては関数名がひどいことになり、かつ分割するほどのロジックもなく、しかしお互いが微妙に依存しており、副作用が多いので、あんまり触るなという意図を込めてこの関数名にした。

こういうことをやる際、コメントがキモで、そもそも関数名で何をやるか明確な場合はコメントは不要なケースが多いと思うのだけど、それが不可能な場合はコメントで何を行うか実装者に担保するしかない。逆に言うと、何をやるかわからない関数名をつけてしまっている場合は、語彙力が不足しているか、そもそも関心の分離ができていない。

テストの追加

関数単位でテストで保護する。 他人に壊されては困る、という意図を押し込む。それはきっと自分のためにもなる。

仮にだけど、もしテストを書かないというプロジェクトの場合、挙動を確認するためのコードを実際に処理が行われるエンドポイントにその場限りのテストを書いて、assertしてもいいだろう。doctestみたいなものだ。で、コミット時にはテストコードを外すが、自分用の秘蔵のタレとして手元に持っておく。

そもそもテスト可能な粒度で分割された関数ならばテストを書く必要がない。という意見もあるぐらいなので、テストを書くという行為を経ていれば、それはある程度マシなコードだ。もちろんテストを書くに越したことはないが。

リファクタリングのローラー作戦

一度リファクタリングあとでコードを眺めると、ある種の規則性が見えてくるかもしれない。今まで見えなかった秩序が表出している。 その規則性を読み取って、もう一段階抽象化する。もちろん、速すぎる抽象化には注意する。秩序が見えそうな気配が見えたけど、それほどでもないときは、コメントを打って終わり、でもいい。

一度リファクタリングするたびに amend する。最後に、rebase して reword して自分がやったことに対して名前をつけて、終わり。

客観的になる

一度書き終わったコードでも、コードレビューツール上でみると新たな発見があるものだ。他人のコードをレビューしているつもりで自分のコードを眺めてみて、このコード糞だろと思ったのを自分でレビューうって自分で修正する。

書いてみて思ったけど、当たり前のことを、当たり前にやろう、という気がしないでもない。