12
7

More than 1 year has passed since last update.

DDDを意識したgolangでの実装

Last updated at Posted at 2022-02-15
1 / 12

本記事はDDDについて超わかりやすく記述されている以下書籍を読み、golangではどうなるかを記載したものです。

ドメイン駆動設計 モデリング/実装ガイド


ユースケース

例として、タスク管理アプリケーションを考える。

・タスクを登録する
・タスクを延期する
・タスクを完了する


オブジェクトの生成や更新時に守らなければいけないルール(ドメイン知識)

・タスクステータスは未完了/完了の2種類でタスク作成時は未完了ステータス
・タスク完了すると完了ステータスとなり、ステータスは戻せない
・タスクの期日は1日ずつ、3回まで延期できる

DDDで重要なのは、ドメイン層にドメイン知識を寄せて、ドメイン知識がわからない途中参入した他のエンジニアにもドメイン知識の遵守を強制できる状態にすることである


駄目なケース(ドメイン知識がドメイン層にない)を見てみる。

ドメイン層

type Task struct {
    ID            string
    TaskStatus    TaskStatus
    Name          string
    DueDate       time.Time
    PostponeCount int64
}

type TaskStatus string

const (
    TaskStatusUndone TaskStatus = "undone"
    TaskStatusDone  TaskStatus = "done"
)

ユースケース層

type TaskUseCase struct {
    ctx            context.Context
    taskRepository repository.TaskRepository
}
// CreateTask タスク作成
func (s *TaskUseCase) CreateTask(name string, dueDate time.Time) error {
    if name == "" || dueDate.IsZero() {
        return errors.New("必須項目が設定されていません。")
    }
    task := domain.Task{
        TaskStatus:    domain.TaskStatusUndone,
        Name:          name,
        DueDate:       dueDate,
        PostponeCount: 0,
    }
    if err := s.taskRepository.Save(ctx, task); err != nil {
        return err
    }
    return nil
}

// PostPoneTask タスク延期
func (s *TaskUseCase) PostponeTask(taskID string) error {
    task, err := s.taskRepository.GetByID(ctx, taskID)
    if err != nil {
        return err
    }
    const POSTPONE_MAX_COUNT = 3
    if task.PostponeCount >= POSTPONE_MAX_COUNT {
        return errors.New("最大延長回数を超過しています。")
    }
    task.DueDate = task.DueDate.Add(24 * time.Hour)
    task.PostponeCount = task.PostponeCount + 1
    if err := s.taskRepository.Save(ctx, task); err != nil {
        return err
    }
    return nil
}

要件を満たすものができたのでリリース。ここまでは問題なかった・・


しばらくして、新しく開発チームに参画したD君が以下を実装

ユースケース層

func (s *TaskUseCase) CreateDoneTask(name string, dueDate time.Time) error {
    task := domain.Task{
        Name:          name,
        DueDate:       dueDate,
        TaskStatus:    domein.TaskStatusDone, // 完了ステータスでタスク作成
        PostPoneCount: -1,                    // カウントにマイナスを設定
    }
    if err := s.taskRepository.Save(ctx, task); err != nil {
        return err
    }
    return nil
}

func (s *TaskUseCase) ChangeTask(taskID, dueDate time.Time, taskStatus domain.TaskStatus) error {
    task, err := s.taskRepository.GetByID(ctx, taskID)
    if err != nil {
        return err
    }
    task.DueDate = dueDate       // 勝手に期日を入力値で設定、延長回数も無視
    task.TaskStatus = taskStatus // タスクを未完了に戻せてしまう
    if err := s.taskRepository.Save(ctx, task); err != nil {
        return err
    }
    return nil
}

ドメイン知識とは何だったのかというレベルで破壊されました。
Taskクラスの属性が全てpublicである為ドメイン知識を把握していないメンバーが開発するとこういうことがいくらでも起きてしまいます。
不整合な処理が追加されたことにも気づきにくいです。


ではどうすれば良いのか。
ドメイン知識をドメイン層に持たせた良いパターンを見てみます。

type task struct {
    id            string // 全属性privateに。
    taskStatus    TaskStatus
    name          string
    dueDate       time.Time
    postponeCount int64
}

const POSTPONE_MAX_COUNT = 3

type TaskStatus string

const (
    TaskStatusUndone TaskStatus = "undone"
    TaskStatusDone  TaskStatus = "done"
)

func NewTask(name string, dueDate time.Time) (*task, error) {
    if name == "" || dueDate.IsZero() {
        return nil, errors.New("必須項目が設定されていません。")
    }
    return &task{
        taskStatus:    TaskStatusUndone,
        name:          name,
        dueDate:       dueDate,
        postponeCount: 0,
    }, nil
}

func (t *task) Postpone() (*task, error) {
    if t.postponeCount >= POSTPONE_MAX_COUNT {
        return nil, errors.New("最大延長回数を超過しています。")
    }
    t.dueDate.Add(24 * time.Hour)
    t.postponeCount++
    return t, nil
}

// getter
func (t *task) GetID() string {
    return t.id
}
func (t *task) GetName() string {
    return t.name
}
func (t *task) GetDueDate() time.Time {
    return t.dueDate
}

クラス外部から想定外の値を設定することができなくなりました。
結果として、この 1 クラスを見るだけですべてのオブジェクト生成、状態遷移のパターンがわかるようになりました。
コードの可読性が高まったので、コードからドメインモデルを理解することも、ドメインモデルに更新があった時に修正することも簡単です。


以下ユースケース層

type TaskUseCase struct {
    ctx            context.Context
    taskRepository repository.TaskRepository
}

func (s *TaskUseCase) CreateTask(name string, dueDate time.Time) error {
    task, err := domain.NewTask(name, dueDate)
    if err != nil {
        return err
    }
    if err := s.taskRepository.Save(ctx, task); err != nil {
        return err
    }
    return nil
}

func (s *TaskUseCase) PostponeTask(taskID string) error {
    task, err := s.taskRepository.GetByID(ctx, taskID)
    if err != nil {
        return err
    }
    postponedTask, err := task.Postpone()
    if err != nil {
        return err
    }
    if err := s.taskRepository.Save(ctx, postponedTask); err != nil {
        return err
    }
    return nil
}

ドメイン知識を表現する実装を全く持たなくなりました。
結果として、修正前に比べて比べてコード量が大幅に減り、「実装上どのように実現するか (How)」は隠蔽され、「何をしたいか (What)」だけを示すようになりました。


まとめ

ドメイン知識の理解度のばらつきが大きい多人数開発、大規模開発ほどDDDのメリットは大きい。
→ドメイン知識が何かわかっていないメンバーが開発してもドメイン知識の遵守を強制できるのはでかい。

参考書籍ではJavaでlombokライブラリを使用して簡単にgetter,setterを自動生成しているが、golangの場合Javaと違ってgetterの自動生成の手段が少ない。
getterを全て自分でコーディングするのは手間過ぎるのでジェネレータを自作したり先人が作成したジェネレータを取り入れたりしないと厳しい。GoLandなどのIDEの機能に頼るのもあり。

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7