1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI時代に"設計"と向き合ってみる ④|後から変えると死ぬ。"契約"だけは最初に考える

1
Posted at

はじめに

前回(③線引き編)で、設計の線引きには「これさえやればOK」の魔法はないこと、そして基本戦略は 「迷ったら具象から始めて、共通項が見えてから抽象化する」 だということを学びました。

これで完璧だ、と思った矢先。記事の最後で先輩が不穏なことを言い出しました。

:sunglasses::……まあ、基本はそれでええ。でもな、「最初から」きっちり設計せなアカンものも、世の中にはあるで。

:sweat_smile::え? 「迷ったら具象」じゃなかったんですか?

:sunglasses::それが通用せん領域があるんよ。後から変えると、死ぬほど痛いやつ。キーワードは 「契約」 や。

今回はこの 「契約」 の話です。「具象から始める」が通用しない、数少ない例外。

「契約」って何?

:sweat_smile::先輩、契約ってなんですか。プログラミングで契約とか言われても、ピンとこないんですけど。

:sunglasses::そのまんまや。人と人との約束ごと。お前、賃貸契約とか結んだことあるやろ。

賃貸契約を思い出してみます。大家さんと借主の間で「家賃はいくら」「退去時はこうする」と取り決める。あれの特徴って、

  • 破ったら相手が困る
  • 後から勝手に「やっぱ家賃変えるわ」とは言えない
  • 一度結んだら、簡単には変更できない

ソフトウェアの「契約」も、まさにこれと同じ。異なる相手(人・モジュール・サービス)との間の、変えにくい約束ごと を指します。

具体的には、こういうやつが「契約」です。

契約の種類 誰と誰の約束か
API契約 「このリクエストを送れば、こういうレスポンスが返る」というフロントとバックの約束
DBスキーマ 「このテーブルにはこの列があって、この型」というDBとアプリの約束
ライブラリのインターフェース 「この関数にこう渡せば、こう動く」という提供者と利用者の約束
ファイルフォーマット 「このJSONはこういう構造」という出力側と読み取り側の約束

:sunglasses::これらに共通するのはな、自分一人で完結せんってことや。必ず「相手」がおる。

:sweat_smile::相手がいる、か。

なぜ契約は「後から変えにくい」のか

ここが核心です。なぜ契約だけ「最初から慎重に」なのか。答えは 契約相手が複数いるから です。

たとえば、あるAPIを作ったとします。そのAPIの利用者が、

  • 自社のWebサイト
  • iOSアプリ
  • Androidアプリ
  • 連携してる外部パートナー企業

と複数いたとする。ここで「APIの仕様、ちょっと変えたいな」と思ったら、どうなるか。

:sunglasses::その変更、全員に影響するやろ。古いアプリ使ってる人がおったら、API変えた瞬間にアプリが壊れる。外部パートナーには事前に連絡して、調整して……。

:sweat_smile::うわ、想像しただけでしんどい。

:sunglasses::これが「後から変えにくい」の正体や。変更コストが、相手の数だけ膨らむ

これが、前回の「迷ったら具象から始める(後で変えればいい)」が通用しない理由です。

:sweat_smile::あー、契約は「後で変えればいい」が成立しないのか。変えるコストが高すぎて。

:sunglasses::そういうことや。普通のコードは後から直せばええ。でも契約は、一度公開したら相手を巻き込むから、最初にちゃんと考えとく価値がある。「迷ったら具象」の数少ない例外、ってわけや。

逆に言うと、自分一人しか使わないコードなら、契約じゃないので、普通に「具象から始める」でOK。慎重さの度合いは、相手の数で決まります。

「慎重に設計する」って、具体的に何をするの?

:sweat_smile::契約が大事なのはわかりました。でも「慎重に設計しろ」って、抽象的すぎません? 具体的に何をすればいいんですか。

:sunglasses::ええ質問や。「慎重に」は、実はチェック項目に分解できる。

「慎重に」がふわっとしてるから動けないんですよね。先輩いわく、契約系の設計で気をつけることは、だいたい決まっていてリスト化できるそうです。代表的なものを。

API設計のチェック項目

  • 命名は一貫してるかPOST /users(リソース志向)であって、POST /createUser(動詞バラバラ)じゃない
  • レスポンス構造に拡張性があるか:いきなり配列を返すんじゃなく、{ data: [...], meta: {} } のようにオブジェクトで包む。後からページネーション情報とかを足しても壊れない
  • バージョニング戦略を決めたか/v1/users みたいに、後でバージョンを上げられる形にしておく
  • エラー形式は統一されてるか{ error: { code, message } } のように、全エンドポイントで揃える
  • 認証認可の粒度は適切か:エンドポイントごとに「誰がアクセスできるか」を決めたか

DBスキーマのチェック項目

  • 主キーの種類:連番か、UUID/ULIDか
  • 命名規則:スネークケースかキャメルケースか、テーブル名は単数か複数か
  • タイムスタンプcreated_at / updated_at を持つか
  • 削除方針:物理削除か、論理削除(deleted_at 列)か

:sunglasses::こういうのを、設計のときにいちいちチェックする。これが「慎重に」の中身や。頭の中の「なんとなく慎重に」を、具体的なリストに落とすだけで、判断のブレが減る。

そして、この全部に通底する原則が一つあります。

「追加は安全、変更は危険」

新しい列やエンドポイントを「追加」するのは、既存の相手に影響しないから安全。でも既存のものを「変更」するのは、相手を巻き込むから危険。だから、最初から「後で追加で済むように」設計しておくのが、契約設計のコツです。これ、前回の「可逆性」とも繋がってますね。

「とりあえず◯◯」でいい問題

ここで、私が普段のDB設計でなんとなくやってたことを、先輩にぶつけてみます。

:sweat_smile::先輩、DB設計でよく聞くやつ、正直ピンときてないんですよね。タイムスタンプって created_atupdated_at、とりあえず両方入れちゃダメなんですか? あと主キーも連番でいいのに、ULIDとかUUIDとか、なんで使い分けるんです? 正規化も、やればやるほど綺麗になるんですよね?

:sunglasses::……お前、「とりあえず」とか「〜でいい」とか、また思考停止しかけてるな。前回なに学んだ?

:sweat_smile::あっ。

前回(③)の結論は「『これさえやればOK』の魔法はない、思考停止で当てはめると事故る」でした。これはDB設計でもそのまま当てはまります。一個ずつ見ていきましょう。

Q1. タイムスタンプ、とりあえず両方入れちゃダメ?

:sunglasses::これはな、だいたいは入れて問題ないcreated_at / updated_at があって困ることは、ほぼない。

:sweat_smile::じゃあ、とりあえず入れていいじゃないですか。

:sunglasses::「だいたい」って言うたやろ。例外もある。

入れなくていい(むしろ入れない方がいい)ケースもあります。

  • イミュータブルなテーブル(イベントログ、履歴):一度書いたら更新しないから、updated_at は不要。created_at だけでいい
  • 大量書き込みされるテーブル(ログ、メトリクス):書き込みのたびに更新する列があると、塵も積もる

:sunglasses::「入れて困らんことが多い」のは事実や。でもな、「とりあえず入れる」と「意味を考えて入れる」は、結果が同じでも中身が違う。思考停止してる時点で、それは設計しとらん

Q2. 主キー、連番でよくない?

:sweat_smile::主キーって、フレームワークのデフォルトのまま連番で使ってるんですけど、困ったことないんですよね。ちゃんと認可でアクセス制限してれば、URLに載せても他人のデータは見れないわけですし。

:sunglasses::お、ええ理解しとるやん。安全の主役は認可。そこが分かってるなら上等や。ただ、連番を外に出すと「安全かどうか」とは別の問題が起きることがある。

整理するとこう。

向いてるケース
連番(SERIAL) 内部だけで完結/ テストやデバッグの利便性/ インデックスを小さくしたい
ULID / UUID IDから情報を推測されたくない/ 分散システム/ 複数システムのデータ統合

:sweat_smile::「IDから情報を推測される」って、どういうことですか?

:sunglasses::たとえば注文IDが /orders/10432 やったら、「この会社、累計1万件くらいしか注文ないんか」って外から分かってまうやろ。登録ユーザー数も同じ。認可で守れるのはデータの中身で、IDの番号そのものが漏らす情報は守れんのよ。

:sweat_smile::あー、セキュリティの問題というより、ビジネスの数字がバレる問題なんですね。

:sunglasses::そういうことや。せやから「連番で困ってない」なら、それはそれで正しい。外に出すIDで件数や順序を知られたくないか——そこで初めてULIDやUUIDの出番や。両方持つ(内部は連番、公開用に別ID)って手もある。

Q3. 正規化はやればやるほどいい?

:sweat_smile::正規化は……たくさんするほど綺麗ですよね?

:sunglasses::ノー。これもやりすぎは悪や。

正規化すると、データの整合性は上がる(同じ情報が一箇所にまとまるから、DRYと同じ発想)。でも、やりすぎるとこうなる。

  • JOINが増えて、クエリが複雑になる
  • 「商品名を見たいだけ」なのに3テーブルJOIN、みたいなことが起きる
  • パフォーマンスが落ちる

:sunglasses::だいたい 第3正規形(主キー以外の列から決まる値を、別テーブルに追い出した状態)までが標準で、それ以上は過剰なことが多い。むしろ、検索を速くするためにわざと非正規化する(重複を持つ)こともある。

:sweat_smile::え、DRYに反するのに?

:sunglasses::そう。整合性(DRY)とパフォーマンスのトレードオフや。「とにかく正規化すれば善」やなくて、目的に応じて使い分けるもんなんよ。

「正規化最大が正解」じゃなくて、整合性が最優先なら正規化を徹底、読み込み速度が最優先なら非正規化も視野、というバランスの問題なんですね。

まとめ

今回のポイント。

  • 契約=異なる相手(人・モジュール・サービス)との変えにくい約束(API、DBスキーマ、ライブラリのインターフェースなど)
  • 契約だけは「具象から始めて後で変える」が通用しない。相手が複数いて、変更コストが相手の数だけ膨らむから
  • 「慎重に設計」は、チェック項目に分解すれば具体化できる(命名・拡張性・バージョニング・エラー形式…)
  • 通底するのは 「追加は安全、変更は危険」。最初から追加で済む形にしておく
  • DB設計の「とりあえず◯◯」は思考停止のサイン。タイムスタンプも主キーも正規化も、目的を考えて使い分ける

契約は「後から変えにくい」からこそ、数少ない「最初に時間をかける価値がある」領域。逆にそれ以外は、前回どおり「具象から始める」でいい。どこに慎重さを投資するか、というメリハリの話なんだと思います。

で、次回

「とりあえず◯◯、で止まったら設計じゃない」。今回もこの結論に戻ってきました。タイムスタンプも、主キーも、正規化も。

……ん? 待てよ。

:sweat_smile::先輩。「とりあえず◯◯は思考停止」って言うなら……世の中の人、めちゃくちゃ崇めてるやつ、ありますよね。

:sunglasses::お、なんや。

:sweat_smile:「とりあえずクリーンアーキテクチャにしとけば間違いない」 ってやつ。あれって、思考停止じゃないんですか? 人間にもAIにも読みやすくて、ミスも減る、最強の構成なんでしょ?

:sunglasses::……(ニヤ)。お前、また地雷踏みにいったな。

:sweat_smile::また!?

次回は、エンジニアが大好きな 「クリーンアーキテクチャ」 に切り込みます。本当に最強なのか? そして、よく一緒に語られる「DDD」とは、そもそも何が違うのか?

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?