9
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

レイヤードアーキテクチャ+DDDでAPI開発[入門]

※ 本記事はレイヤー分けに終始しています。モデリングを含む真のDDDを検討の場合は、別を参照することを強くお勧めします。

前書き

  • Goやその周辺ライブラリ、認証処理などについての説明は省きます。
  • 著者の理解が甘く間違っている箇所もあるかもしれません。その際は優しく教えていただけると幸いです。
  • レイヤードアーキテクチャは具体的なディレクトリ構成等を示すものではなく、あくまで概念的な指標です。正解はなく、本記事も一例に過ぎません。

なるべく初学者にもわかりやすい様に書きました。
始めは実装コード等理解しづらいと思いますが、概念的な理解の足掛かりにしていただけると嬉しいです☺️

サンプルコード: https://github.com/karamaru-alpha/layered-arch-sample

レイヤードアーキテクチャって何?

概要

ソフトウェアの関心事をレイヤー毎に分離するための設計方法の一つです。
他にもクリーンアーキテクチャ・オニオンアーキテクチャなどが有名ですが、どれも責務・依存関係を明確化するというゴールは変わりません。

メリット

アーキテクチャを導入する主なメリットは以下の二つです。

  • DBやUI・ビジネスロジックなど、仕様変更の際にコードの修正箇所が明確・軽微
  • 各レイヤーにおいてテストする目的が明確・処理をモックしやすいことからテストしやすい

仕様変更に強くテストもしやすいことから、保守運用しやすいということで人気なんですね👀

レイヤー解説

※矢印は依存方向です。依存とは、関数や変数を参照することを指します。
スクリーンショット 2020-09-12 23.08.29.png
レイヤードアーキテクチャ+DDDの基本はこの図です。
純正レイヤードアーキテクチャではdomain->infrastructure層という依存関係でしたが、「ビジネスの中核をなすdomainが、使用DB(infra)などに依存する・影響されるっておかしくない?」と言う事で、DDDではdomainが依存の最高位に君臨しています。これを依存関係逆転の原則(DIP)っていうみたいです👀

domain層: ビジネスルールの中核の定義を担当
infrastructure層: DB通信・DBデータ更新を担当
usecase層: アプリケーション固有のビジネスルールを担う処理を担当
interface層: ユーザーからのリクエスト受け取りや、表示に関することを担当

大まかにはこんな役割です。このままではわかりづらいので以下で詳しく説明します!

domain層

ここでは「ドメインモデル」と「リポジトリ」を定義します。

ドメインモデル

一言でいうとビジネスルールの中核・対象の存在です。
ex)「物を売り買いする」というビジネスルールでは、「ユーザー」「商品」などがドメインモデルとして定義される。

SQLのテーブルとよく似ていますが、そのビジネスルールで使用しないデータの場合はdomainとして扱わないという点で異なります
ex) SQLには解析用にcreated_atが保存されているが、サービス上は使用しないためドメインモデルには持たせないなど。

以下、ユーザーのドメインモデルの例です。

path/to/domain/model/user/model.go
package user

type User struct {
    ID    int32
    Name  string
}

リポジトリ

ドメインモデルになんらかの変更をする処理を定義する所です。

注意したいのは、ここで定義されているモノは「ドメインモデルこうやって変更するよ!」と抽象的に宣言しているだけで、具体的な処理を記述したものではないという事です。「ドメイン層は、他の仕様変更(使用DBやUI)に影響されない(依存しない)」という思想の元、リポジトリには具体的な処理を記述せず、抽象的な宣言に止まります
具体的には、Interfaceを用いて抽象的なドメインの更新処理を表現しています。

ここら辺難しいですよねorz
イメージは、リポジトリという上司が「ドメインモデルに対してこんなことしたい!」と算段なく抽象的言っている感じで、DB更新などの具体的な処理はinfra層などの部下がリポジトリの意図(Interface)を汲み取って行います

以下、リポジトリの例です。ユーザーというドメインモデルに対して、「こんな引数・戻り値で名前を更新するぞ!」という抽象的な宣言がなされています。

path/to/domain/repository/user/repository.go
package user

import "path/to/domain/model/user"

type Repository interface {
    UpdateName(record *user.User, name string) error
}

infrastructure層

ここでは実際にDBと通信し、リポジトリに記載された「抽象的なドメインモデルの更新処理」を実現します

リポジトリで宣言されたInterface(抽象的な更新処理)を継承し、infra層での実際の更新処理をメソッド化しています。
そうすることによって、リポジトリで定義された抽象的な更新処理がinfra層の具体的な更新処理と紐づけられています。(go#interfaceの仕様上、整合性が取れないとエラーを吐くためです。もっと噛み砕けば、リポジトリで宣言された抽象的な更新処理が、infra層に実装されていなければエラーを吐くということです。)

ユーザーの名前変更を例にコードをみてみましょう。

  • infra層に使用DBを渡す(サーバー起動等を定義するserver.goに書いています。)
  • リポジトリで定義したInterface(抽象的な更新処理)をinfra層(repositoryImpl)に継承
  • UpdateNameメソッドで実際のDB更新処理を記述

を行っています。(使用DBはinfra層にベタ書きでもいいのですが、sqlmockでテストしやすいためserver.goから渡しています。)

server.go
package server

import (
    ur "path/to/infrastructure/repositoryimpl/user"
)
...
    // mysql.Conn = sqlへのコネクション。sql.Open("mysql"...
    ur.NewRepositoryImpl(mysql.Conn)
...
path/to/infrastructure/repositoryimpl/user/repositoryimpl.go
package user

import (
    um "path/to/domain/model/user"
    ur "path/to/domain/repository/user"
    "database/sql"
)
type repositoryImpl struct {
    db *sql.DB
}

// NewRepositoryImpl Userリポジトリで定義したinterfaceを継承
func NewRepositoryImpl(db *sql.DB)ur.Repository {
    return &repositoryImpl{
        db,
    }
}

// UpdateName repositoryImplのメソッドとして、実際のDB更新。
func (impl repositoryImpl) UpdateName(record *um.User, newName string) error {
    stmt, err := impl.db.Prepare("UPDATE user SET name = ? WHERE id = ?")
    if err != nil {
        return err
    }
    _, err = stmt.Exec(newName, record.ID)
    return err
}

usecase層

ここではアプリケーション固有のビジネスルールを記述します。
ビジネスルールってなんだよ!って感じですよね笑

例として、「一定額以上の買い物をした人に対して、ある還元率でポイントを付与する」というケースで考えてみましょう。おさらいですが、実際にUserというドメインモデルの所持ポイントを更新するのは(抽象的に)リポジトリの役目ですよね?
しかし、いくら以上の支払いでポイントを付与するか・ポイント還元率は何%かなどは、実際の更新というよりも、更新に使うデータの選定・整形に近いと思います。これが「アプリケーション固有のビジネスルール」と言われている部分です。

それらを記述するのがusecase層です。interface層(handler)から渡されたデータを整形し、必要に応じてそのデータをリポジトリに渡します

コードをみてみましょう。例示が行き来して申し訳ないですが、名前更新処理です。

  • 先ほど作成した実際のデータ更新処理が記載されたinfra層(userRepoImpl)をユースケースに(リポジトリを介して間接的に)継承
  • interface層から渡されたnameから更新処理を目論む。データ更新の際は、継承されたrepositoryImplを(リポジトリを介して間接的に)叩く
server.go
package server

import (
    ur "path/to/infrastructure/repositoryimpl/user",
    uu "path/to/usecase/user"
)
...
    userRepoImpl := ur.NewRepositoryImpl(mysql.Conn)
    uu.NewUseCase(userRepoImpl)
...
path/to/usecase/user/usecase.go
package user

import (
    um "path/to/domain/model/user"
    ur "path/to/domain/repository/user"
)

// interface層で参照するためInterfaceで切り出し
type UseCase interface {
    UpdateName(user *um.User, name string) error
}


type useCase struct {
    repository ur.Repository
}

// NewUseCase Userリポジトリ(domain層)を継承したrepositoryImpl(infra層)を間接的に参照する
func NewUseCase(userRepo ur.Repository) UseCase {
    return &useCase{
        repository: userRepo,
    }
}


// UpdateName ユーザーの名前を更新するユースケース
func (ur useCase) UpdateName(user *um.User, name string) error {
    return ur.repository.UpdateName(user, name)
}

interface層

ここではrequest/responseの整形・バリデーションを行います。

ユーザーへの表示をメインとしたこの層では、具体的なビジネスロジックは書かないことが重要です。APIではリクエストが不正値かどうかを確認した後ユースケースを呼び出し、返り値を整形してレスポンスすることだけが役目です。例えば名前更新処理では、長すぎる文字列クエストはこの層でrejectします。

またinterfaceはGo言語の予約語なので、ディレクトリ作成時はinterfaces/とかにしましょう。

以下、名前更新の例です。

  • 先ほど作ったuserのusecaseを継承
  • リクエストのバリデーションを行い、そのデータをusecaseに渡す(呼び出す)。
  • 返り値を整形してレスポンス
server.go
import (
    ur "path/to/infrastructure/repositoryimpl/user",
    uu "path/to/usecase/user",
    uh "path/to/interfaces/api/handler/user"
    "net/http"
    "github.com/labstack/echo"
)

func Serve(addr string) {
    userRepoImpl := ur.NewRepositoryImpl(mysql.Conn)
    userUsecase := uu.NewUseCase(userRepoImpl)
    userHandler := uh.NewHandler(userUsecase)


    e := echo.New()
    // contextにユーザー情報を入れるAuthenticateは各々でお願いします。
    e.PATCH("/user/update", Authenticate(userHandler.HandleUpdate())))


    ...サーバー起動処理...
}
path/to/interfaces/api/handler/user.go
package user

import (
    um "path/to/domain/user"
    uu "path/to/usecase/user"
    "encoding/json"
    "http/net"
)

// Handler UserにおけるHandlerのインターフェース
type Handler interface {
    HandleUpdate(c echo.Context) error
}

type handler struct {
    useCase uu.UseCase
}

// NewHandler Userデータに関するHandlerを生成
func NewHandler(userUseCase uu.UseCase) Handler {
    return &handler{
        useCase: userUseCase,
    }

// HandleUpdate ユーザ情報更新処理
func (uh handler) HandleUpdate(c echo.Context) error {
    type response struct {
        Message string `json:"message"`
    }

    requestBody := new(um.User)
    if err := c.Bind(requestBody); err != nil || requestBody.Name == "" {
        return c.JSON(http.StatusBadRequest, &response{Message: "User updation failed"})
    }

    ...名前の長さバリデーションとかもここで行う...
    ...変数userにcontextなどから現在のユーザー情報を入れる処理...

    if err := uh.useCase.UpdateName(user, requestBody.Name); err != nil {
        return c.JSON(http.InternalServerError, &response{Message: "User updation failed"})
    }

    return c.JSON(http.StatusOK, &response{Message: "User successfully updated"})
}

まとめ

以上がレイヤードアーキテクチャ+DDDの構成例解説です。
稚拙な文章を最後まで読んでくださりありがとうございました。
ご指摘・ご感想あればコメントか、twitterにまで連絡していただけると幸いですmm

参考にした記事は以下ですmm
今すぐ「レイヤードアーキテクチャ+DDD」を理解しよう。(golang)
【Golang + レイヤードアーキテクチャ】DDD を意識して Web API を実装してみる

それではみなさん、楽しい開発ライフを!!

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
9
Help us understand the problem. What are the problem?