バックエンド兼インフラエンジニアのrevenue-hackです!
その時にGPT-4にやってもらったらどうなんだろう?とふと思い、実際にユースケースからドメインモデルを作ってもらいました!
結論
- DDDでドメインモデルは割と高い精度で集約を出してくれる
- ただドメインモデル図までは作ってくれない(プロンプトのやり方しだいでは出来そう)
- 作ってもらうにはユースケースを洗い出しておく必要がある
- コードはある程度は書いてもらえるが、使えるかはケースバイケース
記事執筆者:オザック
Web開発(バックエンド兼インフラ)を生業にしている、エンジニア歴9年以上のrevenue-hackです!
某有名R社で働き、副業も含めて個人事業主で関わってきたプロジェクトは20以上。
DDDやクリーンアーキテクチャ、AWS、RDBの設計、チューニングなどを教えたり、実装していたりします!
現在はフリーランスとして従事。
目次
GPT-4でDDDのドメインモデルを作る方法
手順
- DDDでドメインモデルを作ってねと送る
- ユースケースを箇条書きで送る
- 後は待つだけ
です!
ドメインモデルを作るのに必要なもの
実際に以下は送ったユースケースです。箇条書きで送るだけで行けました!!
- ユーザ新規作成
- 必須項目
- 名前: 255文字以内
- メールアドレス: 255文字以内
- パスワード: 12文字以上,英数字それぞれ最低1文字以上
- スキル
- タグ名(選択式)
- 評価: 1~5
- 年数: 0以上のint型(5年まで)
- 1つ以上
- 任意項目
- 自己紹介: 2000字以内
- 経歴
- 複数
- 詳細: 1000字以内
- 西暦from: 1970年以上のint型
- 西暦to: 1970年以上のint型、西暦fromよりも大きい数字
- 必須項目
- ユーザ更新
- 上記同様
- メンター募集作成
- 必須項目
- タイトル: 255文字以内
- カテゴリ(1つ)
- プログラミング
- マーケティング
- デザイン
- ライティング
- 動画・映像
- ビジネス
- 語学
- ライフスタイル
- 相談形式
- 単発
- 継続
- 相談方式
- チャット
- ビデオ通話
- ディスクリプション(2000文字)
- 予算(from円~to円)
- 1000円以上の数値
- 応募期間
- 1日単位
- 最大14日後まで可能
- ステータス(公開or中止)
- 公開・受付中止にすることができる
- 任意項目
- タグ(複数OK)
- 必須項目
- メンター募集に提案する
- 提案内容: 2000文字以内
- 提案できるのはスキルを5個以上持つユーザのみ
- プラン作成
- 以下必須項目
- タイトル: 255文字以内
- カテゴリ(1つ)
- 上記のカテゴリ参照
- タグ(5個まで)
- 選択式
- 内容
- 2000文字以内
- ステータス
- 公開or中止
- 料金体系
- 月額or単発
- 金額
- 任意項目
- 相談方式
- チャットorビデオ電話
- 相談方式
- 以下必須項目
- プラン契約リクエストを送る
- メッセージ(最大500文字)
- プランへの契約を承認する
- メッセージ(最大500文字)
このユースケースは「DDDの集約の決め方をサンプルを使って考えてみる」という記事のユースケースを使いました。
実際にGPT-4にドメインモデルを作ってもらった
まずはGPT-4にユースケースを送ります。
GPT-4にユースケース送る
ユースケース一覧
そうすると早くも結果を出し始めた!(体感10秒くらいだった)
GPT-4の考えたエンティティと値オブジェクト一覧
結果、こんな出力をしてくれました!
1.エンティティ
- User
- Skill
- Experience
- MentorRequest
- Proposal
- Plan
- ContractRequest
- Contract
2. 値オブジェクト
- UserName
- Password
- SkillTag
- SkillRating
- Introduction
- ExperienceDetail
- Year
- Title
- Category
- ConsultationType
- ConsultationMethod
- Description
- Budget
- ApplicationPeriod
- Status
- Tags
- Content
- Price
3. 集約
- User
- MentorRequest
- Plan
- Contract
4. ドメインイベント
- UserCreated
- UserUpdated
- UserLoggedIn
- MentorRequestCreated
- MentorRequestListed
- ProposalSubmitted
- PlanCreated
- ContractRequested
- ContractApproved
- PlanListed
と言う結果でした。
自分のとGPT-4の集約を比較してみる
自分が書いたドメインモデル図はこちらです!
集約サンプル例のモデリング図
(なぜか集約がAggregateという名前になったままになってますが、同じと考えてくださいw)
集約のみを比較してみると
GPT-4 | 自分で作成した |
|
|
※スマホでは横スクロールできます
と言う感じで割りと異なっていました。
ただ後ほど改めて集約について聞くと違った集約が提案されました!
User集約 |
|
MentorRequest集約 |
|
Proposal集約 |
|
Plan集約 |
|
ContractRequest集約 |
|
Contract集約 |
|
双方の違い!
自分のドメインモデル図と比較すると、ほぼ完全に一致していて、違いはTagを集約とするかどうかのみでした!
Tagに関してはユースケースの渡し方次第で、エンティティではなく値オブジェクトとして考えられるケースもあるため、それを考慮すると、同じになります!!!
(エンジニアもういらない?w)
ドメインモデル図は書ける?
GPT-4でドメインモデル図書いてくれる?
図は書けませんでした。
どうやら読者のコメントによると書けるようです!
ドメインモデルをそのままコードとして書いてもらう事はできる?
GPT-4で書いてもらったGo言語のエンティティコード
エンティティをつらつらと実装してくれています。
ただフィールドが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文字以上」です。
- 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をあえて人間の肩書きに当てはめるなら、プログラミングを結構やってきた、かなり優秀なインターン生と言ったところかなぁ〜と思います!
またMENTAで設計(DDDやクリーンアーキテクチャ)やAWSインフラ、IaC化などを教えていたりしまので、興味のある方は↓からどうぞ!
クラス設計(Clean, DDDなど)をカリキュラムを使って教えます!
サーバサイド&Terraform, CI/CD, AWSインフラの技術サポート
普段ゆる〜くアーキテクチャやデータベースのことなどを発信しています〜