今の現場で初めてDDDに触れたので、よく採用されるアーキテクチャとしてレイヤードアーキテクチャを自分で0から実装してみました。
言語もよくセットで採用されているGoを採用してみました。
この記事の目的
- 0から実装して体系的にDDDとレイヤードアーキテクチャを学ぶ
- DDDに触れたことがない方にもわかりやすく説明する
そもそもDDD(ドメイン駆動設計)とは
要約(引用)すると「ドメインの知識に焦点を当てた設計手法」です。
たとえば電子カルテのシステムを例に取ってみます。
電子カルテには患者情報や手術の予定、入院ベッドの空き具合などの概念があると考えられます。
医療関係者ではないソフトウェアエンジニアは実際につかうユーザー(医療関係者)が直面している問題やドメイン(領域)の概念、事象を理解することが必要です。
それらを理解し、ソフトウェアに落とし込む。落とし込み続けることを実践する開発手法です
詳しくはこちらの記事を参照していただければと思います
https://codezine.jp/article/detail/11968
レイヤードアーキテクチャとは
図のように各層ごとに責務を切り分け、依存の方向を一方向にするようなアーキテクチャです。
純粋なレイヤードアーキテクチャでは上のような図となるのですが、DDDではDIP(依存関係逆転の原則)を用いて下のような依存関係になります。
infrastructureがdomainに依存するようになりました。
このようにDDDではdomainを中心にして、その中心に向かって依存するようにしていきます。(これはよく比較されるクリーンアーキテクチャなどでも同様ですね。)
各層の役割
domain
アーキテクチャの中心となるもっとも重要な層です。
業務の関心事が集まっています
ドメインのルールやデータの加工などのビジネスロジックが置かれます。
infrastructure
技術的基盤へのアクセスを提供する層です。
データを永続化するためにDBの操作をしたり、メールを送信するために外部のメール送信サービスとやり取りをしたりします。
usecase
その名の通りユースケースを担当します。
interface層から受け取ったリクエストをもとにデータの参照や保存、削除などを制御します
applicationと呼ばれることもあります。
interface
ユーザーからのリクエストを受け取ったり、usecase層からのレスポンスをユーザーに返す層です。
ソースコード解説
ここからは実際のソースコードを見ながら解説していきます
ソースコードは以下のリンクから取得できます。
https://github.com/ryokky59/go-layered-architecture-sample
dockerをインストールしてあれば動かせるようになっているのでよろしければ動かしながら見てみてください。
domain層
domain層はmodelとrepositoryに分けています。
まず、modelです。
modelにはそのmodelの構造体やビジネスロジックが置かれています。
taskがどういうものかを定義していきます。(今回はコンストラクタとセッターのみ)
理想はこのファイルを見ればtaskがどういうmodelかわかることです。
今回はtitleは何かしら値が必要だということがわかります。
package model
import (
"errors"
)
// Task taskの構造体
type Task struct {
ID int
Title string
Content string
}
// NewTask taskのコンストラクタ
func NewTask(title, content string) (*Task, error) {
if title == "" {
return nil, errors.New("titleを入力してください")
}
task := &Task{
Title: title,
Content: content,
}
return task, nil
}
// Set taskのセッター
func (t *Task) Set(title, content string) (error) {
if title == "" {
return errors.New("titleを入力してください")
}
t.Title = title
t.Content = content
return nil
}
次はrepositoryです。
ここにはDBとのやりとりを定義します(今回はCRUDを定義しています)
domain層には技術的関心事
を実装してはいけないというルールがあります。
技術的関心事というのは「DBにMySQLを使って〜」や「ORMを使って〜」などです。
これらをdomain層に記述しないことによってdomain層が特定の技術に依存しないようになります。
なのでここにはCRUDの各メソッドをinterface
として定義しておいて、実装が書いてあるinfrastructure層がこのdomain層に依存するようにしてあげます。(この部分が依存関係逆転の原則
を適用しているところです)
package repository
import (
"sample/domain/model"
)
// TaskRepository task repositoryのinterface
type TaskRepository interface {
Create(task *model.Task) (*model.Task, error)
FindByID(id int) (*model.Task, error)
Update(task *model.Task) (*model.Task, error)
Delete(task *model.Task) error
}
infrastructure層
特定の技術基盤にアクセスする層です。
ここで先程のrepositoryに記述してあったinterfaceに合わせて、中身のメソッドを実装していきます。
domain層に定義、infrastructure層に実装、と分けることでプロジェクトで使っているDBやORMが変更したとしてもinfrastructure層の実装のみを修正すればいいだけになります。
今回はDBにMySQL、ORMにgormを使っています。
package infra
import (
"sample/domain/model"
"sample/domain/repository"
"github.com/jinzhu/gorm"
)
// TaskRepository task repositoryの構造体
type TaskRepository struct {
Conn *gorm.DB
}
// NewTaskRepository task repositoryのコンストラクタ
func NewTaskRepository(conn *gorm.DB) repository.TaskRepository {
return &TaskRepository{Conn: conn}
}
// Create taskの保存
func (tr *TaskRepository) Create(task *model.Task) (*model.Task, error) {
if err := tr.Conn.Create(&task).Error; err != nil {
return nil, err
}
return task, nil
}
// FindByID taskをIDで取得
func (tr *TaskRepository) FindByID(id int) (*model.Task, error) {
task := &model.Task{ID: id}
if err := tr.Conn.First(&task).Error; err != nil {
return nil, err
}
return task, nil
}
// Update taskの更新
func (tr *TaskRepository) Update(task *model.Task) (*model.Task, error) {
if err := tr.Conn.Model(&task).Update(&task).Error; err != nil {
return nil, err
}
return task, nil
}
// Delete taskの削除
func (tr *TaskRepository) Delete(task *model.Task) error {
if err := tr.Conn.Delete(&task).Error; err != nil {
return err
}
return nil
}
usecase層
ここではユースケースに沿った処理の流れを実装します。
データの取得や保存などでDBにアクセスするときもdomain層のrepositoryを介してアクセスすることによって、infrastructure層ではなくdomain層のみに依存させています。
package usecase
import (
"sample/domain/model"
"sample/domain/repository"
)
// TaskUsecase task usecaseのinterface
type TaskUsecase interface {
Create(title, content string) (*model.Task, error)
FindByID(id int) (*model.Task, error)
Update(id int, title, content string) (*model.Task, error)
Delete(id int) error
}
type taskUsecase struct {
taskRepo repository.TaskRepository
}
// NewTaskUsecase task usecaseのコンストラクタ
func NewTaskUsecase(taskRepo repository.TaskRepository) TaskUsecase {
return &taskUsecase{taskRepo: taskRepo}
}
// Create taskを保存するときのユースケース
func (tu *taskUsecase) Create(title, content string) (*model.Task, error) {
task, err := model.NewTask(title, content)
if err != nil {
return nil, err
}
createdTask, err := tu.taskRepo.Create(task)
if err != nil {
return nil, err
}
return createdTask, nil
}
// FindByID taskをIDで取得するときのユースケース
func (tu *taskUsecase) FindByID(id int) (*model.Task, error) {
foundTask, err := tu.taskRepo.FindByID(id)
if err != nil {
return nil, err
}
return foundTask, nil
}
// Update taskを更新するときのユースケース
func (tu *taskUsecase) Update(id int, title, content string) (*model.Task, error) {
targetTask, err := tu.taskRepo.FindByID(id)
if err != nil {
return nil, err
}
err = targetTask.Set(title, content)
if err != nil {
return nil, err
}
updatedTask, err := tu.taskRepo.Update(targetTask)
if err != nil {
return nil, err
}
return updatedTask, nil
}
// Delete taskを削除するときのユースケース
func (tu *taskUsecase) Delete(id int) error {
task, err := tu.taskRepo.FindByID(id)
if err != nil {
return err
}
err = tu.taskRepo.Delete(task)
if err != nil {
return err
}
return nil
}
interface層
リクエスト、レスポンスを取り扱う層です。
usecase層と切り離すことでリクエストやレスポンスがどんな形に変わってもここの修正だけで済むようになります。
今回はHTTPリクエストを受け取り、レスポンスをjsonとして返すようにしています。
package handler
import (
"net/http"
"strconv"
"sample/usecase"
"github.com/labstack/echo"
)
// TaskHandler task handlerのinterface
type TaskHandler interface {
Post() echo.HandlerFunc
Get() echo.HandlerFunc
Put() echo.HandlerFunc
Delete() echo.HandlerFunc
}
type taskHandler struct {
taskUsecase usecase.TaskUsecase
}
// NewTaskHandler task handlerのコンストラクタ
func NewTaskHandler(taskUsecase usecase.TaskUsecase) TaskHandler {
return &taskHandler{taskUsecase: taskUsecase}
}
type requestTask struct {
Title string `json:"title"`
Content string `json:"content"`
}
type responseTask struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
}
// Post taskを保存するときのハンドラー
func (th *taskHandler) Post() echo.HandlerFunc {
return func(c echo.Context) error {
var req requestTask
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
createdTask, err := th.taskUsecase.Create(req.Title, req.Content)
if err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
res := responseTask{
ID: createdTask.ID,
Title: createdTask.Title,
Content: createdTask.Content,
}
return c.JSON(http.StatusCreated, res)
}
}
// Get taskを取得するときのハンドラー
func (th *taskHandler) Get() echo.HandlerFunc {
return func(c echo.Context) error {
id, err := strconv.Atoi((c.Param("id")))
if err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
foundTask, err := th.taskUsecase.FindByID(id)
if err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
res := responseTask{
ID: foundTask.ID,
Title: foundTask.Title,
Content: foundTask.Content,
}
return c.JSON(http.StatusOK, res)
}
}
// Put taskを更新するときのハンドラー
func (th *taskHandler) Put() echo.HandlerFunc {
return func(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
var req requestTask
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
updatedTask, err := th.taskUsecase.Update(id, req.Title, req.Content)
if err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
res := responseTask{
ID: updatedTask.ID,
Title: updatedTask.Title,
Content: updatedTask.Content,
}
return c.JSON(http.StatusOK, res)
}
}
// Delete taskを削除するときのハンドラー
func (th *taskHandler) Delete() echo.HandlerFunc {
return func(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
err = th.taskUsecase.Delete(id)
if err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
return c.NoContent(http.StatusNoContent)
}
}
DI
各層の説明は先程のinterface層までで終わりです。
最後にDI(依存性の注入)しているところを説明します。
今回はmain.goにDIポイントを記述しました。
main関数内の上から3行目までのところでDIを行っています。
ここで各層に記述されていたコンストラクタ(New〇〇といった名前の関数)が使われます。
package main
import (
"sample/config"
"sample/infra"
"sample/interface/handler"
"sample/usecase"
_ "github.com/jinzhu/gorm/dialects/mysql"
"github.com/labstack/echo"
)
func main() {
taskRepository := infra.NewTaskRepository(config.NewDB())
taskUsecase := usecase.NewTaskUsecase(taskRepository)
taskHandler := handler.NewTaskHandler(taskUsecase)
e := echo.New()
handler.InitRouting(e, taskHandler)
e.Logger.Fatal(e.Start(":8080"))
}
さいごに
ここまで読んでいただきありがとうございます。
普段なにげなく実装していますが、人に説明するのはとても大変だなと書いてて思いました笑
ただ、記事にしたおかげで自分の中で理解がとても深まったので良かったです。
まだDDDには値オブジェクトだったりファクトリだったり色んな要素があるので気になった方はぜひ調べてみてください(もしかしたらまた記事にするかもしれません)
今回使ったソースコードのサンプルはこちらになります。
https://github.com/ryokky59/go-layered-architecture-sample
参考文献
【Golang + レイヤードアーキテクチャー】DDD を意識して Web API を実装してみる