はじめに
前回(①地図編)で、設計という言葉が広すぎて混乱してた私は、「縦の地図(スコープのレベル)」と「横の地図(用語の分類)」という2枚の地図を手に入れました。
でも記事の最後で、新しい絶望が待ってたんですよね。
:先輩、これレベルも種類もこんなにあったら、全部ひとつずつ勉強しなきゃいけないんですか…? さすがに無理なんですけど
:あー、安心せえ。実はな、全部のレベル・全部の領域に共通して効く「大原則」 ってのがあるんよ。
:え、そんな都合のいいものが…? ほんとですか?
ほんとでした。今回はその 「設計の大原則」 の話です。
そんな都合のいいものが、あった
調べてみると、設計の世界には 何十年も語り継がれてる、抽象度を問わず効く原則 がいくつかあるらしい。Lv1のコードでもLv5のシステム設計でも、同じように効くやつ。
:お前が地図で見た DDDだのSOLIDだの、ああいう個別の知識な。あれの理解が早くなるのは、根っこにこの大原則があるからや。先に根っこを押さえる方が、結局は近道なんよ。
なるほど。じゃあ、その大原則とやらを見せてくれと。
まず、7つ全部の名前だけ並べる
先輩が出してきた大原則は、7つ。とりあえず名前だけバサッといきます。
| # | 原則 | ざっくり一言 |
|---|---|---|
| 1 | 関心の分離(SoC) | 違うことをするやつは、違う場所に置け |
| 2 | 低結合・高凝集 | 仲間は近くに、他人は遠くに |
| 3 | 依存性逆転(DIP) | 変わりやすいものに、変わりにくいものを依存させるな |
| 4 | YAGNI | 今いらんものは作るな |
| 5 | DRY | 同じ知識を2箇所に置くな(ただし条件あり) |
| 6 | 可逆性 | 後で戻せる方を選べ |
| 7 | 認知負荷低減 | 読む人の脳みそを使わせるな |
:うっ…7つもあるじゃないですか。結局これ全部覚えなきゃいけないんでしょ? 地図のときと同じで、また暗記ゲーが始まる予感
:待て待て。お前のその「全部バラバラに暗記しよう」とする癖、それが一番アカン。
:え?
:この7つな、ぜんぶ同じことを言ってるんよ。
…は?
種明かし:7つは、ぜんぶ「変更に強くする」の手段
先輩いわく、7つの原則の上には、それを束ねる たった一つのゴール があるそうです。それが、
「変更に強いソフトウェアを作る」
これだけ。
なんで「変更」が主役なのか。先輩の説明はこうでした。
:ソフトウェアって、作って終わりちゃうやろ。リリースしてからも、仕様変更、機能追加、バグ修正…要件は必ず変わる。生き物みたいなもんや。
:たしかに、一回も変更されないコードなんて見たことない。
:そう。だから「最初から完璧で、二度といじらない設計」なんて存在せんし、目指してもムダ。本当に目指すべきは、変化が来たときに柔軟に対応できる構造。これが設計の本当のゴールなんよ。
ここで7つを見返すと、全部この一点に向かってるのがわかります。
- 関心が混ざってないから、一箇所の変更が他に波及しない
- 結合が低いから、ある部品を変えても他が壊れない
- 変わりやすい技術に依存してないから、技術を変えても本質が無傷
- 今いらんものを作ってないから、変更時に邪魔な負債がない
- 重複がないから、変更が一箇所で済む
- 可逆だから、間違えても戻せる
- 認知負荷が低いから、変更時に安全に手を入れられる
:あ、ほんとだ。全部「変えやすさ」の話してる…!
:やろ? だから7つをバラバラに覚える必要はない。「これは"変えやすくする"ためにどう効くんやっけ?」 って親玉から考えれば、全部自然に繋がる。
ちなみにこの7つ、全部同時には満たせないんですよね。どれをどこまで適用するかは毎回トレードオフです。そのトレードオフの中身は、ほぼ全部 「今の楽さ」と「将来の変えやすさ」のバランス取り だったりします。大原則、ぜんぶ地続きなんですね。
原則①:低結合・高凝集
ここからは、7つの原則を一つずつ、例え話で腹落ちさせていきます。まずは 低結合・高凝集 から。
実は私、前から「結合度はなんとなくわかるけど、凝集度ってイメージ湧かないな」と思ってたんですよね。先輩に聞いてみました。
:先輩、凝集度ってなんですか。「高い方がいい」って言われても、ピンとこなくて。
:そうやな。例えるなら、冷蔵庫の整理や。
:冷蔵庫?
先輩の冷蔵庫トークが始まりました。
:凝集度が低い冷蔵庫ってのはな、一段の棚に「牛乳」「ネジ」「電池」「醤油」がごちゃ混ぜに入ってる状態や。
:それはもう冷蔵庫じゃない。
:逆に凝集度が高いのは、「調味料の段」「飲み物の段」「野菜室」ってちゃんとテーマで分かれてる状態。同じ目的のものが、同じ場所にまとまってる。これが高凝集や。
コードで言うと、こういうこと。
// 凝集度が低い(なんでも屋クラス)
class UserManager {
createUser() { ... }
sendEmail() { ... } // メール送信?
generatePdfReport() { ... } // PDF生成??
calculateTax() { ... } // 税金計算???
}
「User」を名乗ってるのに、メールもPDFも税金も詰め込まれてる。さっきの「ネジが入った冷蔵庫」です。これを、それぞれの正しい家に引っ越しさせると、
// 凝集度が高い(テーマで分かれてる)
class UserService { // ユーザー操作だけ
createUser() { ... }
updateUser() { ... }
deleteUser() { ... }
}
class EmailService { // メールだけ
sendWelcomeEmail() { ... }
sendPasswordReset() { ... }
}
class PdfReportService { // PDF生成だけ
generateUserReport() { ... }
}
class TaxCalculator { // 税金計算だけ
calculate() { ... }
}
ポイントは、機能を消したわけじゃないってこと。メールもPDFも税金も、ちゃんと残ってます。ただ「ごちゃ混ぜの段」から引っ張り出して、それぞれテーマの合った棚に移しただけ。こう分けると、各クラスが「同じ目的の仲間」だけで構成される。これが高凝集です。
で、ここからが大事なんですが、凝集度と結合度は表裏一体なんですよね。
:冷蔵庫をテーマごとに整理する(高凝集)と、どうなる? 「醤油どこ?」ってときに調味料の段だけ見ればええ。他の段を引っかき回さんで済むやろ。これが低結合や。
:逆に、ごちゃ混ぜ冷蔵庫だと、醤油探すのに全部の段をひっくり返すことになる…
:そう。それが「ここ直したらあそこも壊れた」の正体。散らかってるから、あちこち触らなあかんくなるんよ。
覚え方は前回も出た 「仲間は近くに、他人は遠くに」。同じ目的の機能は集める(凝集度UP)、違う目的のものは離す(結合度DOWN)。この2つは別々の原則じゃなくて、同じ整理整頓の裏表なんですね。
そしてこれも「変更に強くする」に繋がります。整理されてれば、変更のとき触る場所が局所で済む=壊すリスクが減る、というわけ。
原則②:認知負荷を下げる
次は 認知負荷低減。個人的には、これがAI時代に一番効いてくる原則だと思ってます(その話は後の記事で)。
認知負荷ってのは、人間の頭の中にある小さなホワイトボードの使用量みたいなものです。
そのホワイトボードは意外と小さい。だから設計は、その限られたスペースを圧迫しないように工夫する行為とも言えます。
ざっくり言えば、「読む人が、理解するために頭の中で同時に保持しなきゃいけない情報の量」 のこと。
:人間の脳のメモリって、めちゃくちゃ少ないんよ。一度に7個くらいしか覚えてられん。なのに、それを使い切らせるコードを書くやつがおる。
:耳が痛い。
たとえば、こんな関数。
function p(a, b, c, d, e) {
if (a && (b || c)) {
return d ? e * 1.1 : e * 0.9;
}
// ...さらにネストが続く
}
これを読むには、a〜eが何者かを全部覚えつつ、条件分岐を頭の中で追わなきゃいけない。読み手の脳のメモリがすぐ満タンになります。
設計の質は、読み手が同時に保持しなきゃいけない情報量で測れる。だから、
- 引数は3つ以内に(覚える変数を減らす)
- ネストは浅く(条件の積み重ねを減らす)
- 名前で意図を語らせる(
pじゃなくてcalculatePrice) - 「これを理解するには、まずあれを理解して…」の連鎖を断つ
:これ、要は「読む人に優しくしろ」ってことですか?
:半分そう。でもな、もっと実利的な理由がある。認知負荷が高いコードは、変更するときに事故るんよ。頭がパンクした状態でいじるから、考慮漏れが出る。バグの温床や。
:あー、結局ここも「変更に強くする」に繋がるのか。
そうなんです。読みやすさは優しさの問題に見えて、実は 「安全に変更できるか」 の問題でもある。これも親玉に直結してます。
原則③:関心の分離(SoC)
3つ目は 関心の分離。違うことをするものは、違う場所に置け、という原則です。
低凝集の冷蔵庫と近い話に聞こえますが、こっちは「違うものを混ぜるな」という分離の側に軸足があります。
:家を想像してみ。キッチンとトイレ、同じ部屋にあったら嫌やろ。
:絶対嫌ですね。
:料理する場所と用を足す場所は、目的が違うから分けるのが当たり前。なのにコードになると、平気で「画面の表示」と「業務ルール」と「DB保存」を一つの関数に詰め込むやつがおる。それ、キッチンにトイレ置いてるのと同じやで。
たとえばWebアプリなら、ざっくり「見た目(プレゼンテーション)」「業務ルール(ドメイン)」「データの出し入れ(データアクセス)」は、別の層に分ける。これが関心の分離の基本形です。
:これも結局「変更に強くする」ですか?
:もちろん。画面のデザインだけ変えたいときに、業務ルールのコードを触らんで済む。トイレのリフォームでキッチンを壊さんでええ、ってことや。
ちなみに前回の地図で出てきた「レイヤードアーキテクチャ」も「ヘキサゴナルアーキテクチャ」も、根っこはこの関心の分離の具体化だったりします。原則がアーキテクチャパターンの土台になってる、いい例ですね。
ちなみに、高凝集と似ているように見えますが少し視点が違います。
高凝集は「仲間を集める」話。関心の分離は「そもそも別の目的を混ぜない」話です。
高凝集が部品の中の整理整頓だとすると、関心の分離はシステム全体の部屋割りに近いイメージですね。
原則④:依存性逆転(DIP)
さあ来ました。正直、私が7つの中で 一番わからなかった のがこれです。
実際、この原則だけは他より少し長めに説明します。なぜなら私自身がここで何度もつまずいたからです。
依存性逆転。
言葉からして難しい。「変わりやすいものに、変わりにくいものを依存させるな」。…なんのこっちゃ。
:先輩、これだけマジでわからないんですよ。「依存」とか「逆転」とか、字面で脳が止まる。
:よし、コンセントで説明したろ。
先輩、急に家電の話を始めました。
:お前の家の電子レンジ、発電所と直接ケーブルで繋がってると思うか?
:いや、繋がってないでしょ。コンセントに挿してるだけで。
:そうやろ。もし電子レンジが「東京電力の○○発電所」に直結する設計やったら、どうなる? 電力会社を乗り換えた瞬間、家中の家電が全部死ぬ。
:それは困る。
:困るやろ。でも現実はそうならん。なぜか。間に 「コンセントという規格」 が挟まってるからや。家電は「コンセントの形」さえ守れば動く。発電所が何やろうが、電力会社が変わろうが、知ったこっちゃない。
これがDIPの正体でした。コードで見てみましょう。まず、発電所に直結してる悪い例。
// 悪い例:業務ロジックがMySQLに直結している
class OrderService {
place(order: Order) {
const db = new MySQLClient(); // ← MySQLを直接 new してる(発電所直結)
db.query("INSERT INTO orders (id, total) VALUES (?, ?)", [order.id, order.total]);
}
}
OrderService(業務ロジック)の中で、MySQLClient(具体的な技術)を直接 new して、INSERT文まで書いてます。これだと「注文を保存する」という本質が、MySQLという特定の技術にベッタリ依存してる。電子レンジが発電所に直結してる状態です。MySQLをやめてPostgresにした瞬間、この OrderService も書き換えるハメになります。
これを、間に「規格(インターフェース)」を挟むとこうなる。
// 規格(コンセントの形)を定義する:「saveというメソッドを持て」とだけ決める
interface OrderRepository {
save(order: Order): void;
}
// 業務ロジックは「規格」だけを見る。MySQLという単語が消えた
class OrderService {
constructor(private repo: OrderRepository) {} // ← 規格を受け取る
place(order: Order) { // ← 注文を確定するメソッド
// 「注文を保存する」としか言ってない。保存先がどこかは知らない
this.repo.save(order);
}
}
// 技術側:規格が決めた save の中身を、MySQL用に実装する
class MySQLOrderRepository implements OrderRepository {
save(order: Order) {
const db = new MySQLClient();
db.query("INSERT INTO orders (id, total) VALUES (?, ?)", [order.id, order.total]);
}
}
// 別の技術でも、同じ規格に従えば差し替えられる
class PostgresOrderRepository implements OrderRepository {
save(order: Order) {
const pg = new PostgresClient();
pg.query("INSERT INTO orders (id, total) VALUES ($1, $2)", [order.id, order.total]);
}
}
見てほしいのは、MySQLOrderRepository と PostgresOrderRepository の関係です。OrderRepository(規格)が「save を持て」とだけ決めていて、実際のINSERT文を書くのは技術側。MySQLとPostgresで中身(SQLの書き方)は違うけど、どっちも save という同じ形にハマってる。これが「技術が規格に合わせる」ということです。
そして肝心の OrderService を見ると、MySQLともPostgresとも書いてない。ただ「規格(OrderRepository)の save を呼ぶ」だけ。だから保存先をMySQLからPostgresに変えても、OrderService は一文字も変わりません。
// 呼び出す側(OrderServiceを使う側)で組み合わせる
const service = new OrderService(new MySQLOrderRepository());
service.place(order);
業務ロジックの外側で「どの技術を使うか」を1箇所だけ決める。OrderServiceはそれを知らない。だから差し替えるのはここ1行だけ:
new MySQLOrderRepository() → new PostgresOrderRepository()
// OrderService は一文字も触らなくていい
ポイントは、「変わりやすいもの」と「変わりにくいもの」を見極めること。
- 業務ロジック(「注文を保存する」みたいな本質)→ 変わりにくい
- 使う技術(DB、UIフレームワーク、外部API)→ 変わりやすい
普通に書くと、最初の悪い例みたいに、本質である業務ロジックが変わりやすい技術に依存してしまう。コンセントなしで発電所直結してる状態です。
これを、間にインターフェース(規格)を挟んで 依存の向きをひっくり返す。
Before: OrderService → MySQL
After: OrderService → 規格 ← MySQL
MySQL側が規格に従う形に変わる。だから「依存性"逆転"」。
:あー! 業務ロジックがDBに依存してたのを、間に規格を噛ませて「DBの方が規格に合わせる」形にするから、逆転なのか。
:そういうことや。こうしとけば、
MySQLOrderRepositoryをPostgresOrderRepositoryに差し替えるだけで、OrderServiceは指一本触らんで済む。コンセントの規格さえ同じなら、発電所を乗り換えても電子レンジはそのまま使える、ってことやな。
前回の地図で「クリーンアーキテクチャもヘキサゴナルアーキテクチャも、根底はこの原則」と書きましたが、まさにこれ。DIPは、アーキテクチャパターンの心臓部なんですね。ちなみにこのDIP、よく聞く「SOLID原則」の"D"にあたります。SOLIDの残り4つのうち、単一責任原則(SRP)は関心の分離や低結合・高凝集としてすでに出てきています。開放閉鎖・リスコフの置換・インターフェース分離の3つは、オブジェクト指向のクラス設計を前提とした話が多いため、この記事では省いています。
(ちなみに、こうやって「後で技術を選び直せるよう、間に規格を挟んで決定を遅らせる」考え方は、後の記事でまた出てきます)
原則⑤:YAGNI
5つ目は YAGNI(You Aren't Gonna Need It = どうせ要らないって)。「今いらんものは作るな」。
これは 旅行のパッキング がしっくりきます。
:旅行のとき、「使うかもしれんから」って、あれもこれも詰め込むやつおるやろ。
:僕です。
:で、結局9割使わんと、重いカバン引きずって後悔する。コードも一緒や。「将来この機能要るかも」で先回りして作ったもの、大半は使われん。
しかも厄介なのは、使われないだけじゃ済まないこと。
- 作るのに時間を食う
- コードが無駄に複雑になる
- いざ本当の要件が来たとき、先回りで作ったものが邪魔になる
:足りなかったら現地調達すればいい、みたいな話ですか?
:そうそう。必要になってから作ればええ。そのときのお前は、今より要件をちゃんとわかってるはずやからな。
これも「変更に強くする」に効きます。余計なものを作らない=変更のとき、考慮すべき荷物が少ない、ということ。
原則⑥:DRY
6つ目、DRY(Don't Repeat Yourself = 繰り返すな)。「同じことを2回書くな」。
ここで大事なのは、「同じこと」が指してるのは 見た目じゃなくて"知識" だということ。
:たとえばな、お前の住所。免許証にも、通販サイトにも、会社の書類にも同じ住所を書いてるとするやろ。引っ越したら、全部直さなあかん。一個でも忘れたら、古い住所が残って事故る。
:あー、住所っていう一個の事実が、何箇所にも散らばってるのが問題なのか。
:そういうことや。「同じ事実・同じルール」が複数箇所にコピーされてる状態を避けろ、っちゅうのがDRYや。一箇所直せば済むようにしとけば、変更が楽になるし、直し忘れも起きん。
コードで言えば、消費税率みたいな「一個の事実」が、あちこちに * 1.1 とベタ書きされてる状態。税率が変わった瞬間、全部を探して直す羽目になります。これを定数や関数に一箇所だけ持たせておけば、変更はそこだけ。
:なるほど。じゃあ似たようなコード見つけたら、どんどん一箇所にまとめてけばいいんですね!
:………。
:え、なんですその間。
この「DRYだから、似てたらまとめればいい」という素直すぎる発想——実はここに落とし穴があるんですが、その話は次回に取っておきます。今は 「同じ知識を一箇所に」 がDRYだ、と押さえればOK。
原則⑦:可逆性
最後、7つ目は 可逆性。「後で戻せる方を選べ」。
設計判断って、たいてい 情報が足りない中 でやるものなんですよね。だから「絶対正しい選択」なんて選べない。それなら、せめて 「間違えても戻せる方」 を選んでおこう、という考え方です。
:契約書にいきなりボールペンで書くか? 普通、大事な書類ほど鉛筆で下書きするやろ。
:消せるようにってことですね。
:そう。設計も一緒や。「これ、もし間違ってたら戻すのにどれだけかかる?」を毎回考える。戻すのが軽い方を選んどけば、判断ミスが致命傷にならん。
たとえば、
- いきなり大きな共通化をするより、小さく分けておく方が、後で組み替えやすい
- 「えいやで全部決める」より、決定を後回しにできる構造にしておく方が、情報が増えてから選べる
:これ、さっきのYAGNIとかDIPとも繋がってる気がする。
:鋭いな。全部「将来の自分に選択肢を残す」って意味で地続きや。だから言うたやろ、7つはぜんぶ同じこと言うてるって。
まとめ:親玉から考える
今回のポイント。
- 設計には、抽象度を問わず効く 大原則が7つ ある(関心の分離 / 低結合・高凝集 / 依存性逆転 / YAGNI / DRY / 可逆性 / 認知負荷低減)
- でも7つをバラバラに暗記するのは悪手。全部「変更に強いソフトウェアを作る」という一つのゴールの手段にすぎない
- ソフトウェアは必ず変わる。だから「変化に対応できる構造」が設計の本質ゴール
- 新しい原則に出会ったら、「これは"変えやすくする"ためにどう効く?」 と親玉から考えれば自然に繋がる
7つを覚えるんじゃなくて、「変更に強くする」という1本の軸を持つ。これが大原則との付き合い方なんだと思います。
で、次回
7つの原則を一通り押さえて、いい感じに賢くなった気がした私。でも、さっきDRYのところで「まだ早い」とばかりに先輩に話を止められたのが、地味に引っかかってました。
:先輩、さっきの話なんですけど。DRYって「同じ知識は一箇所に」ってことですよね。じゃあ、似たようなコード見つけたら、どんどんまとめてけばいいじゃないですか。さっきの間、なんかまずかったですか?
:……お前さ、その「似てたらまとめればいい」っての、一番事故るやつやで。
:え。DRY守ってるだけなのに?
:(ニヤリ)ええ勘違いしとるわ。それ、次回みっちりやったろ。
原則は知ってる。でも、それを「どこまで」「いつ」適用するか——次回は、大原則を知っていてもなお難しい 「線引き」の問題 に切り込みます。
