なぜ、DRY原則 vs 単一責務原則(SRP)の優先度判断は死ぬほど難しいのか?
はじめに
ソフトウェア設計の初学者がまず戸惑うのは、原則が一枚岩ではないことです。DRY原則は重複を嫌いますが、SOLID原則の1番目にあたる単一責務原則では変更理由の分離を求めます。表面だけ見れば、同じものをまとめよと言う原則と、違うものを分けよと言う原則が、真正面から衝突しているように見えます。
しかも厄介なのは、この二つの原則のどちらを優先して適用すべきかを、ソースコードのその場の見た目だけでは決められないことです。今は同じように見える処理が、将来も同じようであり続けるとは限りません。逆に、別々の部門に存在する処理であっても、背後では同じ法律や同じ規格に従っているため、本来は一つの知識として扱うべきこともあります。ここに設計判断の難しさがあります。
さらに新人にとっては、この判断が覚えにくいだけでは済みません。正しいかどうかを常に考えさせられ、しかも外したときには責任だけが残ります。だから人は、難しい原則よりも、見た瞬間に適用しやすい単純ルールへ逃げたくなります。DRYが初学者に強く見えるのは、理論的に優れているからではなく、心理的に飛びつきやすいからです。
この問題を本当に理解するには、DRYとSRPを単なる教条として並べるのではなく、それぞれが何を守ろうとしているのかを丁寧に見直さなければなりません。そして同時に、組み込み開発のように資源制約が厳しい世界では、そもそもDRYの意味さえ変質することも理解する必要があります。そこで以下では、この優先度判断がなぜこれほど難しいのかを、変更理由と共通知識と資源制約という三つの軸から整理します。
DRYとSRPはそもそも見ているものが違う
この対立をわかりにくくしている最大の理由は、DRYとSRPが同じものを見ていないことです。DRY原則が見ているのは、同じ知識が複数箇所にばらまかれていないかという問題です。税率、法令、通信規格、計算式の根拠のように、一つであるべき知識が二重三重に存在すると、変更時に修正漏れや解釈不一致が起きます。DRYはそこを嫌います。
一方でSRPが見ているのは、重複ではなく変更理由です。あるモジュールが営業部の都合でも変わり、開発部の都合でも変わり、さらに法規改正でも変わるような状態であれば、そのモジュールには複数の責務が混ざっています。SRPは、同じ理由で変わるものをまとめ、別の理由で変わるものを分けろと言っているのであって、見た目が似ているかどうかは本質ではありません。
この二つは本来、対立原則ではありません。むしろ、うまく設計できているときには両立します。営業部の判断ロジックは営業部に、開発部の判断ロジックは開発部に、法令由来の知識は法令モジュールに分かれているなら、責務は分離され、しかも共通知識の重複も消せます。問題は、現実の設計ではこの境界が最初から明瞭ではないことです。
つまり難しさの第一は、DRYとSRPが喧嘩しているからではありません。別のものを守ろうとする原則を、同じ平面で比較してしまうことにあります。知識の一元化と変更理由の分離は、似た言葉で語られがちですが、本当は別の問いです。この問いの違いが見えない限り、優先度判断はいつまでも曖昧なままです。
何が一緒に変わり、何が別々に変わるかは自明ではない
DRYとSRPの優先順位が死ぬほど難しいのは、結局のところ、何が一緒に変わるのかが最初からわからないからです。設計の教科書はしばしば「変更理由が一つなら単一責務」と簡単に言いますが、現実にはその変更理由が見えていません。将来どの仕様が同時に動き、どの仕様が分岐するのかは、完成時点どころか運用が始まってからでないと見えないことすらあります。
たとえば営業部と開発部は、組織としては別の責務を負っています。しかし日本の国法に関しては、両者とも同じように従わねばなりません。ここで「部門が別だから全部別責務」と考えると、法令ロジックが各所に重複し、法改正のたびに複数モジュールを直すことになります。逆に「似ているから全部共通化」と考えると、今度は部門固有のルールまで一緒になり、営業の事情が開発のコードへ侵入します。
つまり設計者が見なければならないのは、変わるか変わらないかではありません。一緒に変わるのか、独立に変わるのかです。しかもそれは数学の定理のようには確定しません。今は一緒に見えているものが後で分かれることもあれば、別々に作っていたものが、実は同じ規格知識の重複だったと後から気づくこともあります。
この不確実性を認めない限り、設計は必ず形式主義に陥ります。原則の暗記では足りず、未来の壊れ方を想像し続けなければなりません。そしてその想像は、しばしば外れます。だからこそ、この優先度判断は難しいのです。難しいのは原則が悪いからではなく、対象である将来の変更構造そのものが曖昧だからです。
新人がDRYに流れ、判断そのものを避けるのは仕方が無いことである
新人がDRYに流れやすいのは、DRYは現在目の前に見えているものを相手にできるからです。同じようなコードが二つ並んでいれば、誰でも「まとめたほうがきれいだ」と感じます。行数が減り、関数が一つになり、仕事をした感触も得やすいです。重複の除去は、視覚的な達成感を与えます。
これに対してSRPは、見えている形ではなく、見えていない未来を問題にします。この関数は将来どの都合で変わるのか。営業の事情と法令改正と通信規格変更が同じ場所へ集まっていないか。そうした問いに答えるには、業務、組織、保守運用、将来の仕様差分まで想像しなければなりません。新人にとってこれは、見えないものを相手にする作業です。
しかもDRYは初期に成功体験を与えやすいのに対し、SRPは成功しても見返りが見えにくいという非対称があります。DRYで無理に共通化すると、問題は数か月後に出ます。共通関数にフラグが増え、モード分岐が増え、責務が混ざり始めてからようやく破綻が見えます。一方SRPは、正しく分割しても、その時点では何も起きません。将来の事故が減るだけです。このため初学者は、DRYの側へ引き寄せられます。DRYは空間的に見える原則であり、SRPは時間的にしか見えない原則だからです。
加えて、判断を要しない単純ルールへ避難したくなる動機も理解すべきです。重複は悪だからまとめるという行動は、思考停止に見えるかもしれませんが、本人から見れば自己防衛でもあります。SRPとDRYの優先度判断は覚えにくいうえに考えさせられ、外した場合の責任はしばしば判断した本人へ返ってきます。正解保証のない問いに答えさせられ、しかも失敗時だけ責任が残るのだから、教科書的に見える形へ寄せて判断を回避したくなるのは当然です。
ここでベテランが「そんなの考えればわかる」と言ってしまうと、教育は壊れます。問題は、考えれば絶対にわかることではなく、考えてもなお外れる可能性があることです。だから本来、求めるべきなのは正解ではありません。今は将来差分が読めないので分けた、法令由来で全体共通だからまとめた、仕様照合性を優先して数字のままにした。その程度の言語化こそが重要です。設計判断を完全正答ゲームだと見なす限り、新人は必ず形式主義へ逃げます。
初期の成功体験がもたらす偽の収束感
この領域で典型的な失敗が生じやすいのは、DRYがわかりやすく、しかも一時的には成果に見えるからです。同じような処理をまとめると、本人には抽象化に成功した感覚があります。コード量も減り、見た目もすっきりします。このとき初学者は、重複除去の成功を、設計能力そのものの証明だと誤認しやすくなります。ここに偽の収束感があります。
ところが本当の設計は、現在のコードの整頓ではなく、未来の変更に対する耐性です。いま美しい抽象化が、半年後にはフラグ引数だらけの地獄になることは珍しくありません。部署ごとの差異、製品ごとの差異、法令改正、機能追加が重なると、あのとき共通化した関数が、誰のためのものなのかわからなくなります。つまり初期の成功体験は、しばしば時間差で破綻します。
この現象が厄介なのは、本人が怠慢だから起こるのではなく、むしろ真面目だから起こることです。原則を学び、重複は悪だと信じ、積極的にコードを整えようとする。その誠実さが、まだ見えていない変更軸への無知と結びつくと、過剰な共通化へ向かいます。だからこの種の失敗は、単なる未熟というより、原則の片面だけを過信した結果と見るべきです。
知識が増えれば自然に片づく問題でもありません。むしろ知識が増えるほど、簡単な正解らしきものが存在しないことを思い知らされます。難しさの本質は、原則の不足ではなく、原則を適用するための未来認識の不足にあります。
ベテランの勘とは、ただの危険察知である
この問題をさらに難しくしているのは、経験を積めば簡単になるわけでもないことです。初学者はしばしば、ベテランになれば「どちらを優先すべきか」が自動的にわかるようになると思っています。しかし現実には、ベテランが身につけるのは正解判定機ではありません。身につくのは、一緒にするとヤバそうだ、分けると別の地獄が来そうだという危険察知です。
つまり勘とは、十分条件の集積ではありません。この形なら必ず正しい、この条件なら必ず共通化せよといった万能規則は、実務ではほとんど成立しません。あるのは、過去に壊れた構造の記憶と、それに対する警戒感だけです。引数が増え始めた共通関数を見ると嫌な予感がする。部署固有の都合が共通ライブラリへ入り始めると危険を感じる。そうした破綻の臭いがわかるようになるだけです。
これは逆に言えば、ベテランであっても常に考え続けなければならないことを意味します。いったん正しい境界を見つけて終わりではありません。何が一緒に変わり、何が別々に変わるかは、運用によって変わります。製品系列が増えれば変わり、組織が変われば変わり、法規や契約が変わればまた変わります。設計とは、結晶化した知識を当てはめる仕事ではなく、仮説を更新し続ける仕事です。
だからDRYとSRPの優先度判断は、経験で完全に解決する問題ではありません。経験が与えるのは正解ではなく、事故の予兆への感度です。そしてその感度が高い人ほど、むしろ軽々しく共通化も分割もできなくなります。簡単に見える判断ほど危ないと知っているからです。この点で、初学者が求める「覚えれば済む設計基準」は、そもそも存在しません。
組み込みではDRYの意味そのものが変わる
この話をさらに複雑にするのが、組み込み開発ではDRYが別の意味を持ちうることです。一般的な業務アプリケーションでのDRYは、主として知識の重複排除です。同じ税率や同じ法判定や同じ規格処理を各所に書かないことで、保守性を上げます。ところが組み込み、とくに古いゲーム機や極端なメモリ制約の世界では、DRYは資源の重複排除に変わります。
そこでは、同じ知識を一か所にするというより、同じバイトを二度持たないことが重要になります。コード、テーブル、画像、フォント、メッセージ、ワーク領域をいかに使い回すかが、生存条件になります。責務分離の純度より、ROMに収まるか、RAMが足りるか、クロック内で回るかのほうが先です。この状況では、SRPは守れれば守るが、まずは載ることが最優先です。
昔のファミコンゲームのような世界では、この圧力は極端でした。フォントやメッセージでさえ素朴に持てず、表現を削り、資産を共有し、無理やり再利用することで成立していました。そこではDRYは、設計美の原則ではありません。容量と性能に対する生々しい闘争です。この意味でのDRYは、SRPと美しく両立するとは限りません。むしろ多少割り込んででも、容量優先で選ぶことがあります。
したがってDRYとSRPの優先度判断は、抽象設計の問題であると同時に、開発対象の物理条件の問題でもあります。組み込みの現場では、DRYを単に保守性の文脈だけで教えると半分嘘になります。知識のDRYと資源のDRYがあり、後者は前者より荒々しく、しばしばSRPを押しのけます。この二重性がある限り、優先度判断は一層難しくならざるをえません。
マジックナンバー論争が示す形式主義の罠
DRYとSRPの難しさは、名前付き定数の扱いにもよく表れます。初学者がよくやる誤りの一つが、マジックナンバー禁止を数字禁止だと勘違いすることです。その結果、仕様書に数字で明記されている値まで、無理やり名前付き定数にしたがります。これは一見すると整って見えますが、実際には仕様との照合性を落とすことがあります。
本来問題なのは、数字であることではなく、出所や意味がわからないことです。ネイピア数や円周率のように覚えにくいが意味のある定数、税率のように将来変わる可能性が高い定数は、名前付き定数にする価値があります。数字を隠すためではなく、数字だけでは読者に伝わらない意味を前に出すためです。ここでは名前が説明として機能します。
しかしアルゴリズム仕様書と一対一に対応する数字や、誰でも知っている12か月のような数字は、必ずしも名前付き定数にすべきではありません。関数自体が特定アルゴリズムの実装であるなら、その内部の数字は、むしろ数字のままのほうが仕様と突き合わせやすいことがあります。無理に名前へ置き換えると、読者は一度名前を解釈してから仕様書の数字へ戻す必要が生じ、かえってわかりにくくなります。
ここでも同じ問題が起きています。見た目に現れている規則だけで判断すると、数字を見た瞬間に定数化したくなります。しかし設計上の本質は、何が読者にとって最も意味が通るかです。つまりDRYや命名規則は、考えなくて済む免罪符ではありません。正しく使えば説明になりますが、誤用すれば形式主義になります。この微妙さこそが、設計判断を単なる暗記で済ませられない理由です。
良い設計とは危険の比較である
DRYとSRPの優先度判断は、唯一の正解を当てる作業ではありません。どちらに倒したときにどの種類の事故が起こるかを比較し、その時点でよりましな仮説を選ぶ作業です。共通化すれば修正漏れは減るかもしれませんが、責務の混線が起きるかもしれません。分割すれば責務は明瞭になるかもしれませんが、法改正時の多点修正が増えるかもしれません。設計者は常に、壊れ方のトレードオフを見ています。
この見方に立つと、DRYとSRPは対立原則というより、異なる破局を警告するセンサーです。DRYは知識の重複による不整合を警告し、SRPは変更理由の混線による肥大化を警告します。どちらが優先かは、その場の見た目ではなく、何が共通知識であり、何が局所知識であり、どの資源制約が優先されるかによって決まります。だから一般論だけで答えることはできません。
そして必要なのは、正解集を暗記することではなく、判断根拠を言語化し、変更履歴を観察し、外れたら境界を引き直すことです。設計とは原則の忠実な適用ではなく、原則同士が警告する危険を見比べる能力です。その能力は失敗例への感受性として身につきます。だからこの優先度判断は、いつまでも難しいままです。
まとめ
DRY原則と単一責務原則の優先度判断が死ぬほど難しいのは、二つの原則が別のものを守っているからです。DRYは共通知識の重複を嫌い、SRPは変更理由の混線を嫌います。したがって、どちらを優先するかは、見た目の重複ではなく、何が一緒に変わり、何が独立に変わるのかという未来認識に依存します。
しかしその未来認識は自明ではありません。営業部と開発部は別責務でも、国法には同じように従います。ローカルルールは将来分岐しやすく、法令や規格は全体共通の知識として扱うべきことが多いです。ところがその境界は最初から完全には見えません。だから設計は、結晶化した知識の適用ではなく、仮説を更新し続ける営みになります。ベテランの勘もまた、十分条件ではなく危険察知としてしか成立しません。
さらに組み込み開発では、DRYに資源節約という別の意味が加わります。そこでは知識の重複排除だけでなく、コードやデータの重複排除が最優先となり、SRPへ食い込むことがあります。マジックナンバーや名前付き定数の扱いにも同じ難しさがあり、形式的な規則適用ではなく、仕様照合性と意味伝達のどちらを優先するかが問われます。ここでも単純な正解はありません。
したがって、この問題の核心は、どちらの原則が上位かという序列ではありません。難しさの本体は、原則が未来の壊れ方を別々に警告していることにあります。そのため設計者は、原則を暗記するだけでは足りず、変更構造、責任構造、資源制約、読者の理解負荷を含めて、常にどれが最適っぽいかを考え続けなければなりません。優先度判断が難しいのは、設計がそもそも正解選択ではなく、壊れ方の比較だからです。
Discussion