※ 本記事はレイヤー分けに終始しています。モデリングを等含む設計を検討の場合は、別の記事参照することを強くお勧めします。
前書き
- Goやその周辺ライブラリ、認証処理などについての説明は省きます。
- 著者の理解が甘く間違っている箇所もあるかもしれません。その際は優しく教えていただけると幸いです。
なるべく初学者にもわかりやすい様に書きました。
始めは実装コード等理解しづらいと思いますが、概念的な理解の足掛かりにしていただけると嬉しいです☺️
サンプルコード: https://github.com/karamaru-alpha/layered-arch-sample
レイヤードアーキテクチャって何?
概要
ソフトウェアの関心事をレイヤー毎に分離するための設計方法の一つです。
他にもクリーンアーキテクチャ・オニオンアーキテクチャなどが有名ですが、どれも責務・依存関係を明確化するというゴールは変わりません。
メリット
アーキテクチャを導入する主なメリットは以下の二つです。
- DBやUI・ビジネスロジックなど、仕様変更の際にコードの修正箇所が明確・軽微
- 各レイヤーにおいてテストする目的が明確・処理をモックしやすいことからテストしやすい
仕様変更に強くテストもしやすいことから、保守運用しやすいということで人気なんですね👀
レイヤー解説
※矢印は依存方向です。依存とは、関数や変数を参照することを指します。
レイヤードアーキテクチャの基本はこの図です。
純正レイヤードアーキテクチャではdomain->infrastructure層
という依存関係でしたが、「ビジネスの中核をなすdomainが、使用DB(infra)などに依存する・影響されるっておかしくない?」と言う事で、domainが依存の最高位に君臨しています。これを**依存関係逆転の原則(DIP)**っていうみたいです👀
domain層: ビジネスルールの中核の定義を担当
infrastructure層: DB通信・DBデータ更新を担当
usecase層: アプリケーション固有のビジネスルールを担う処理を担当
interface層: ユーザーからのリクエスト受け取りや、表示に関することを担当
大まかにはこんな役割です。このままではわかりづらいので以下で詳しく説明します!
domain層
ここでは「ドメインモデル」と「リポジトリ」を定義します。
ドメインモデル
一言でいうとビジネスルールの中核・対象の存在です。
ex)「物を売り買いする」というビジネスルールでは、「ユーザー」「商品」などがドメインモデルとして定義される。
SQLのテーブルとよく似ていますが、そのビジネスルールで使用しないデータの場合はdomainとして扱わないという点で異なります。
ex) SQLには解析用にcreated_atが保存されているが、サービス上は使用しないためドメインモデルには持たせないなど。
以下、ユーザーのドメインモデルの例です。
package user
type User struct {
ID int32
Name string
}
リポジトリ
ドメインモデルになんらかの変更をする処理を定義する所です。
注意したいのは、ここで定義されているモノは「ドメインモデルこうやって変更するよ!」と抽象的に宣言しているだけで、具体的な処理を記述したものではないという事です。「ドメイン層は、他の仕様変更(使用DBやUI)に影響されない(依存しない)」という思想の元、リポジトリには具体的な処理を記述せず、抽象的な宣言に止まります。
具体的には、Interfaceを用いて抽象的なドメインの更新処理を表現しています。
ここら辺難しいですよねorz
イメージは、リポジトリという上司が「ドメインモデルに対してこんなことしたい!」と算段なく抽象的言っている感じで、DB更新などの具体的な処理はinfra層などの部下がリポジトリの意図(Interface)を汲み取って行います。
以下、リポジトリの例です。ユーザーというドメインモデルに対して、「こんな引数・戻り値で名前を更新するぞ!」という抽象的な宣言がなされています。
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から渡しています。)
package server
import (
ur "path/to/infrastructure/repositoryimpl/user"
)
...
// mysql.Conn = sqlへのコネクション。sql.Open("mysql"...
ur.NewRepositoryImpl(mysql.Conn)
...
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を(リポジトリを介して間接的に)叩く。
package server
import (
ur "path/to/infrastructure/repositoryimpl/user",
uu "path/to/usecase/user"
)
...
userRepoImpl := ur.NewRepositoryImpl(mysql.Conn)
uu.NewUseCase(userRepoImpl)
...
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に渡す(呼び出す)。
- 返り値を整形してレスポンス
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())))
...サーバー起動処理...
}
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"})
}
まとめ
以上がレイヤードアーキテクチャの構成例解説です。
稚拙な文章を最後まで読んでくださりありがとうございました。
ご指摘・ご感想あればコメントか、twitterにまで連絡していただけると幸いですmm
参考にした記事は以下ですmm
①今すぐ「レイヤードアーキテクチャ+DDD」を理解しよう。(golang)
②【Golang + レイヤードアーキテクチャ】DDD を意識して Web API を実装してみる
それではみなさん、楽しい開発ライフを!!