DDDやクリーンアーキテクチャの設計を学ぶ

Chat GPT-4にDDDのドメインモデルを考えさせたら凄かった件

バックエンド兼インフラエンジニアのrevenue-hackです!

 

DDD(ドメイン駆動設計)でドメインモデル考えますよね?

 

その時に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
  • Email
  • 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
  • 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)

 

ドメインモデル図は書ける?

GPT-4でドメインモデル図書いてくれる?

 

図は書けませんでした。

どうやら読者のコメントによると書けるようです!

 

オザック
流石にそこまでは出来ないようですが、プロンプト次第ではUMLのコードを書き出すくらいはやってくれる可能性は十分にありそうです!

 

ドメインモデルをそのままコードとして書いてもらう事はできる?

オザック
実際に頼んでみました!

 

GPT-4で書いてもらったGo言語のエンティティコード

 

エンティティをつらつらと実装してくれています。

ただフィールドがpublicのためsetterも兼ねてしまっていて、カプセル化が出来ていないのが残念ではありました。

 

Userエンティティのコードを書いてもらった

UserエンティティのコードをGPT-4に試しに書いてもらいました。

  1. package domain
  2. import (
  3.     "errors"
  4.     "regexp"
  5. )
  6. // カスタムエラーを定義
  7. var (
  8.     ErrInvalidName = errors.New("name must be unique and within 255 characters")
  9.     ErrInvalidEmail = errors.New("email must be within 255 characters")
  10.     ErrInvalidBiography = errors.New("biography must be within 2000 characters")
  11.     ErrInvalidSkillTag = errors.New("invalid skill tag")
  12.     ErrInvalidScore = errors.New("score must be between 1 and 5")
  13.     ErrInvalidExperience = errors.New("experience must be an integer between 0 and 5")
  14.     ErrInvalidWorkHistoryDetail = errors.New("work history detail must be within 1000 characters")
  15.     ErrInvalidWorkHistoryYear = errors.New("work history year must be an integer greater than or equal to 1970")
  16. )
  17. func NewUser(id UserID, name string, email string, password Password, skills []Skill, experience int, biography string, history []WorkHistory) (*User, error) {
  18.     if err := validateUserName(name); err != nil {
  19.         return nil, err
  20.     }
  21.     if err := validateEmail(email); err != nil {
  22.         return nil, err
  23.     }
  24.     if err := validateBiography(biography); err != nil {
  25.         return nil, err
  26.     }
  27.     if err := validate    Experience(experience); err != nil {
  28.         return nil, err
  29.     }
  30.     for _, skill := range skills {
  31.         if err := validateSkillTag(skill.Tag); err != nil {
  32.             return nil, err
  33.         }
  34.         if err := validateSkillScore(skill.Score); err != nil {
  35.             return nil, err
  36.         }
  37.     }
  38.     for _, h := range history {
  39.         if err := validateWorkHistoryDetail(h.Detail); err != nil {
  40.             return nil, err
  41.         }
  42.         if err := validateWorkHistoryYear(h.From, h.To); err != nil {
  43.             return nil, err
  44.         }
  45.     }
  46.     return &User{
  47.         ID: id,
  48.         Name: name,
  49.         Email: email,
  50.         Password: password,
  51.         Skills: skills,
  52.         Experience: experience,
  53.         Biography: biography,
  54.         History: history,
  55.     }, nil
  56. }
  57. func validateUserName(name string) error {
  58.     if len(name) > 255 {
  59.         return ErrInvalidName
  60.     }
  61.     return nil
  62. }
  63. func validateEmail(email string) error {
  64.     if len(email) > 255 {
  65.         return ErrInvalidEmail
  66.     }
  67.     return nil
  68. }
  69. func validateBiography(biography string) error {
  70.     if len(biography) > 2000 {
  71.         return ErrInvalidBiography
  72.     }
  73.     return nil
  74. }
  75. func validateExperience(experience int) error {
  76.     if experience < 0 || experience > 5 {
  77.         return ErrInvalidExperience
  78.     }
  79.     return nil
  80. }
  81. func validateSkillTag(tag string) error {
  82.     if len(tag) == 0 {
  83.         return ErrInvalidSkillTag
  84.     }
  85.     return nil
  86. }
  87. func validateSkillScore(score int) error {
  88.     if score < 1 || score > 5 {
  89.         return ErrInvalidScore
  90.     }
  91.     return nil
  92. }
  93. func validateWorkHistoryDetail(detail string) error {
  94.     if len(detail) > 1000 {
  95.         return ErrInvalidWorkHistoryDetail
  96.     }
  97.     return nil
  98. }
  99. func validateWorkHistoryYear(from int, to int) error {
  100.     if from < 1970 || to < 1970 || from > to {
  101.         return ErrInvalidWorkHistoryYear
  102.     }
  103.     return nil
  104. }

 

かなりの高い精度で書けています!

しいて突っ込むとすれば

  • 文字列の長さ比較でlenを使っているので、マルチバイト対応出来ていなかったり
  • intを使っていたり
  • publicフィールドになっていてカプセル化が出来ていなかったり

ということはありそうですが、自分でコードを書くよりも修正する方がこれであれば簡単なので、大変助かりますね!

 

値オブジェクトPasswordのコードを書いてもらった

ちなみに値オブジェクトのPasswordに関する実装も試しにしてもらいました!

 

パスワードの要件は「パスワード: 12文字以上,英数字それぞれ最低1文字以上」です。

  1. package domain
  2. import (
  3.     "errors"
  4.     "regexp"
  5. )
  6. type Password struct {
  7.     Value string
  8. }
  9. func NewPassword(value string) (*Password, error) {
  10.     if err := validatePassword(value); err != nil {
  11.         return nil, err
  12.     }
  13.     return &Password{Value: value}, nil
  14. }
  15. func validatePassword(password string) error {
  16.     if len(password) < 12 {
  17.         return errors.New("password must be at least 12 characters long")
  18.     }
  19.     containsDigit, _ := regexp.MatchString(`\d`, password)
  20.     if !containsDigit {
  21.         return errors.New("password must contain at least one digit")
  22.     }
  23.     containsLowercase, _ := regexp.MatchString(`[a-z]`, password)
  24.     if !containsLowercase {
  25.         return errors.New("password must contain at least one lowercase letter")
  26.     }
  27.     containsUppercase, _ := regexp.MatchString(`[A-Z]`, password)
  28.     if !containsUppercase {
  29.         return errors.New("password must contain at least one uppercase letter")
  30.     }
  31.     return nil
  32. }

 

オザック

要件通りに実装できてしまっていますねwww

強い、、、ただエラーを無視しているのは良くないですがw

 

ちなみにテストコードも書いてもらいました。

  1. package domain_test
  2. import (
  3.     "testing"
  4.     "github.com/your_project/domain"
  5. )
  6. func TestNewPassword(t *testing.T) {
  7.     t.Parallel()
  8.     testCases := []struct {
  9.         name string
  10.         input string
  11.         expectedError error
  12.     }{
  13.         {
  14.             name: "Valid password",
  15.             input: "Abc123456789",
  16.             expectedError: nil,
  17.         },
  18.         {
  19.             name: "Invalid password: too short",
  20.             input: "Abc123",
  21.             expectedError: domain.ErrPasswordTooShort,
  22.         },
  23.         {
  24.             name: "Invalid password: no digit",
  25.             input: "Abcdefghijkl",
  26.             expectedError: domain.ErrPasswordNoDigit,
  27.         },
  28.         {
  29.             name: "Invalid password: no lowercase",
  30.             input: "ABCDEFGHIJK1",
  31.             expectedError: domain.ErrPasswordNoLowercase,
  32.         },
  33.         {
  34.             name: "Invalid password: no uppercase",
  35.             input: "abcdefghijkl1",
  36.             expectedError: domain.ErrPasswordNoUppercase,
  37.         },
  38.     }
  39.     for _, tc := range testCases {
  40.         tc := tc
  41.         t.Run(tc.name, func(t *testing.T) {
  42.             t.Parallel()
  43.             password, err := domain.NewPassword(tc.input)
  44.             if err != tc.expectedError {
  45.                 t.Errorf("expected error %v, got %v", tc.expectedError, err)
  46.             }
  47.             if err == nil && password.Value != tc.input {
  48.                 t.Errorf("expected value %v, got %v", tc.input, password.Value)
  49.             }
  50.         })
  51.     }
  52. }
  53. // カスタムエラーを定義
  54. var (
  55.     ErrPasswordTooShort = errors.New("password must be at least 12 characters long")
  56.     ErrPasswordMissingDigit = errors.New("password must contain at least one digit")
  57.     ErrPasswordMissingLowercase = errors.New("password must contain at least one lowercase letter")
  58.     ErrPasswordMissingUppercase = errors.New("password must contain at least one uppercase letter")
  59. )

 

ちゃんとテーブルテストで書いてくれてるし、Parallelも入れてくれています!

エラー定義だけ既存コードのを使えうように書き換えれば、テストコードとしては完璧そうです!

 

GPT3.5だと出来ない?

ちょっとやってみましたが、かなり重く、全然レスが返ってこないためフリープランだと難しそうですね。

 

回答精度も確かめられなかったです!

 

まとめ: DDDのドメインモデルを作らせたらGPT-4は凄いけど、、、

GPT-4にドメインモデルを作らせると

まとめ

  • たたきとしては使えそう
  • あまり微妙な集約だったらプロンプトを再度書き直して聞いてみると良い
  • コードは単体のエンティティや値オブジェクトには有効(微修正程度で使える)

でした!

 

オザック
ではエンジニアは不要?かというとそれは今の所「否!」ですね!

 

  • コードが正しいのか?
  • 質を担保できているのか?
  • 不要な実装はないか?
  • コードや集約を修正する必要がある

 

などまだまだ人の目を使って精査が必要であり、コードの設計に関してはエンジニアがちゃんと担保して考えていく必要がありそうです。

 

GPT-4をあえて人間の肩書きに当てはめるなら、プログラミングを結構やってきた、かなり優秀なインターン生と言ったところかなぁ〜と思います!

 

 

 

またMENTAで設計(DDDやクリーンアーキテクチャ)やAWSインフラ、IaC化などを教えていたりしまので、興味のある方は↓からどうぞ!

クラス設計(Clean, DDDなど)をカリキュラムを使って教えます!

サーバサイド&Terraform, CI/CD, AWSインフラの技術サポート

データベースの設計と仕組み解説(SQLチューニングもあり)

 

普段ゆる〜くアーキテクチャやデータベースのことなどを発信しています〜

Twitter@taraganoko1014

 

© 2024 エンジニア副業道場 Powered by AFFINGER5