「シンプル」と「小規模」は別物!エンジニアが知るべき本質的な設計思想
Simple Is Not Small
Simple Is Not Small
「シンプルであること」と「コードの行数が少ないこと(小規模)」を混同していませんか?真のシンプルさは、複雑さを削ぎ落とし、システムの意図を明確にすることから生まれます。規模の大小に関わらず、本当にメンテナンスしやすいコードを書くための視点について考えましょう。
素晴らしい記事だし、例もすごく分かりやすかった。ただ、クロージャの部分を理解するのにはかなり目を凝らす必要があったけどね。
今、まさに「すごく」モジュール化されたソフトウェアを作っている最中なんだけど、これまでのキャリアの中で一番設計が難しいプロジェクトになってる。こんなに頑健で明確に定義されたものを作るために、これだけの時間と労力をかけさせてもらえるなんて、これまで働いてきたどんな職場でもあり得なかったよ。このプロジェクトの多くの側面で、シンプルじゃない抽象化を何度も何度もやり直しては捨てる作業を3〜5回は繰り返した。
まさにこれが、小さな分離されたスクリプトを作るには「vibe coding」がうまくいくのに、それ以外のことになると大惨事になる理由なんだよね。大規模かつシンプルに何かを作るっていうのは、本当にめちゃくちゃ難しい。
これも大事な点だけど、「シンプル」は「劣っている」とか「バカっぽい」っていう意味じゃない。もっと巨大で広範囲であっても「シンプル」であり得るんだ。
Rich Hickeyが一番うまく説明してくれているから、まだ見たことがなければ「Simple Made Easy」のトークを見てみて。個人的に半年に1回は見返している数少ないトークの一つだよ: https://www.youtube.com/watch?v=SxdOUGdseq4
Hickeyのトークや、結局のところClojureが、これまでの自分のプログラミングに対する考え方をこれほど変えてくれたことはないよ。
彼がまた素晴らしいトークをしてくれることを願ってるけど、そろそろ引退しそうな雰囲気なのかな?
有名なエッセイ「The Rise of Worse is Better」[1]に強く共感するよ。そこでは「MIT/Stanford流の設計」(より良い)と「New Jerseyアプローチ」(より悪い)が対比されている。
MIT/Stanford:
シンプルさ -- 設計は実装とインターフェースの両面でシンプルでなければならない。実装よりもインターフェースがシンプルであることの方が重要だ。
New Jersey:
シンプルさ -- 設計は実装とインターフェースの両面でシンプルでなければならない。インターフェースよりも実装がシンプルであることの方が重要だ。シンプルさは設計において最も重要な考慮事項である。
TFA(この記事)は「シンプルさ」を「MIT/Stanford流のシンプルさ(ユーザーにとってのシンプルさ)」に、「小ささ」を「New Jersey流のシンプルさ(開発者にとってのシンプルさ)」にマッピングしているね。
この2つの流派の対立の根本は、ユーザーと開発者の境界が曖昧なことにあるんじゃないかと思う。開発者もまたユーザーだからね。実装のシンプルさは、開発者が直接実装に取り組んでいるときには役立つし、インターフェースのシンプルさは、他の開発者の仕事を利用するときに役立つ。
「正しさのステートメントの長さ + その証明の長さ」をコンポーネントの複雑さの尺度として使うのはかなり有効だと思う(長いほど複雑ってこと)。
細かい修正を加えてコピペされた関数は、証明を再利用できない(DRYじゃない)。かといって、巨大な関数にif/elseが山盛りだと、証明の中で分岐が爆発してしまうこともある。優れた抽象化は証明が依存する仮定を大量に排除してくれるから、探索空間が制限されて、しばしばエレガンスを強いることになるんだ(これは数学にも当てはまる。例えば、整数ではなく抽象代数学の群で推論するときのようにね)。
抽象化が間違っていると、抽象化の利用者側に場合分けを強いることになってしまう。
Unixにはfrequencies(度数分布)に相当するネイティブコマンドがないから、sort | uniq -c が一番近い。パフォーマンスが低い(処理を続ける前にすべての入力をメモリに読み込む必要がある)だけでなく、集計処理を並び替えに縛り付けている。
Unixコマンドラインの核心的な特徴の一つは、ユーザーが拡張可能だということだよ。frequenciesに相当する「ネイティブ」なコマンドがないなら、自分で書けばいい。そうすれば、PATHにある他のバイナリと同じように一級品の扱いを受けられるんだ。これはシンプルさを重視するUnix哲学に完全に合致しているよ。
シンプルであることは、簡単であることを意味しない。ソフトウェア開発において、これは最もよくある誤解の一つだ。
著者はコードの視点からシンプルなプログラムを説明するために「結合(coupling)」という言葉を使っているね。「プログラムの優れたメンタルモデルを持つ必要がある」という点には同意する。でも、それよりも重要なのは優れたドメインモデルを持つことだと思う。優れた設計は正確に意図を伝えるものだし、実は「評価(Evaluation)の乖離」と「実行(Execution)の乖離」が大きいことこそが、プログラムをユーザーにとって「複雑」に感じさせる原因なんだ。
これらの概念は、ユーザーの「やりたいことは分かっているのに、どうやってプログラムにそれを実行させればいいのか?」(実行)と「プログラムが何かしたけど、結局どういう状態になったのか?」(評価)という疑問を単純化してくれる。
著者はこれらの概念、特にGoogle Driveの例で言及していたと思うよ。巨大なプログラムなのに、UXがどうやって「小さく」なっているのかっていう話だね。ドメインモデルを理解することは、ユーザーインターフェースを設計するためのより強力な基盤になるし、評価と実行の乖離を理解すれば、ユーザーを混乱させたり圧倒させたりすることなく、見た目は非常に複雑で大規模なUXを構築できるようになるんだ。
複雑さ(シンプルさの対義語)はプログラムのサイズとは関係ないよ。ただ、大規模なプログラムの方が小さなものより複雑になる可能性は高い。なぜなら、複雑さは足し算じゃなくて掛け算で増えていくからだ。
たった1行の正規表現の方が、100行のJavaプログラムよりもはるかに複雑なことだってあり得るからね。
その理由は、Rustでは構造体が型チェックを固定されたデータ表現と結合させているからだ。どちらか一方だけを選ぶことはできない。
Clojureはデータ表現と型チェックを分離している。
面白い見方だね。逆の視点から見ると、Clojureの方がランタイムの型情報をデータ構造に結合させているようにも見える。実行時の型情報が付いていないデータ構造を定義することが許されないわけだから。固定された静的な構造というのは、単に動的な型情報を追加していないことの結果に過ぎないよ。
ちなみにRustでも、トレイトオブジェクトを使えば固定された構造なしで型チェックができるよ。
本筋とは外れるし、細かいことを言うつもりはないんだけど、UnixのパイプラインとClojureの式は完全に同じ処理をしているわけじゃないよ。
Clojureの式はまずファイル全体をメモリに読み込んでから、そのメモリ上の表現に対して操作を行う。
一方でパイプラインは入力をチャンク単位で処理する。2つのsortは入力全体をメモリに読み込むかもしれないけど、GNU sortのような実装であれば、入力が巨大な場合は部分ごとに一時ファイルを作成してマージソートを行うというやり方をするんだ。
Clojureでも同じようにできるけど、パイプラインほど「シンプル」にはならないかもしれないね。