はじめに
前回(③線引き編)で、設計の線引きには「これさえやればOK」の魔法はないこと、そして基本戦略は 「迷ったら具象から始めて、共通項が見えてから抽象化する」 だということを学びました。
これで完璧だ、と思った矢先。記事の最後で先輩が不穏なことを言い出しました。
:……まあ、基本はそれでええ。でもな、「最初から」きっちり設計せなアカンものも、世の中にはあるで。
:え? 「迷ったら具象」じゃなかったんですか?
:それが通用せん領域があるんよ。後から変えると、死ぬほど痛いやつ。キーワードは 「契約」 や。
今回はこの 「契約」 の話です。「具象から始める」が通用しない、数少ない例外。
「契約」って何?
:先輩、契約ってなんですか。プログラミングで契約とか言われても、ピンとこないんですけど。
:そのまんまや。人と人との約束ごと。お前、賃貸契約とか結んだことあるやろ。
賃貸契約を思い出してみます。大家さんと借主の間で「家賃はいくら」「退去時はこうする」と取り決める。あれの特徴って、
- 破ったら相手が困る
- 後から勝手に「やっぱ家賃変えるわ」とは言えない
- 一度結んだら、簡単には変更できない
ソフトウェアの「契約」も、まさにこれと同じ。異なる相手(人・モジュール・サービス)との間の、変えにくい約束ごと を指します。
具体的には、こういうやつが「契約」です。
| 契約の種類 | 誰と誰の約束か |
|---|---|
| API契約 | 「このリクエストを送れば、こういうレスポンスが返る」というフロントとバックの約束 |
| DBスキーマ | 「このテーブルにはこの列があって、この型」というDBとアプリの約束 |
| ライブラリのインターフェース | 「この関数にこう渡せば、こう動く」という提供者と利用者の約束 |
| ファイルフォーマット | 「このJSONはこういう構造」という出力側と読み取り側の約束 |
:これらに共通するのはな、自分一人で完結せんってことや。必ず「相手」がおる。
:相手がいる、か。
なぜ契約は「後から変えにくい」のか
ここが核心です。なぜ契約だけ「最初から慎重に」なのか。答えは 契約相手が複数いるから です。
たとえば、あるAPIを作ったとします。そのAPIの利用者が、
- 自社のWebサイト
- iOSアプリ
- Androidアプリ
- 連携してる外部パートナー企業
と複数いたとする。ここで「APIの仕様、ちょっと変えたいな」と思ったら、どうなるか。
:その変更、全員に影響するやろ。古いアプリ使ってる人がおったら、API変えた瞬間にアプリが壊れる。外部パートナーには事前に連絡して、調整して……。
:うわ、想像しただけでしんどい。
:これが「後から変えにくい」の正体や。変更コストが、相手の数だけ膨らむ。
これが、前回の「迷ったら具象から始める(後で変えればいい)」が通用しない理由です。
:あー、契約は「後で変えればいい」が成立しないのか。変えるコストが高すぎて。
:そういうことや。普通のコードは後から直せばええ。でも契約は、一度公開したら相手を巻き込むから、最初にちゃんと考えとく価値がある。「迷ったら具象」の数少ない例外、ってわけや。
逆に言うと、自分一人しか使わないコードなら、契約じゃないので、普通に「具象から始める」でOK。慎重さの度合いは、相手の数で決まります。
「慎重に設計する」って、具体的に何をするの?
:契約が大事なのはわかりました。でも「慎重に設計しろ」って、抽象的すぎません? 具体的に何をすればいいんですか。
:ええ質問や。「慎重に」は、実はチェック項目に分解できる。
「慎重に」がふわっとしてるから動けないんですよね。先輩いわく、契約系の設計で気をつけることは、だいたい決まっていてリスト化できるそうです。代表的なものを。
API設計のチェック項目
-
命名は一貫してるか:
POST /users(リソース志向)であって、POST /createUser(動詞バラバラ)じゃない -
レスポンス構造に拡張性があるか:いきなり配列を返すんじゃなく、
{ data: [...], meta: {} }のようにオブジェクトで包む。後からページネーション情報とかを足しても壊れない -
バージョニング戦略を決めたか:
/v1/usersみたいに、後でバージョンを上げられる形にしておく -
エラー形式は統一されてるか:
{ error: { code, message } }のように、全エンドポイントで揃える - 認証認可の粒度は適切か:エンドポイントごとに「誰がアクセスできるか」を決めたか
DBスキーマのチェック項目
- 主キーの種類:連番か、UUID/ULIDか
- 命名規則:スネークケースかキャメルケースか、テーブル名は単数か複数か
-
タイムスタンプ:
created_at/updated_atを持つか -
削除方針:物理削除か、論理削除(
deleted_at列)か
:こういうのを、設計のときにいちいちチェックする。これが「慎重に」の中身や。頭の中の「なんとなく慎重に」を、具体的なリストに落とすだけで、判断のブレが減る。
そして、この全部に通底する原則が一つあります。
「追加は安全、変更は危険」
新しい列やエンドポイントを「追加」するのは、既存の相手に影響しないから安全。でも既存のものを「変更」するのは、相手を巻き込むから危険。だから、最初から「後で追加で済むように」設計しておくのが、契約設計のコツです。これ、前回の「可逆性」とも繋がってますね。
「とりあえず◯◯」でいい問題
ここで、私が普段のDB設計でなんとなくやってたことを、先輩にぶつけてみます。
:先輩、DB設計でよく聞くやつ、正直ピンときてないんですよね。タイムスタンプって
created_atとupdated_at、とりあえず両方入れちゃダメなんですか? あと主キーも連番でいいのに、ULIDとかUUIDとか、なんで使い分けるんです? 正規化も、やればやるほど綺麗になるんですよね?
:……お前、「とりあえず」とか「〜でいい」とか、また思考停止しかけてるな。前回なに学んだ?
:あっ。
前回(③)の結論は「『これさえやればOK』の魔法はない、思考停止で当てはめると事故る」でした。これはDB設計でもそのまま当てはまります。一個ずつ見ていきましょう。
Q1. タイムスタンプ、とりあえず両方入れちゃダメ?
:これはな、だいたいは入れて問題ない。
created_at/updated_atがあって困ることは、ほぼない。
:じゃあ、とりあえず入れていいじゃないですか。
:「だいたい」って言うたやろ。例外もある。
入れなくていい(むしろ入れない方がいい)ケースもあります。
-
イミュータブルなテーブル(イベントログ、履歴):一度書いたら更新しないから、
updated_atは不要。created_atだけでいい - 大量書き込みされるテーブル(ログ、メトリクス):書き込みのたびに更新する列があると、塵も積もる
:「入れて困らんことが多い」のは事実や。でもな、「とりあえず入れる」と「意味を考えて入れる」は、結果が同じでも中身が違う。思考停止してる時点で、それは設計しとらん。
Q2. 主キー、連番でよくない?
:主キーって、フレームワークのデフォルトのまま連番で使ってるんですけど、困ったことないんですよね。ちゃんと認可でアクセス制限してれば、URLに載せても他人のデータは見れないわけですし。
:お、ええ理解しとるやん。安全の主役は認可。そこが分かってるなら上等や。ただ、連番を外に出すと「安全かどうか」とは別の問題が起きることがある。
整理するとこう。
| 向いてるケース | |
|---|---|
| 連番(SERIAL) | 内部だけで完結/ テストやデバッグの利便性/ インデックスを小さくしたい |
| ULID / UUID | IDから情報を推測されたくない/ 分散システム/ 複数システムのデータ統合 |
:「IDから情報を推測される」って、どういうことですか?
:たとえば注文IDが
/orders/10432やったら、「この会社、累計1万件くらいしか注文ないんか」って外から分かってまうやろ。登録ユーザー数も同じ。認可で守れるのはデータの中身で、IDの番号そのものが漏らす情報は守れんのよ。
:あー、セキュリティの問題というより、ビジネスの数字がバレる問題なんですね。
:そういうことや。せやから「連番で困ってない」なら、それはそれで正しい。外に出すIDで件数や順序を知られたくないか——そこで初めてULIDやUUIDの出番や。両方持つ(内部は連番、公開用に別ID)って手もある。
Q3. 正規化はやればやるほどいい?
:正規化は……たくさんするほど綺麗ですよね?
:ノー。これもやりすぎは悪や。
正規化すると、データの整合性は上がる(同じ情報が一箇所にまとまるから、DRYと同じ発想)。でも、やりすぎるとこうなる。
- JOINが増えて、クエリが複雑になる
- 「商品名を見たいだけ」なのに3テーブルJOIN、みたいなことが起きる
- パフォーマンスが落ちる
:だいたい 第3正規形(主キー以外の列から決まる値を、別テーブルに追い出した状態)までが標準で、それ以上は過剰なことが多い。むしろ、検索を速くするためにわざと非正規化する(重複を持つ)こともある。
:え、DRYに反するのに?
:そう。整合性(DRY)とパフォーマンスのトレードオフや。「とにかく正規化すれば善」やなくて、目的に応じて使い分けるもんなんよ。
「正規化最大が正解」じゃなくて、整合性が最優先なら正規化を徹底、読み込み速度が最優先なら非正規化も視野、というバランスの問題なんですね。
まとめ
今回のポイント。
- 契約=異なる相手(人・モジュール・サービス)との変えにくい約束(API、DBスキーマ、ライブラリのインターフェースなど)
- 契約だけは「具象から始めて後で変える」が通用しない。相手が複数いて、変更コストが相手の数だけ膨らむから
- 「慎重に設計」は、チェック項目に分解すれば具体化できる(命名・拡張性・バージョニング・エラー形式…)
- 通底するのは 「追加は安全、変更は危険」。最初から追加で済む形にしておく
- DB設計の「とりあえず◯◯」は思考停止のサイン。タイムスタンプも主キーも正規化も、目的を考えて使い分ける
契約は「後から変えにくい」からこそ、数少ない「最初に時間をかける価値がある」領域。逆にそれ以外は、前回どおり「具象から始める」でいい。どこに慎重さを投資するか、というメリハリの話なんだと思います。
で、次回
「とりあえず◯◯、で止まったら設計じゃない」。今回もこの結論に戻ってきました。タイムスタンプも、主キーも、正規化も。
……ん? 待てよ。
:先輩。「とりあえず◯◯は思考停止」って言うなら……世の中の人、めちゃくちゃ崇めてるやつ、ありますよね。
:お、なんや。
:「とりあえずクリーンアーキテクチャにしとけば間違いない」 ってやつ。あれって、思考停止じゃないんですか? 人間にもAIにも読みやすくて、ミスも減る、最強の構成なんでしょ?
:……(ニヤ)。お前、また地雷踏みにいったな。
:また!?
次回は、エンジニアが大好きな 「クリーンアーキテクチャ」 に切り込みます。本当に最強なのか? そして、よく一緒に語られる「DDD」とは、そもそも何が違うのか?