「意味の圧縮」から考えるコード設計
プログラミングにおける良い設計とは何か。この話題は定期的に議論されるが、多くの場合は結論の出ないまま終わる。袋小路のような不毛さのあるテーマだ。
とにかく世の中にはおびただしい量の設計に関する考え方が溢れている。
実践的なテクニックなのか、抽象的な思想なのかも曖昧な「ナントカ指向」。
何を成した人物なのか詳しく知らないが、とにかく有名らしい誰かの格言。
経験則をそれらしく一般化して「原則」と呼び、無条件に当てはめようとするもの。
DRY、KISS、YAGNI。1名前は知っていても、いつどれを優先すべきかは誰も教えてくれない。
それらすべてに真面目に向き合っていたら、それだけで人生が終わってしまうだろう。 だからといって何の指針も持たずに良し悪しを判断し続けるのも困る。
では、設計判断の軸はどこに置けばよいのか。
それは、プログラムが表現している「意味」そのものだと思う。
コードは常に途中であり続ける
ソフトウェアは完成しない。機能は追加され、仕様は変わり、環境は変化する。メンテナンスが止まるまで、コードは変わり続けてゆく。
このコードの成長には段階がある。
- とりあえず動くものを書く段階
- 似た処理が増えてくる段階
- 意味の重複が明確になる段階
新規プロジェクトでも、機能追加でも、大規模改修でも、どんな変更でも基本的にはこの流れを辿るだろう。
つまり設計とは、完成形を一度決めて終わる作業ではない。構造の固定ではなく、構造の更新だ。どの段階にいるかによって、取るべき判断が変わる。
設計から入るのではなく、まずはゴールを決めて、実際に作ってみる。将来の構造を予測して先回りするのではなく、目の前のコードに現れた事実を観測し、そこから構造を導き出す。それが設計の出発点になるはずだ。
意味の圧縮としての設計
実際にコードを書き始めると見えてくるものがある。変更の重さには明確な差があるのだ。DBのデータ構造の変更、公開APIの修正、プログラミング言語の切り替え。こういったものは技術的には可能でも、影響範囲が広く重い。2
つまり設計で考慮すべきことには、気軽に試せる変更とできればやり直したくない変更が混在している。重い決断はできるだけ先送りし、軽い決断は素早く試す。
軽い決断を繰り返していくと、コードは自然と増えていく。似たような処理があちこちに現れ、同じ意図の処理が重複し始める。
この段階で中心に据えるべきなのが、「意味の圧縮」だ。
意味の圧縮を、
プログラム中に散らばっている同一の意味の処理を、
より少ない表現でより明確に表すこと
と定義しよう。
この概念はCasey Muratoriが「Semantic Compression」と呼んだものに基づいている。3
これは単なるコードの短縮や共通化とは着眼点が異なっている。対象としているのはコードの見た目や処理の仕方ではなく、そのプログラムの意味そのものなのだ。
初期段階でのコードは冗長でばらばらで重複している。それは失敗ではなく観測のために必要な状態だ。
何が共通で、何が偶然で、何が将来変わりうるのか。この段階ではそれはまだ分からない。
時間とともに使われ方が固まっていき、同じ意図の操作が何度も現れたとき、初めて「これは同じ意味だ」と判断に入る。
その瞬間に行う統合こそが、意味の圧縮であり、設計だ。
共通化はなぜ壊れるか
コードの重複を見つけたとき多くの人がまずやるのは共通化だ。似ている処理を1つにまとめる、一見すると合理的に見えるだろう。
しかし共通化とは依存関係を作る行為だ。2つの処理が1つの実装を共有すれば、片方の都合による変更がもう片方に波及する。たまたま見た目が似ているだけのコードを共通化すると、この依存が足かせになる。コピペで独立させておく方が正解だった、ということは普通にありえる。
同じことをしているように見えて、実は意味が違うというケースは多い。2つの画面で同じ「YYYY/MM/DD」形式の日付フォーマット処理が書かれているとしよう。コードも同じ、目的も「日付を整形する」で同じだ。しかし片方はユーザーへの表示用で、もう片方はAPI送信用だとしたら、意味は違う。表示用が「1月5日」のような形式に変わったとき、API側が巻き込まれてはいけない。
ECサイトの「会員割引」と「クーポン割引」という例も考えてみよう。計算ロジックが似ているからと共通化したとする。しかし「クーポンは送料には適用しない」というルールが出てきた瞬間、共通化した関数に条件分岐が増え、どちらの割引の話をしているのか読み取れないコードが出来上がる。会員割引とクーポン割引は最初から別の概念だった。「割引」という名前においては同じ意味かもしれないが、実際には違う概念なのだ。4
理想的には「変更の理由が同じかどうか」で判断できればいい。変更の理由が同じなら意味が同じ、違うなら意味が違う。しかし実際には、変更の理由を事前に見抜くのは難しい。構文、処理の目的、扱うデータ、使われている用語。どれだけ共通点があっても、それが本質的に同じものなのか、たまたま似ているだけなのかを区別しきれない。
区別しきれないまま共通化すると、後から意味の違いが表面化したときに壊れてしまうかもしれない。依存関係を作ってしまった後では、条件分岐やフラグで差異を吸収するしかなくなる。だからこそ、意味が同じだと確信できるまではコピペで独立させておく方が安全だろう。
抽象化と呼ばれているもの
ここまで読んで、「それは抽象化の話ではないのか」と思った人もいるだろう。
実際、意味の圧縮と本来の意味での抽象化は、同じようなものだろう。具体的なコードの中から「これらは同じことをしている」という意味を発見し、それに名前を与える。これが抽象化だ。
しかし現実には、「抽象化した」と言いながら実際にやっているのは共通化でしかない、というケースが非常に多い。コードが似ている、目的が同じに見える、同じ名前がついている。だからまとめた。それだけだ。意味が同じかどうかは見落とされやすい。
本来の抽象化は、こうした表面的な類似ではなく、意図の一致を基準にする。APIへのGETリクエストとPOSTリクエストで、認証トークンの付与やエラーハンドリングが別々に書かれていることがある。コードの見た目は違う。しかし「認証付きでAPIを叩き、エラーを処理する」という意味は同じだ。これを「認証付きAPIリクエスト」として統合し名前を与えるのは、共通化ではなく抽象化だ。
意味の圧縮とは、共通化ではなく抽象化の側に立つことだ。
設計原則が衝突して見える理由
冒頭で触れた「原則」の話に戻る。DRY、KISS、YAGNI といった原則自体が悪いわけではない。問題は、それらを常に同時に満たすべき規則だと捉えてしまうことだ。
初期段階では、
- YAGNI:余計な構造を作らない
- KISS:単純な実装を保つ
ことが最優先になる。
意味の重複が十分に観測された段階では、
- DRY:同じ意味は一箇所に集約する
ことが重要になる。
原則が衝突しているわけではない。
コードの成長段階に応じて、発動する原則が変わるだけだ。
そして、その切り替えを判断する基準が「意味の重複」になる。
具体的に考えてみよう。「ユーザー一覧画面」と「商品一覧画面」で、テーブル描画・ページネーション・ソートがそれぞれコピペされている。初期段階ではこれでいい(YAGNI、KISS)。しかし3つ目、4つ目の一覧画面が増えてきて、同じ修正を何度も繰り返すようになったら、「一覧を表示する」という意味の重複が明確になったということだ。ここで初めてDRYが発動する。
設計とは時間の中で行われる圧縮作業である
設計とは最初に完成図を描くことではない。
観察と修正を繰り返しながら、表現を少しずつ洗練させていく過程だ。
- まず具体的に書く
- 次に意味を観測する
- 重なった意味を圧縮する
この循環が続く限り、設計は続く。
設計とは構造を作る作業ではなく、意味を発見し、言語化し、再配置する作業だ。
もちろん、意味の圧縮だけで設計のすべてが解決するわけではない。圧縮しすぎて柔軟性を失うこともあるし、関係者同士での「何を同じ意味と見なすか」の合意が必要になる。しかし少なくとも、「このコードは何を意味しているのか」という問いを持ち続けることは、どんな状況でも設計判断の軸になる。
良い設計とは、美しい構造を持つことではない。 意味が過不足なく表現されている状態のことなのだ。