ワイ「はぁ〜、またワイの書いた記事がSNSでディスられてるで」
ワイ「まったく、エンジニアってのは性格の捻くれた奴が多いよな」
娘(10歳)「パパ!消えないで!」
ワイ「ふぅ〜、危なく消滅するところだったやで・・・」
ワイ「それはともかく」
ワイ「今日は、自分のためのブログサイトを作っていくで!」
娘「へぇ〜、いいね」
ワイ「今回は、バックエンドも自分で作ってみたいんや」
ワイ「せやからREST APIを作るやで」
ワイ「まず、エンドポイントは・・・/do-anything
でええわ」
娘「え」
娘「/do-anything
って、そのエンドポイント1つで何でもやるってこと?」
ワイ「せやで」
娘「何そのクソ設計」
娘「流石にヤバくない?」
ワイ「別に、設計とかどうでもええやろ」
ワイ「ええか?この先の時代はな?」
ワイ「AI君がコード読んで、コード書いて、保守もしてくれねん」
ワイ「せやから、人間にとって保守しやすい設計だとか、理解しやすいコードなんて」
ワイ「もう要らん時代やねん」
娘「うーん」
娘「本当かなぁ」
ワイ「ほんまやで」
ワイ「テストコードをAI君にいっぱい書かせて、自動テストが通ってりゃそれでええねん」
娘「とか言って、本当は」
娘「単にパパがフロントエンドの経験しかなくて」
娘「バックエンドの設計が分からないだけじゃないの?」
ワイ「ギクゥ!!!」
ワイ「いやいや、まさかまさか」
ワイ「とにかく大丈夫やから、やって見せるで!」
ワイ「まずはclass DoAnything
を作って」
ワイ「この中に全部の処理を書くやで」
class DoAnything {
public handleRequest(request: Request): Response {
// ユーザー登録処理
if (request.action === "register_user") {
/* 省略 */
}
// 記事を投稿する処理
else if (request.action === "create_post") {
/* 省略 */
}
// コメントを投稿する処理
else if (request.action === "add_comment") {
/* 省略 */
}
/* 以下省略 */
}
}
ワイ「↑こんな感じや」
ワイ「リクエストに含まれるaction
の値を見て、処理を分岐させるんや」
ワイ「完璧すぎるで」
娘「1つのクラスに全ての機能が入ってて、すごいことになってるね・・・」
ワイ「大丈夫や!AI君が保守してくれるから!」
ワイ「よっしゃ、とりあえず動くものができたで」
ワイ「やっぱり問題なしや!」
娘「うーん」
ワイ「ここからは、AIのCursor君とDevin君を使って機能を追加していくで」
娘「2つのAIが、2つのgitブランチで並行開発する感じ?」
ワイ「せや」
ワイ「複数AIで爆速開発や!」
ワイ「ヘイ、Cursor君!ユーザーにプロフィール画像を設定する機能を追加しといて!」
ワイ「ヘイ、Devin君!記事にタグ付けできる機能を追加しといて!」
ワイ「ワイはもう、高みの見物や」
ワイ「楽でええわ〜」
ワイ「・・・あかん」
ワイ「コンフリクトが発生して、全然マージできん・・・」
娘「そりゃそうだよ」
娘「だって、2人のAIが、2人とも同じファイルをガンガン修正しようとしてるんだもん」
娘「例えば──」
Cursor君が追加しようとしているコード
type Request =
| { action: "register_user"; data: UserData }
| { action: "create_post"; data: PostData }
| { action: "add_comment"; data: CommentData }
+ | { action: "set_profile_image"; data: ImageData };
Devin君が追加しようとしているコード
type Request =
| { action: "register_user"; data: UserData }
| { action: "create_post"; data: PostData }
| { action: "add_comment"; data: CommentData }
+ | { action: "tag_post"; data: TagData };
娘「──こんな感じで、同じ場所に違う行を追加しようとしてるんでしょ?」
娘「これじゃあ、確実にコンフリクトするよ」
ワイ「ぐぬぬ・・・」
ワイ「いや、でもAI君は賢いんやから」
ワイ「これくらい、うまいことマージできるやろ」
娘「うーん、確かにこれくらいのコンフリクトなら、AI君が解決してくれるかもしれないけどさぁ」
娘「それにしても、何を修正するにも必ずDoAnything
クラスの修正が必要で」
娘「必ずコンフリクトが起きるのは、流石にウザ過ぎない・・・?」
娘「例えば、この先もっと機能を追加したくなったときに──」
ワイ「もっと複数のAIで、複数のブランチで爆速開発したいやで!」
娘「──なんて時に」
娘「こんなにコンフリクトが起こりやすかったら、地獄すぎない?」
娘「やっぱり、できるだけ小さな機能のクラスをたくさん作るべきだよ」
ワイ「そうか・・・」
ワイ「AIの時代も、やっぱりクソデカ神クラスはアカンのか・・・」
娘「当たり前だよ」
娘「単一責任原則って、パパ知ってる?」
ワイ「えっと」
ワイ「さっきのDoAnything
クラスみたいに」
ワイ「単一のクラスが、全ての責任を持つべし!」
ワイ「そういう原則やったっけ」
娘「いや、真逆だね・・・」
娘「1つのクラスは、1つの責任だけを持つべき、っていう設計の基本原則だよ」
娘「今のDoAnything
クラスは、色んな責任を持ちすぎてるの」
- ユーザー関連の責任
- 記事関連の責任
- コメント関連の責任
娘「↑こんなに多くの責任がごちゃ混ぜになってるから問題なんだよ」
娘「何をするにもgitのコンフリクトが起こって、訳わかんなくなっちゃう」
娘「こんなんじゃ──」
ワイ「複数AIで爆速開発や!」
娘「──なんてのは、夢のまた夢だね」
ワイ「せやな・・・」
娘「だから、まずは責任ごとにクラスをちゃんと分割しないと」
ユーザー関連の処理を担当するクラス
class UserService {
public register(data: UserData): User { /* ... */ }
public setProfileImage(data: ProfileImageData): User { /* ... */ }
}
記事関連の処理を担当するクラス
class PostService {
public create(data: PostData): Post { /* ... */ }
public tag(data: TagData): Post { /* ... */ }
}
娘「↑こんな風に、責任ごとにクラスを分割するの」
ワイ「ほうほう」
娘「そうすれば、複数のAI君たちが同時開発しても、コンフリクトしにくくなるでしょ?」
ワイ「おお、確かにな」
ワイ「Cursor君はUserService
を、Devin君はPostService
を修正する、みたいに」
ワイ「それぞれが別のファイルを触ることになるから平和やな」
娘「そう」
娘「あと、エンドポイントのパスも/do-anything
1つだけで良いわけなくて」
娘「機能ごとに細かく分けようね」
ワイ「か、かしこまりましたやで」
娘「それと、抽象的なインターフェースに依存する設計にしておくと、更にコンフリクトが起きにくくなるよ」
ワイ「インターフェース?」
娘「例えば、記事を投稿したらメールで通知する機能があったとして」
娘「最初はこんな風に、具体的なメール送信クラスを直接使ってたとするでしょ?」
悪い例:具体的なクラスに直接依存
import { MailSender } from './MailSender';
class PostService {
private mailSender = new MailSender();
public create(postData: PostData): void {
// ... 記事作成処理 ...
this.mailSender.sendMail("新しい記事が投稿されました!");
}
}
ワイ「うんうん」
娘「でも、後から『メール通知じゃなくて、Slack通知に変えたい』ってなったらどうする?」
ワイ「えっと・・・」
ワイ「MailSender
を使ってる全部のファイルを修正せなアカンな」
ワイ「PostService.ts
、CommentService.ts
、UserService.ts
・・・」
ワイ「あと、他にもMailSender
を使うてるファイルがあったら、そっちも全部や」
娘「そうそう」
娘「例えば10個のファイルを修正する必要があったとしたら・・・」
ワイ「あー、他のAI君たちの作業と、コンフリクトしやすくなるわけか」
ワイ「修正箇所が多ければ多いほど、コンフリクトする確率は高くなるもんな」
娘「そうだね」
娘「でも、抽象化したインターフェースを使った設計にしておけば──」
良い例:インターフェースに依存
// 通知機能のインターフェース(抽象)
interface Notifier {
send(message: string): void;
}
class PostService {
private notifier: Notifier;
// コンストラクタで何らかの通知方法を受け取る
constructor(notifier: Notifier) {
this.notifier = notifier;
}
public create(postData: PostData): void {
// ... 記事作成処理 ...
this.notifier.send("新しい記事が投稿されました!");
}
}
ワイ「なるほど、メールという具体的な方法に依存するんやなくて」
ワイ「何らかの通知方法っていう抽象的な方法に依存するんやな」
娘「そうそう」
娘「そうしておくと、通知方法を変えたくなったときは──」
NotifierFactory.ts
- import { MailNotifier } from './MailNotifier';
+ import { SlackNotifier } from './SlackNotifier';
export function createNotifier(): Notifier {
- return new MailNotifier(); // メール通知の場合
+ return new SlackNotifier(); // Slack通知の場合
}
娘「──こうやって」
娘「通知方法を決めるファイルの数行だけを修正すればOK」
ワイ「おお」
ワイ「さっきはファイルを何個も修正する必要があったのに」
ワイ「今度は1個のファイルの数行だけなんやね」
娘「そう」
娘「修正箇所が少なければ少ないほど、コンフリクトする確率も下がるでしょ?」
娘「そうすると、複数のAI君が同時に作業してても安心だよね」
ワイ「なるほどな」
- AIがコードを書く時代でも、既存の設計論は役に立つ
-
単一責任原則
- 1つのクラスには1つの責任だけを持たせる
- クラスを適切に分割することで、コンフリクトを減らし、AIによる並行開発がスムーズになる
-
インターフェースへの依存
- 具体的な実装ではなく、抽象的なインターフェースに依存する
- 疎結合な設計になり、変更の影響範囲が狭くなり、複数AIで保守しやすいシステムになる
- 変わりにくいものに依存すべし
ワイ「↑こういうことやな」
ワイ「生成AI君たちが開発してくれる時代でも、今までの設計原則は引き続き有効なんやな」
娘「そうだね」
娘「いつかは、何もかもAIに任せて、gitのコンフリクトさえも気にせずに開発できる時代が来るのかもしれないけど」
娘「今のところはちゃんと設計して、理解してAIに頼むのが良いんじゃないかな」
ワイ「せやな・・・これからはちゃんと設計も勉強するわ・・・」
〜おしまい〜
Views: 0