前段
転職活動の一環で、簡易なバックエンドポートフォリオを作成しました。
なおコードを見せるのが目的なため、アプリケーションの中身は簡易な TODO アプリとしました。
作業時間短縮のために画面も用意していなければデプロイも想定していないので、Docker を立ち上げてローカルで curl を叩くとローカル MySQL で CRUD 処理ができるというだけの機能しか有しておりません。
(なお同様の理由で単体テストの実装も割愛しております)
概要
今回「ドメイン駆動設計(DDD)」を意識しました。
業務上でも一部 DDD の設計が取り入れられてはおりますが、本ポートフォリオにおいてはオリジナルの極めてシンプルな構成にしております。
ディレクトリ構成
大まかには、下記のような構成です。
.
├── api/
│ └── openapi
├── application/
│ ├── server/
│ │ └── main.go
│ └── usecases/
│ └── todos.go
├── domain/
│ └── models/
│ └── todo.go
├── infrastructure/
│ ├── mysql/
│ │ ├── initials
│ │ ├── migrations
│ │ └── tables
│ └── repositories/
│ └── todo.go
└── presentation/
└── controllers/
├── controllers.go
└── todo.go
ポイントは、大きく下記のカテゴリーに分けている点です。
- Application
- Usecase
- Domain
- Model
- Infrastructure
- Repository
- Presentation
- Controller
設計方針
伝統的な設計ですと、下記のような依存関係が発生してしまいます。
本ポートフォリオにおいては、下記のように Domain 層にビジネスロジックを凝集させ、その他の関心事を外側に追いやる DDD の考え方を採用しています。
アプリケーションにおいて最も重要なのはドメイン(ビジネスの関心事)であり、技術詳細などその他のことは外側に置くべきである、というクリーンアーキテクチャの考え方を表現しています。
以下、実装を一部取り上げます。
実装の一部抜粋
TodoController
(Presentation
)
- まずプレゼンテーション層でリクエストパラメーターを受け付けます
- 受け取ったパラメーターに問題が無ければ、アプリケーション層のメソッドを呼び出します
func (c *TodoController) Mount(group *echo.Group) {
group.POST("/", c.Create)
}
func (c *TodoController) Create(ec echo.Context) error {
params := &struct {
Content string `json:"content" validate:"required"`
}{}
if err := ec.Bind(params); err != nil {
return err
}
if err := ec.Validate(params); err != nil {
return err
}
output, err := c.todoInteractor.CreateTodo(
ec.Request().Context(),
&usecases.CreateTodoInput{
Content: params.Content,
},
)
if err != nil {
return err
}
return ec.JSON(http.StatusCreated, map[string]string{
"id": output.TodoID,
})
}
TodoInteractor(Usecase)
(Application
)
- ここでは(「TODOを作成する」という)ユースケースに沿ったメソッドを用意しています
- アプリケーション層からドメイン層のインターフェースを介して、Create メソッドを呼び出します
type TodoInteractor interface {
CreateTodo(ctx context.Context, input *CreateTodoInput) (*CreateTodoOutput, error)
}
type CreateTodoInput struct {
Content string
}
type CreateTodoOutput struct {
TodoID string
}
func (i *todoInteractor) CreateTodo(ctx context.Context, input *CreateTodoInput) (*CreateTodoOutput, error) {
todo := i.todoFactory.Create(&models.TodoFactoryOptions{
Content: input.Content,
})
if err := todo.Validate(); err != nil {
return nil, err
}
if err := i.todoRepository.Create(
ctx,
todo,
); err != nil {
return nil, err
}
return &CreateTodoOutput{
TodoID: todo.ID,
}, nil
}
TodoRepository
(Domain
)
- ドメイン層のインターフェースでは、Create メソッドのシグネチャのみを定義しています
- このドメイン層に業務ロジックを凝集させます
type TodoRepository interface {
Create(ctx context.Context, todo *Todo) error
}
type TodoFactory interface {
Create(options *TodoFactoryOptions) *Todo
}
TodoRepository
(Infrastructure
)
- インフラストラクチャ層では、先ほどの Create メソッドを実装しています
- それにより、技術詳細がここに隠蔽されることになります
type TodoRepository struct {
}
func (r *TodoRepository) Create(ctx context.Context, todo *models.Todo) error {
db, err := mysql.Open()
if err != nil {
return err
}
record := r.convertToTable(todo)
if err := record.Insert(ctx, db, boil.Infer()); err != nil {
return err
}
return nil
}
func (r *TodoRepository) convertToTable(todo *models.Todo) *tables.Todo {
return &tables.Todo{
ID: todo.ID,
Content: todo.Content,
CreatedAt: todo.CreatedAt,
UpdatedAt: todo.UpdatedAt,
CompletedAt: null.TimeFromPtr(todo.CompletedAt),
}
}
以上のような流れです。
本件のように簡単なアプリケーションだと効果が見えづらいのですが、開発対象の業務ロジックが複雑であればあるほど、ビジネスロジックをドメイン層に凝集し、余計な技術詳細を外側に出せることの威力が見えてくるかと思います。
なお本件では DDD の基本的な考え方を表現するに留めております。
API 設計書
API 設計書については、下記 redocly-cli を活用する前提で、openapi により表現しています。
コマンド実行により HTML ファイルが自動生成されると、下記のような API 設計書が出来上がります。
その他、補足
- Web フレームワークには Echo を採用しています
- MySQL の ORM としては SQLBoiler を採用し、自動生成したコードを活用しています
- 開発環境は Docker での構築を想定しています
- 設計には、Factory パターン、Repository パターン、などのデザインパターンも取り入れています
以上、お読みいただきありがとうございました!お疲れ様です🍵