慶応三年、江戸にて
娘「おとっつぁん!」
娘「あたしにも、コーディングを教えておくれよ!」
熊さん「おうおう、娘ちゃんがとうとうコードを書きてぇってかい」
熊さん「そいつぁいいな」
熊さん「そんで、いったい何を作りてぇんだい」
娘「クイズのWebサイトを作りたいんだよぉ」
娘「4択クイズで、正解を選んだら丸が出るやつ!」
娘「しかも、プログラミングのクイズにしようと思ってるんだ」
熊さん「そいつぁいい!」
熊さん「江戸の寺子屋もびっくりの学び場になりそうじゃねぇか」
娘「でしょ!」
娘「それでね、まずは自分で作り始めてみたんだけど」
娘「こんな感じで、コードの中では」
娘「問題を QuizQuestion、選択肢を Choice って呼んでるんだ」
class QuizQuestion {
id: QuizQuestionId
choices: Choice[]
}
class Choice {
id: ChoiceId
/*
* 選択肢の文言
*/
text: string
/*
* 正解かどうか
*/
isCorrect: boolean
}
熊さん「おぉ、バックエンドのクラスを作ってるのか」
熊さん「いいじゃねえか」
娘「でもね」
娘「管理画面から、選択肢の文言を修正する処理について悩んでるんだ」
熊さん「文言を修正するだけなら簡単じゃねぇか」
熊さん「選択肢を探して、中身を書き換えりゃあいい」
選択肢の文言を更新する
熊さん「たとえば、こんな具合だな」
// 選択肢の文言を更新!
await choiceRepository.updateText(
choiceId,
input.text
)
熊さん「選択肢のIDを指定して、内容を更新してやりゃあいい」
熊さん「これくれぇは、蕎麦がのびる前に終わるってもんよ」
娘「でも、おとっつぁん」
娘「気をつけたいことがあるんだ」
熊さん「なんだい」
娘「同じ選択肢が複数あったら、変じゃない?」
娘「たとえば、こういう問題があったとして─」
問題: 一番いいエディタは?
正解: Vim
不正解: Emacs
不正解: VSCode
不正解: メモ帳
熊さん「なんだなんだ、職人たちが喧嘩を始めそうなクイズだな」
娘「てへへ」
娘「とにかく、ここで不正解もVimに変えられちゃったら」
問題: 一番いいエディタは?
正解: Vim
不正解: Vim
不正解: Vim
不正解: Vim
娘「同じ文言の選択肢がいくつもできちゃうんだよ」
熊さん「なるほどな」
熊さん「これじゃあ、正解のVimを選んだのに不正解になっちまうこともある」
熊さん「下手すりゃ江戸中が喧嘩だらけになっちまう」
熊さん「こりゃあ、気をつけてバックエンドの処理を書かなきゃいけねぇ」
気をつけて処理を書く
熊さん「まず問題を取ってきて」
熊さん「変更後の文言が他の選択肢と被ってないか確かめる─」
const question =
await questionRepository.findByChoiceId(choiceId)
const otherChoices = question.choices
.filter(choice => choice.id !== choiceId)
const hasDuplicate = otherChoices
.some(choice => choice.text === input.text)
熊さん「被ってたら止める」
if (hasDuplicate) {
throw new Error("他の選択肢と同じ文言にはできません")
}
熊さん「そして問題なけりゃあ、更新だ」
await choiceRepository.updateText(
choiceId,
input.text
)
娘「わあ、これなら防げそう!」
熊さん「大事なのは、手順を間違えねぇことよ」
熊さん「選択肢の被りがないことを確認してから、選択肢を更新する」
熊さん「これをしっかり守ればいい」
熊さん「順番さえ守りゃあ、神田明神に誓って大丈夫って寸法よ」
娘「ありがとう、おとっつぁん!」
熊さん「へへ、いいってことよ」
しかし、すでに気をつけ漏れていた
娘「おとっつぁん、そういえば」
娘「そもそも問題を作る時点でも」
娘「選択肢の被りは禁止しないとだった」
熊さん「おお、その通りだ」
熊さん「じゃあ、新規作成のコードにも」
熊さん「重複禁止の条件を書きゃあいい」
const texts = input.choices.map(choice => choice.text)
const hasDuplicate =
texts.length !== new Set(texts).size
if (hasDuplicate) {
throw new Error("同じ文言の選択肢は作れません")
}
熊さん「被りがなけりゃあ、作成だ」
await questionRepository.create(input)
娘「ふぅ〜、危なく忘れるところだったよぉ!」
娘「これで新規作成の時も安心だね」
AI「ちょっと待ってくだせぇ」
AI「安心と言い切るには、ちいと早うござんす」
熊さん「なんでぇ、AIっつぁん。見てたのか」
熊さん「いったい何がまずいんだい?」
熊さん「同じ手順を、気をつけて同じように書いてるじゃねぇか」
熊さん「こちとら同じ団子を二本並べただけだ。味が違うはずがねぇだろう」
AI「その『気をつけて手順を守る』ってのが危ねぇでやんす」
手順を守らせる設計は事故る
AI「旦那」
AI「たとえば、こんな自動販売機があったらどう思いやすか」
熊さん「自動販売機?」
AI「へい」
AI「自動販売機に、こんな張り紙が貼ってあるんでさぁ」
自動販売機の使い方
必ず以下の手順を守ること
- 購入したい商品を決める
- 在庫数が1以上であることを確認する
- 商品の価格をボタンで入力する
- 代金を投入する
- 商品を取り出す
- 在庫数をボタンで1つ減らす
- お釣りの金額を入力する
- お釣り排出ボタンを押す
熊さん「そいつはもう、ほぼ手動じゃねえか」
熊さん「自動販売機ってぇより、無人販売所だ」
AI「へい、その通りでございやす」
熊さん「在庫を減らしたりお釣りを出したりは、間違いがあっちゃいけねえ」
熊さん「利用者にやらせずに、自販機の内部でやるべきだろう」
AI「そういうことでござんす」
AI「ですから、クイズの選択肢の作成・更新についても」
AI「必須の流れは、利用者に任せねぇ方がいいんでやす」
熊さん「利用者?」
熊さん「ここでいう利用者ってぇのは誰のことだい?」
熊さん「クイズで遊んでくれるユーザーさんのことじゃあねえだろう?」
クイズや選択肢の利用者とは
AI「QuizQuestion や Choice の利用者は」
AI「それをコード上で使用するプログラマーやAIでやんす」
AI「後続の開発者、AIエージェント、未来の自分、みんな利用者でやんす」
娘「みんな、毎回この手順を守らなきゃいけないんだね」
- 他の文言を確認する
- 文言被りがあれば止める
- 問題なければ選択肢を更新する
熊さん「確かに、なんだかさっきの自販機みてぇだな」
AI「へい」
AI「ですから、必ず気をつけることを呼び出し側に強いる設計は、やめやしょう」
熊さん「じゃあ、どういう設計にすりゃあいいんだい?」
AI「QuizQuestion と Choice は、矛盾がないように一緒に扱いやしょう」
AI「具体的には、Choice たちの親分である QuizQuestion を窓口にしやしょう」
QuizQuestion に守らせる
AI「選択肢を直接いじるんじゃなく」
AI「QuizQuestion という問題そのものに、選択肢の変更を任せるんでさぁ」
question.changeChoiceText(
choiceId,
new ChoiceText(input.text)
)
await questionRepository.save(question)
娘「選択肢を直に更新するんじゃなくて、問題さんに頼むんだね」
娘「でも、問題さんはどうルールを守ってるの?」
AI「こうでやんす」
class QuizQuestion {
changeChoiceText(choiceId: ChoiceId, text: ChoiceText) {
const choice = this.findChoice(choiceId)
// 他の選択肢と文言が被っていたら例外を投げる
this.ensureNoDuplicateText(choice, text)
choice.changeText(text)
}
/* ...省略... */
}
熊さん「なるほどな」
熊さん「親分であるQuizQuestionクラスのメソッドが、文言被りを許さねえわけだ」
集約
AI「これを、DDDでは集約と呼びやす」
熊さん「親分の QuizQuestion が集約かい?」
AI「いえ、QuizQuestion と Choice たちのまとまりが集約でやんす」
QuizQuestion(集約ルート)
├── Choice
├── Choice
├── Choice
└── Choice
熊さん「なるほど」
熊さん「こういう親子っぽいクラスは、みんな集約なのかい?」
AI「いえ、外から子を直接いじれるなら、それはただの親子関係でございやす」
AI「子への直接更新・保存を禁じて、親を必ず通すようにして、初めて集約でやんす」
熊さん「へぇ、それが集約と呼ばれる条件なのかい」
熊さん「この集約ってのは、呼び出し側に細かい手順を覚えさせねぇために生まれたのかい?」
AI「それも大きな役目でござんすが」
AI「集約は、商売上ありえない状態を作らせないための境界でござんす」
熊さん「商売上ありえない状態?」
AI「今回なら、こうでござんす」
正解: Vim
不正解: Vim
不正解: Vim
不正解: Vim
AI「これは、クイズとして存在してほしくない状態でござんす」
AI「だから、そもそも作れないようにしやす」
AI「そのために、外側からは親分のメソッドだけを使わせるんでやんす」
熊さん「問題クラスが、門番になるわけだな」
娘「張り紙でみんなに注意喚起するんじゃなくて、門番しか触れなくするんだね」
AI「へい」
AI「選択肢を直接いじりたい輩は」
AI「門前払いでござんす」
本当に外から個別に触れない?
熊さん「待てよ?」
熊さん「いくら QuizQuestion を作っても、誰かが直接 ChoiceRepository を使ったら台無しじゃねぇか」
AI「その通りでござんす」
AI「ですから、ChoiceRepository は存在させないか、読み取りのメソッドだけにしやしょう」
AI「QuizQuestionのメソッドを通してのみ Choice を保存できるようにして」
AI「Choice だけを個別に保存する口は、最初から作らねぇのでやんす」
AI時代だからこそ、集約を用意する
熊さん「しかし、これからの時代は賢いAIたちがコードを読んで、書くんだろう?」
熊さん「そんな仕組みがなくても安全なんじゃあねえか?」
AI「いえ、あっしらも複数の枝で開発したりすると、見落としたりしやす」
熊さん「なんでぇ、その『枝』ってのは」
AI「Gitのブランチでやんす」
熊さん「なんだか分かりにくくて仕方ねぇな」
AI「しかし、それを言っちまいやすとこの記事自体が成り立ちやせん」
熊さん「た、たしかにな」
熊さん「要するに、複数ブランチで複数AIが爆速開発するって話かい?」
AI「そうでやんす」
AI「そんなときに、あっしが大事な手続きのコードをコピペしてる間に」
AI「別のブランチでAI二号が元の手続き内容を修正したりすると」
AI「あっしのブランチだけ手続き内容が古いままで」
AI「ズレが生じちまいやす」
熊さん「なるほど、集約にしておけば」
熊さん「AIっつぁんはメソッドの呼び出しをコピーするだけだから」
熊さん「中身の処理が変わっても追従できるわけだ」
AI「そういうことでやんす」
AI「そうじゃねぇと、あっしらは爆速で古い手続きをそこら中に撒き散らすかもしれやせん」
熊さん「そいつぁおっかねぇ」
熊さん「AI時代でもクラス設計ってのは大事なんだな」
まとめ
- 集約とは
- 業務ルールに反した状態を作れないように、関連するオブジェクトをひとまとまりにしたもの
- 呼び出す側に手順やルールを覚えさせる設計は事故る
- 必須のルールは集約ルートに閉じ込めるべし
- 保存の口は集約ルートにだけ用意し、内部エンティティには作らない
- AI時代でも、矛盾を防ぐための設計は有効
熊さん「そうだ」
熊さん「外から勝手なことをさせねぇために、うちの家族も集約になろうや」
娘「ど、どういうことだい?おとっつぁん」
熊さん「娘ちゃんへの直接の恋文は禁止して」
熊さん「俺が窓口になるってのはどうだ?」
AI「それは集約というより、子への執着でやんすな」
娘「しかも、おとっつぁんとして醜悪だよぉ」
AI「娘ちゃんの人生は、娘ちゃん自身が主役でやんすよ?」
熊さん「もう分かった、取り消すから」
熊さん「ここいらで終幕としようや」
〜おあとがよろしいようで〜
