Chat GPT-4にDDDのドメインモデルを考えさせたら凄かった件
バックエンド兼インフラエンジニアのrevenue-hackです!
DDD(ドメイン駆動設計)でドメインモデル考えますよね?
その時にGPT-4にやってもらったらどうなんだろう?とふと思い、実際にユースケースからドメインモデルを作ってもらいました!
結論からいうと
- DDDでドメインモデルは割と高い精度で集約を出してくれる
- ただドメインモデル図までは作ってくれない(プロンプトのやり方しだいでは出来そう)
- 作ってもらうにはユースケースを洗い出しておく必要がある
- コードはある程度は書いてもらえるが、使えるかはケースバイケース
です!
GPT-4でDDDのドメインモデルを作る方法
手順は
- DDDでドメインモデルを作ってねと送る
- ユースケースを箇条書きで送る
- 後は待つだけ
です!
ドメインモデルを作るのに必要なもの
必要なものはユースケースのみです!
実際に以下は送ったユースケースです。箇条書きで送るだけで行けました!!
- ユーザ新規作成
- 必須項目
- 名前: 255文字以内(ユニーク)
- メールアドレス: 255文字以内
- パスワード: 12文字以上,英数字それぞれ最低1文字以上
- スキル
- タグ名(選択式)
- 評価: 1~5
- 1つ以上
- 年数: 0以上のint型(5年まで)
- 任意項目
- 自己紹介: 2000字以内
- 経歴
- 詳細: 1000字以内
- 西暦from: 1970年以上のint型
- 西暦to: 1970年以上のint型、西暦fromよりも大きい数字
- 必須項目
- ユーザ更新
- 上記同様
- メールパスログイン
- メンター募集作成
- 必須項目
- タイトル: 255文字以内
- カテゴリ(1つ)
- プログラミング
- マーケティング
- デザイン
- ライティング
- 動画・映像
- ビジネス
- 語学
- ライフスタイル
- 相談形式
- 単発
- 継続
- 相談方式
- チャット
- ビデオ通話
- ディスクリプション(2000文字)
- 予算(from円~to円)
- 1000円以上の数値
- 応募期間
- 1日単位
- 最大14日後まで可能
- ステータス(公開or中止)
- 任意項目
- タグ(複数OK)
- 必須項目
- メンター募集一覧
- 表示するもの
- タイトル
- タグ一覧
- カテゴリ
- 予算
- 提案数
- 応募期間
- 単発or継続
- 提案したユーザ名
- ページネーション20件
- 表示するもの
- メンター募集に提案する
- 提案内容: 2000文字以内
- 提案できるのはスキルを5個以上持つユーザのみ
- プラン作成
- 以下必須項目
- タイトル: 255文字以内
- カテゴリ(1つ)
- 上記のカテゴリ参照
- タグ(5個まで)
- 選択式
- 内容
- 2000文字以内
- ステータス
- 公開or中止
- 相談形式
- 単発
- 継続
- 金額
- 任意項目
- 相談方式
- チャットorビデオ電話
- 相談方式
- 以下必須項目
- プラン契約リクエストを送る
- メッセージ(最大500文字)
- プランへの契約を承認する
- メッセージ(最大500文字)
- プラン一覧
- 表示するもの
- タイトル
- カテゴリ
- タグ
- 内容
- ステータス
- 料金体系
- 相談方式
- プランを作成したユーザ名
- ステータスが「中止」となっていないものだけ表示する
- ページネーション20件
- 表示するもの
このユースケースは「DDDの集約の決め方をサンプルを使って考えてみる」という記事のユースケースを使いました。
実際にGPT-4にドメインモデルを作ってもらった
まずはGPT-4にユースケースを送ります。
そうすると早くも結果を出し始めた!(体感10秒くらいだった)
結果、こんな出力をしてくれました!
- エンティティ:
- User
- Skill
- Experience
- MentorRequest
- Proposal
- Plan
- ContractRequest
- Contract
- 値オブジェクト:
- UserName
- Password
- SkillTag
- SkillRating
- Introduction
- ExperienceDetail
- Year
- Title
- Category
- ConsultationType
- ConsultationMethod
- Description
- Budget
- ApplicationPeriod
- Status
- Tags
- Content
- Price
- 集約:
- User
- MentorRequest
- Plan
- Contract
- ドメインイベント:
- UserCreated
- UserUpdated
- UserLoggedIn
- MentorRequestCreated
- MentorRequestListed
- ProposalSubmitted
- PlanCreated
- ContractRequested
- ContractApproved
- PlanListed
と言う結果でした。
自分のとGPT-4の集約を比較してみる
自分が書いたドメインモデル図はこちらです!
(なぜか集約がAggregateという名前になったままになってますが、同じと考えてくださいw)
集約のみを比較してみると
GPT-4
- User
- MentorRequest
- Plan
- Contract
自分
- ユーザ
- メンター募集
- メンターリクエスト
- メンタープラン
- 契約リクエスト
- 契約
- タグ
と言う感じで割りと異なっていました。
ただ後ほど改めて集約について聞くと違った集約が提案されました!
- User集約:
- 集約ルート:Userエンティティ
- 関連するエンティティ/値オブジェクト:Skill, Experience, Introduction
- MentorRequest集約:
- 集約ルート:MentorRequestエンティティ
- 関連するエンティティ/値オブジェクト:Title, Category, ConsultationType, ConsultationMethod, Description,Budget, ApplicationPeriod, Status, Tag
- Proposal集約:
- 集約ルート:Proposalエンティティ
- 関連するエンティティ/値オブジェクト:User, MentorRequest, Description
- Plan集約:
- 集約ルート:Planエンティティ
- 関連するエンティティ/値オブジェクト:Title, Category, Tag, Content, Status, ConsultationType, Price, ConsultationMethod
- ContractRequest集約:
- 集約ルート:ContractRequestエンティティ
- 関連するエンティティ/値オブジェクト:User, Plan, Message
- Contract集約:
- 集約ルート:Contractエンティティ
- 関連するエンティティ/値オブジェクト:User, Plan, Message
自分のドメインモデル図と比較すると、ほぼ完全に一致していて、違いはTagを集約とするかどうかのみでした!
Tagに関してはユースケースの渡し方次第で、エンティティではなく値オブジェクトとして考えられるケースもあるため、それを考慮すると、同じになります!!!
(エンジニアもういらない?w)
ドメインモデル図は書ける?
図は書けませんでした。
流石にそこまでは出来ないようですが、プロンプト次第ではUMLのコードを書き出すくらいはやってくれる可能性は十分にありそうです!
ドメインモデルをそのままコードとして書いてもらう事はできる?
実際に頼んでみました!
エンティティをつらつらと実装してくれています。
ただフィールドがpublicのためsetterも兼ねてしまっていて、カプセル化が出来ていないのが残念ではありました。
Userエンティティのコードを書いてもらった
UserエンティティのコードをGPT-4に試しに書いてもらいました。
package domain
import (
"errors"
"regexp"
)
// カスタムエラーを定義
var (
ErrInvalidName = errors.New("name must be unique and within 255 characters")
ErrInvalidEmail = errors.New("email must be within 255 characters")
ErrInvalidBiography = errors.New("biography must be within 2000 characters")
ErrInvalidSkillTag = errors.New("invalid skill tag")
ErrInvalidScore = errors.New("score must be between 1 and 5")
ErrInvalidExperience = errors.New("experience must be an integer between 0 and 5")
ErrInvalidWorkHistoryDetail = errors.New("work history detail must be within 1000 characters")
ErrInvalidWorkHistoryYear = errors.New("work history year must be an integer greater than or equal to 1970")
)
func NewUser(id UserID, name string, email string, password Password, skills []Skill, experience int, biography string, history []WorkHistory) (*User, error) {
if err := validateUserName(name); err != nil {
return nil, err
}
if err := validateEmail(email); err != nil {
return nil, err
}
if err := validateBiography(biography); err != nil {
return nil, err
}
if err := validate Experience(experience); err != nil {
return nil, err
}
for _, skill := range skills {
if err := validateSkillTag(skill.Tag); err != nil {
return nil, err
}
if err := validateSkillScore(skill.Score); err != nil {
return nil, err
}
}
for _, h := range history {
if err := validateWorkHistoryDetail(h.Detail); err != nil {
return nil, err
}
if err := validateWorkHistoryYear(h.From, h.To); err != nil {
return nil, err
}
}
return &User{
ID: id,
Name: name,
Email: email,
Password: password,
Skills: skills,
Experience: experience,
Biography: biography,
History: history,
}, nil
}
func validateUserName(name string) error {
if len(name) > 255 {
return ErrInvalidName
}
return nil
}
func validateEmail(email string) error {
if len(email) > 255 {
return ErrInvalidEmail
}
return nil
}
func validateBiography(biography string) error {
if len(biography) > 2000 {
return ErrInvalidBiography
}
return nil
}
func validateExperience(experience int) error {
if experience < 0 || experience > 5 {
return ErrInvalidExperience
}
return nil
}
func validateSkillTag(tag string) error {
if len(tag) == 0 {
return ErrInvalidSkillTag
}
return nil
}
func validateSkillScore(score int) error {
if score < 1 || score > 5 {
return ErrInvalidScore
}
return nil
}
func validateWorkHistoryDetail(detail string) error {
if len(detail) > 1000 {
return ErrInvalidWorkHistoryDetail
}
return nil
}
func validateWorkHistoryYear(from int, to int) error {
if from < 1970 || to < 1970 || from > to {
return ErrInvalidWorkHistoryYear
}
return nil
}
かなりの高い精度で書けています!
しいて突っ込むとすれば
- 文字列の長さ比較で
len
を使っているので、マルチバイト対応出来ていなかったり -
int
を使っていたり - publicフィールドになっていてカプセル化が出来ていなかったり
ということはありそうですが、自分でコードを書くよりも修正する方がこれであれば簡単なので、大変助かりますね!
値オブジェクトPasswordのコードを書いてもらった
ちなみに値オブジェクトのPasswordに関する実装も試しにしてもらいました!
パスワードの要件は
パスワード: 12文字以上,英数字それぞれ最低1文字以上
GPT-4に実装してもらった結果は
package domain
import (
"errors"
"regexp"
)
type Password struct {
Value string
}
func NewPassword(value string) (*Password, error) {
if err := validatePassword(value); err != nil {
return nil, err
}
return &Password{Value: value}, nil
}
func validatePassword(password string) error {
if len(password) < 12 {
return errors.New("password must be at least 12 characters long")
}
containsDigit, _ := regexp.MatchString(`\d`, password)
if !containsDigit {
return errors.New("password must contain at least one digit")
}
containsLowercase, _ := regexp.MatchString(`[a-z]`, password)
if !containsLowercase {
return errors.New("password must contain at least one lowercase letter")
}
containsUppercase, _ := regexp.MatchString(`[A-Z]`, password)
if !containsUppercase {
return errors.New("password must contain at least one uppercase letter")
}
return nil
}
要件通りに実装できてしまっていますねwww
強い、、、ただエラーを無視しているのは良くないですがw
ちなみにテストコードも書いてもらいました。
package domain_test
import (
"testing"
"github.com/your_project/domain"
)
func TestNewPassword(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
input string
expectedError error
}{
{
name: "Valid password",
input: "Abc123456789",
expectedError: nil,
},
{
name: "Invalid password: too short",
input: "Abc123",
expectedError: domain.ErrPasswordTooShort,
},
{
name: "Invalid password: no digit",
input: "Abcdefghijkl",
expectedError: domain.ErrPasswordNoDigit,
},
{
name: "Invalid password: no lowercase",
input: "ABCDEFGHIJK1",
expectedError: domain.ErrPasswordNoLowercase,
},
{
name: "Invalid password: no uppercase",
input: "abcdefghijkl1",
expectedError: domain.ErrPasswordNoUppercase,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
password, err := domain.NewPassword(tc.input)
if err != tc.expectedError {
t.Errorf("expected error %v, got %v", tc.expectedError, err)
}
if err == nil && password.Value != tc.input {
t.Errorf("expected value %v, got %v", tc.input, password.Value)
}
})
}
}
// カスタムエラーを定義
var (
ErrPasswordTooShort = errors.New("password must be at least 12 characters long")
ErrPasswordMissingDigit = errors.New("password must contain at least one digit")
ErrPasswordMissingLowercase = errors.New("password must contain at least one lowercase letter")
ErrPasswordMissingUppercase = errors.New("password must contain at least one uppercase letter")
)
ちゃんとテーブルテストで書いてくれてるし、Parallelも入れてくれています!
エラー定義だけ既存コードのを使えうように書き換えれば、テストコードとしては完璧そうです!
GPT3.5だと出来ない?
ちょっとやってみましたが、かなり重く、全然レスが返ってこないためフリープランだと難しそうですね。
回答精度も確かめられなかったです!
まとめ: DDDのドメインモデルを作らせたらGPT-4は凄いけど、、、
GPT-4にドメインモデルを作らせると
- たたきとしては使えそう
- あまり微妙な集約だったらプロンプトを再度書き直して聞いてみると良い
- コードは単体のエンティティや値オブジェクトには有効(微修正程度で使える)
でした!
ではエンジニアは不要?かというとそれは今の所「否!」ですね!
- コードが正しいのか?
- 質を担保できているのか?
- 不要な実装はないか?
- コードや集約を修正する必要がある
などまだまだ人の目を使って精査が必要であり、コードの設計に関してはエンジニアがちゃんと担保して考えていく必要がありそうです。
GPT-4をあえて人間の肩書きに当てはめるなら、プログラミングを結構やってきた、かなり優秀なインターン生と言ったところかなぁ〜と思います!
↓DDDやクリーンアーキテクチャ、設計サポートをMENTAでやっています〜↓
またツイッターはこちら
Discussion
面白そうな記事ありがとうございます。gpt君にuml書いてもらうなら、plantUMLの形式を指定したら良いかと思います。
おっそうなんですね!
plantUMLで書いてってプロンプト投げれば行ける感じですか?