概要
弊社で開発中のプロダクトは、ドメイン駆動設計とクリーンアーキテクチャを組み合わせた設計となっています。
これまでフロントエンドの開発を担当することが多かった中で、golangでのAPI開発を担当した際に、「この処理はRepositoryに書くべき?」「Usecaseにこの処理を書いたら責務を持たせすぎ?」「Domain ServiceとUsecaseはどう分ける?」など迷うことが多々ありました。
そこで、これら設計に関する考え方を整理するために具体的な実装を踏まえてこの記事を書いています。
この記事は第二回です。第一回(記事はこちら)ではドメイン層・ユースケース層について触れています。
DDD(ドメイン駆動設計)とは
ドメインの専門家からの入力に従ってドメインに一致するようにソフトウェアをモデル化することに焦点を当てるソフトウェア設計手法である。
ドメイン駆動設計
下の画像のように4つの層に責務を分けて実装を進めます。
[新卒にも伝わるドメイン駆動設計のアーキテクチャ説明(オニオンアーキテクチャ)[DDD]](https://little-hands.hatenablog.com/entry/2018/12/10/ddd-architecture)
クリーンアーキテクチャとは
ソフトウェアを層に分けることで依存関係を分離し、高品質なシステムを構築する方法の一つです。
DBやフレークワークからの独立性を確保できるなどのメリットがあります。

Infrastructure層
この層の責務
ざっくりDDD・クリーンアーキテクチャにおける各層の責務を理解したい①(Repository)
// 前回定義したUserRepositoryインターフェース
type UserRepository interface {
Search(params SearchParams) ([]User, error)
Save(id int, name string, address Adress, mail string, passWordHash string, roleId int) (*User, error)
Update(id int, name string, address Adress, mail string, passWordHash string, roleId int) (*User, error)
Delete(userId number) error
}
上記で定義したRepositoryインターフェースの、DB個別の振る舞いを実装します。
下図のServiceをUsecaseに読み換えてください。
UsecaseはRepositoryに依存し、その実装は今から実装するRepositoryImplで行います。
DB個別の振る舞い
例えば、現在SQLiteを使用している場合、下記のような実装になります。
package sqlite_repository
func NewUserRepository(deps *SqliteDependency) userRepositoryImpl {
err := validator.New().Struct(deps)
if err != nil {
log.Panic(err)
}
impl := userRepositoryImpl{
deps,
}
return &impl
}
type userRepositoryImpl struct {
*SqliteDependency
}
func (i *userRepositoryImpl)Search(params SearchParams) ([]User, error) {
// SQLite用の実装
}
func (i *userRepositoryImpl)Save(id int, name string, address Adress, mail string, passWordHash string, roleId int) (*User, error) {
// SQLite用の実装
}
func (i *userRepositoryImpl)Update(id int, name string, address Adress, mail string, passWordHash string, roleId int) (*User, error) {
// SQLite用の実装
}
func (i *userRepositoryImpl)Delete(userId number) error {
// SQLite用の実装
}
上記ソースの中のSqliteDependencyは、SQLite独自の情報が含まれる実装となります。
簡単な例としては、DB接続情報等です。
package db
type DBDependency interface {
OpenDB() *DB
}
package sqlite_repository
// DBDependency インターフェースを実装する構造体
type SQLiteDependency struct {
db *sql.DB
}
func (d *SQLiteDependency)OpenDB() *DB {
// SQLiteデータベースファイルのオープン
db, err := sql.Open("sqlite3", "test.db")
if err != nil {
log.Fatal(err)
}
return db
}
次に、mysql用の実装をしてみます。
package mysql_repository
func NewUserRepository(deps *MysqlDependency) userRepositoryImpl {
err := validator.New().Struct(deps)
if err != nil {
log.Panic(err)
}
impl := userRepositoryImpl{
deps,
}
return &impl
}
type userRepositoryImpl struct {
*MysqlDependency
}
func (i *userRepositoryImpl)Search(params SearchParams) ([]User, error) {
// Mysql用の実装
}
func (i *userRepositoryImpl)Save(id int, name string, address Adress, mail string, passWordHash string, roleId int) (*User, error) {
// Mysql用の実装
}
func (i *userRepositoryImpl)Update(id int, name string, address Adress, mail string, passWordHash string, roleId int) (*User, error) {
// Mysql用の実装
}
func (i *userRepositoryImpl)Delete(userId number) error {
// Mysql用の実装
}
package mysql_repository
// DBDependency インターフェースを実装する構造体
type MysqlDependency struct {}
func (d *MysqlDependency)OpenDB() *DB {
// mysqlへの接続
db, err := sql.Open("mysql", "test_user:test_password@tcp(127.0.0.1:3306)/test_db")
if err != nil {
log.Fatal(err)
}
return db
}
さて、実装したRepositoryImplを用いて、Usecaseをそれぞれ初期化してみます。
func initUserUsecase() *UserServiceImpl {
err := godotenv.Load(".env")
if err != nil {
fmt.Printf("faild open envfile: %v", err)
}
dbName := os.Getenv("DB_NAME") // .envファイルに定義したDB_NAME環境変数の読み込み
var iUserRepo userRepositoryImpl
if dbName == "sqlite" {
sqliteDeps := new(sqlite_repository.SqliteDependency)
iUserRepo = NewUserRepository(sqliteDeps)
}
if dbName == "mysql" {
mysqlDeps := new(mysql_repository.MysqlDependency)
iUserRepo = NewUserRepository(mysqlDeps)
}
userUsecase := NewUserUsecaseImpl(&usecase.UserUsecaseDeps{
UserRepository iUserRepo
})
return userUsecase
}
Usecaseは、RepositoryImplではなく、Repositoryインターフェースに依存しています。
そのため、Usecaseの実装を変更することなく、環境変数を修正するだけでDBの切り替えを簡単に行うことが可能となります。
Presentation層
この層の責務
ユーザーの入力とUsecaseまでの橋渡し。リクエストを受け付けたりレスポンスを返すなど、アプリと外部とのやりとりを担う。
リクエストパラメータとUsecaseへ渡す値のマッピング
APIのリクエストパラメータとして定義されている型と、Usecaseで使用する型が異なる場合、そのマッピングを行います。
入力値バリデーション
フロントエンド側でバリデーションを実装している場合でも、
API側でバリデーションを行うことで不正な値の流入を防ぐことができるため、より安全となります。
また、フロントのバリデーションはdevtool等を用いて無理やり突破される恐れがあるので、それをAPI側で防ぐことが必要です。
上記2点はController(Handler)として実装します。
type UserController interface {
Search(c echo.Context) error
Save(c echo.Context) error
Update(c echo.Context) error
Delete(c echo.Context) error
}
type UserController struct {
*KaraokeHandlerDeps
}
func NewUserController(deps UserControllerDeps) UserController {
v := validator.New()
err := v.Struct(deps)
if err != nil {
log.Error(err)
panic(err)
}
impl := UserController{
&deps,
}
return &impl
}
type UserControllerDeps struct {
UserUsecase usecases.UserUsecase `validate:"required"` // Usecaseへの依存
}
func (u *UserController) Save(c echo.Context) error {
// リクエストパラメータのバインド
requestParams := new(UserSaveRequestParams)
err := c.Bind(requestParams)
if err != nil {
log.Error("couldn't bind request parameters to struct, ", err)
return c.Render(http.StatusInternalServerError, "error.html", echo.Map{
"ErrorMessage": "Internal Server Error",
})
}
// 入力値のバリデーション
isValid := validation_service.ValidateUserInput(requestParams)
if !isValid {
log.Error("invalid request parameters")
return c.Render(http.StatusInternalServerError, "error.html", echo.Map{
"ErrorMessage": "Internal Server Error",
})
}
// Usecaseで使用する型へのマッピング
userParams := conv.ConvertInputToUserParams(*requestParams)
savedUser, err := self.UserUsecase.Save(userParams)
if err != nil {
log.Error("couldn't save user")
return c.JSON(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, conv.ConvertSaveUserResult(*savedUser))
}
// 以下省略
エンドポイント
フロントエンドから送信されたリクエストを、各処理にルーティングするための定義を行います。
以下はgolangのフレームワークechoの例です。
e.GET("/search/users", userController.Search)
e.POST("/user", userController.Save)
e.PUT("/user", userController.Update)
e.DELETE("/user", userController.Delete)
まとめ
以上が、インフラストラクチャ層とプレゼンテーション層です。比較的こちらの実装は単純でしたね。
DDD、クリーンアーキテクチャは前回と今回で説明した以外にも、多くの要素があるため今後も知識の幅を広げていきたいと思います。