はじめに
機能追加のたびに、既存コードが少しずつ読みにくくなっていく。そんな感覚を持ったことはないでしょうか。散らかりは一度に発生するのではなく、一つひとつの「あとで直そう」が積み重なって生まれます。そして積み重なるほど、変更のたびに支払うコストが増えていきます。
この記事では、散らかりを放置しないために日々どう整頓を回すかを、実際のコード例とともに整理していきます。考え方の下敷きにはKentBeck著の「Tidy First?」があります。
構造の変更と振る舞いの変更を分ける
出発点は、コードへの変更を2種類に分けて捉えることです。
- 振る舞いの変更: システムの動作そのものを変える。機能追加やバグ修正がこれにあたる
- 構造の変更: 動作を変えずに、コードの並びや形を整える。いわゆる整頓・リファクタリング
この2つを意識して分けるだけで、日々の作業がかなり見通しよくなります。たとえばプルリクエストのレビューで「この変更、機能は変わっていないのに差分が大きくて何が起きているのか読めない」と感じるのは、多くの場合、構造の変更と振る舞いの変更が同じコミットに混ざっているからです。
整頓は、次の振る舞いの変更を楽にするための準備として行います。「はるか先の未来のため」ではなく、あくまで当面のニーズに応えるための投資という位置づけです。
整頓のカタログから、実務で効くもの
整頓の手法にはいろいろありますが、ここでは自分が実務で特に効くと感じたものを実例とともに挙げていきます。
ガード節で事前条件を先に片付ける
コードの細部に入る前に、満たすべき事前条件を先頭で捌いてしまう手法です。ネストが深いと、いま自分がどの条件の中にいるのかを追うだけで読み手の頭を使わせてしまいます。
# before: 本筋の処理が深いネストの奥に埋もれている
def apply_discount(user, price):
if user is not None:
if user.is_active:
if price > 0:
return price * 0.9
return price
# after: 例外的なケースを先に返して、本筋を平らにする
def apply_discount(user, price):
if user is None:
return price
if not user.is_active:
return price
if price <= 0:
return price
return price * 0.9
本筋の処理がインデントの一番浅いところに来るので、「この関数が結局なにをするのか」が読み取りやすくなります。
デッドコードは消す
実行されないコードは、消すだけです。「いつか使うかも」で残したコードは、読み手に「これは生きているのか」という余計な確認作業を強います。
ここで一つ、最近強く感じることを補足しておきます。AIにコードを書かせると、部分的な文脈だけを見て処理を組み立てた結果、実行されないコードが紛れ込むことがあります。到達不能な分岐、二度と呼ばれない関数、使われないまま宣言された変数などです。
対策はシンプルで、AIに書かせたあとに、もう一度AI自身か人間のレビューを通すことです。「書く」と「点検する」を別の工程として分けるだけで、デッドコードはかなり減ります。バージョン管理があるのですから、消して困ればいつでも戻せます。
説明変数と説明定数で意図を残す
式は放っておくと成長します。小さく始めた条件式も、要件が増えるたびに項が足され、やがて一目では読めなくなります。そこで、苦労して読み解いた意味をコードに書き戻すのが説明変数です。
# before: 条件が何を意味しているのか、読み解かないと分からない
if user.created_at < timezone.now() - timedelta(days=365) and user.orders.count() > 10:
grant_loyal_customer_badge(user)
# after: 条件そのものに名前を与える
is_long_term_user = user.created_at < timezone.now() - timedelta(days=365)
is_frequent_buyer = user.orders.count() > 10
if is_long_term_user and is_frequent_buyer:
grant_loyal_customer_badge(user)
同じ発想で、コード中に直接書かれた数値、いわゆるマジックナンバーには説明定数で名前をつけます。
# before
if len(password) < 8:
raise ValidationError('パスワードが短すぎます')
# after
MIN_PASSWORD_LENGTH = 8
if len(password) < MIN_PASSWORD_LENGTH:
raise ValidationError('パスワードが短すぎます')
8 が何を意味するのかが名前から分かり、同じ値を複数箇所で使うときの取りこぼしも防げます。
凝集の順番を整える
振る舞いを変えようとしたとき、コードのあちこちに散らばった箇所を触らないといけないと気づくことがあります。そういうときは、まず変更対象の要素が隣り合うように順番を入れ替えておきます。変数の宣言と初期化は使う場所の近くにまとめ、関連する処理は近くに寄せる。散らばりを先に集約しておくと、続く振る舞いの変更が一箇所で済むようになります。
ヘルパーの抽出、ただしやりすぎない
他のコードとの相互作用が限られているコードブロックは、ヘルパールーチンとして切り出します。名前をつけることで、その塊が何をするのかを呼び出し側で説明でき、本体の見通しがよくなります。
ただし、小さな部品に分けすぎると、今度は全体像が追いにくくなります。分割のしすぎを疑うべき予兆としては、次のようなものがあります。
- 長くて繰り返しの多い引数リスト
- あちこちに繰り返されるコードや条件式
- ヘルパーに付けられた要領を得ない名前
- 複数箇所から共有される可変なデータ構造
こうした兆候が出てきたら、分割の粒度が細かすぎないかを立ち止まって考えるようにしています。
コメントは「コードから読み取れないこと」だけ
コメントには、コードからは読み取れない背景や意図を書きます。逆に、コードを読めば分かることをなぞるだけのコメントは削除します。return x の上に「xを返す」と書いても、読み手の時間を奪うだけで何の価値もありません。
いつ整頓するか、判断する
手法を知っていても、実務で難しいのは「いつやるか」です。答えは常に「状況による」ですが、判断の軸は整理できます。
先に整頓するかどうか迷ったら、自分に次の質問を投げかけます。
- この散らかった箇所を変更するのは、どのくらい大変か。整頓しても変更が楽にならないなら、先に整頓しない
- 整頓の利点をどのくらいすぐ得られるか。今はまだコードを理解するために読んでいる段階だとしても、整頓すれば理解が早まる。それなら先に整頓する
- この整頓はどう償却されるか。一度しか変更しないコードなら整頓は控えめに。何年も毎週見返りがあるなら整頓する
- その整頓にどのくらい自信があるか。「ここを直せば変更が楽になる」という確信があるか、それとも憶測か
これを踏まえると、整頓の選択肢は次の4つに整理できます。
- 整頓しない: このコードを二度と変更しない、あるいは設計から学ぶことがない場合
- あとで整頓する: いま整頓するとかえって高くつく場合。区切りをつける意味でも、変更の直後に手を入れる
- 改めて整頓する: すぐには見返りのない大きな塊を、小出しに整頓していく場合
- 先に整頓する: すぐに見返りがある場合。理解が深まる、続く振る舞いの変更が楽になる、そして何をどう整頓すればいいか分かっている場合
「壊れていないなら、直さなくていい」という判断も、本当に静的で今後変更しないシステムに対しては合理的です。整頓は義務ではなく、あくまで投資対効果で選ぶものだと考えると気が楽になります。
バッチサイズを小さく保つ
整頓を実際に回すうえで効いてくるのが、プルリクエストの大きさです。ここにはトレードオフがあります。
- 巨大なプルリクエスト: 全体像は掴みやすいが、レビュアーが有用なフィードバックを返すには大きすぎる
- 小さなプルリクエスト: 細かいフィードバックを促せるが、枝葉末節にとらわれるリスクもある
ここで効いてくるのが、レビューのコストを下げてバッチサイズを小さく保つことです。それによって整頓そのもののコストも下げられます。構造の変更と振る舞いの変更を別のプルリクエストに分けておくと、レビュアーは「これは動作を変えない整頓」「これは動作を変える変更」と切り分けて読めるので、レビューが一気に楽になります。
同時に、変更しすぎにも注意が必要です。一つの整頓の失敗は、うまくいった整頓の連鎖よりもコストが高くつきます。小さく、確実に、が基本方針になります。
整頓は「オプションの購入」
整頓を投資として捉えると、こういう見方ができます。今日の設計は、明日の振る舞いの変更を安く「購入」できるオプションに対して支払うプレミアムだ、というものです。
ソフトウェアの設計判断のほとんどは、簡単に元へ戻せます。間違えても戻せるのだから、間違いを避けること自体にはそれほど価値がない。ならば、間違いを避けるために過剰に投資するのではなく、小さく試して違ったら戻す、という進め方のほうが理にかなっています。
そしてコストのかかるプログラムは、一つの要素を変えるために他の要素まで変えないといけない。コストのかからないプログラムは、局所的な変更で済む。つまりソフトウェアのコストを下げるには結合を減らすことになるわけですが、分離もタダではなく、そこにもトレードオフがあります。「料理をしながらでもキッチンは美しく保てる」という言葉のとおり、作りながら整えることを日常の一部にしておくのが、結局は一番コストが低いのだと思います。
おわりに
整頓は、遠い未来のための壮大なリファクタリングではなく、次の一手を楽にするための小さな投資です。構造の変更と振る舞いの変更を分けて意識し、判断の軸を持って、小さなバッチで回していく。この基本を体に馴染ませることが、散らかりを放置しないための一番の近道だと感じました。
最後に一つ、実務への応用として考えていること。ここで挙げた整頓の観点は、AIにコードを書かせる際のSKILLやレビュー用のプロンプトに組み込む価値がありそうです。ガード節の有無、デッドコードの検出、マジックナンバーの排除といったチェック項目を渡しておけば、AIが生成したコードの散らかりを、生成の直後に自動で整える運用にできるはずです。整頓を人間の心がけだけに頼らず、仕組みに落とし込んでいく。それが次に試してみたいことです。
参考文献
