概要
弊社で開発中のプロダクトは、ドメイン駆動設計とクリーンアーキテクチャを組み合わせた設計となっています。
これまでフロントエンドの開発を担当することが多かった中で、golangでのAPI開発を担当した際に、「この処理はRepositoryに書くべき?」「Usecaseにこの処理を書いたら責務を持たせすぎ?」「Domain ServiceとUsecaseはどう分ける?」など迷うことが多々ありました。
そこで、これら設計に関する考え方を整理するために具体的な実装を踏まえてこの記事を書いています。
この後、インフラストラクチャ層・プレゼンテーション層は別の記事として書く予定です。
DDD(ドメイン駆動設計)とは
ドメインの専門家からの入力に従ってドメインに一致するようにソフトウェアをモデル化することに焦点を当てるソフトウェア設計手法である。
ドメイン駆動設計
下の画像のように4つの層に責務を分けて実装を進めます。
[新卒にも伝わるドメイン駆動設計のアーキテクチャ説明(オニオンアーキテクチャ)[DDD]](https://little-hands.hatenablog.com/entry/2018/12/10/ddd-architecture)
クリーンアーキテクチャとは
ソフトウェアを層に分けることで依存関係を分離し、高品質なシステムを構築する方法の一つです。
DBやフレークワークからの独立性を確保できるなどのメリットがあります。
参考
この資料が非常に分かり易かったため参考にしています。
ドメイン層
この層の責務
後述するUsecase(Application)層に対して提供する、業務ロジックを実装するための層です。
以下の要素が含まれます。
Entity(およびValue Object)
ソフトウェアによって解決したい対象領域(課題)をモデリングし、コードに落とし込んだもの。
Entityは一意な識別子を持ち、変更される場合があります。
一方でValue Objectは不変であり、識別子を持ちません。
不変であるので、外部からフィールドを変更できないよう定義し、コンストラクタで初期化します。
// 例・・・ユーザー Entity
type User struct {
Id int
Name string
Address Adress
Mail string
password string
roleId int
UserService *UserService // 後述するDomain Service(業務ロジック)への依存
}
// 例・・・住所 Value Object
type Address struct {
postCode string // 郵便番号
prefecture string // 都道府県
municipality string // 市町村
addressNumber string // 番地
}
func NewAddress(postCode string, prefecture string, municipality string, addressNumber string) (Address, error) {
address := new(Address)
// 誤った値で初期化しないようバリデーション(省略)
address.postCode = postCode
address.prefecture = prefecture
address.municipality = municipality
address.addressNumber = addressNumber
return address, nil
}
Repository
後述するUsecaseに対して、Entityのライフサイクルを制御するための操作(Repositoryインタフェース)を提供。具体的には、Entityオブジェクトに対するCRUD操作を担当します。
下図のServiceをUsecaseに読み換えると分かりやすいです。
RepositoryインターフェースはDBの種類に依存しません。
DBの種類に依存した個別の実装は、インフラストラクチャ層にてRepositoryインターフェースを実装したRepositoryImplで行います。
// 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
}
Domain Service
ドメイン層における特定のドメイン関連のロジックをカプセル化するために使用されます。
Userの例としては
- パスワードのハッシュ化
- roleの確認
等が挙げられます。
type UserService struct {
// UserServiceに関連するフィールドや依存性
}
func (us *UserService) VerifyUserCredentials(username, password string) bool {
// ユーザーの認証を行うロジック
return true // 仮の結果
}
func (us *UserService) CheckIsBetaUser(roleId int) bool {
// ユーザーがベータユーザーのロールを持つかをチェックするロジック
return true // 仮の結果
}
// User Entity
type User struct {
Id int
Name string
Address Adress
Mail string
password string
roleId int
UserService *UserService // Domain Service(業務ロジック)への依存
}
func (u *User) Authenticate() bool {
// ドメインサービスを利用して認証を行う
return u.UserService.VerifyUserCredentials(u.Name, u.Password)
}
func (u *User) CheckIsBeta() bool {
// ドメインサービスを利用してベータユーザーか判定する
return u.UserService.CheckIsBetaUser(u.roleId)
}
Usecase(Application)層
この層の責務
ユーザーの操作に対してユースケースを実現すること。
Presentation層のController(Handler)から依存され、機能を提供すること。
Domain Serviceとの違い
ドメインサービスは特定の業務ロジックをカプセル化し、独立した操作を提供します。
ユースケースはアプリケーションの具体的な操作を表現し、ドメインモデルやサービスを使用してユーザーやシステムの操作を実現します。
Userというドメインで具体例を挙げると、
- Domain Service
- Userが持つフィールドを操作・変換したり、フィールドを用いて何かしらの判定を行う
- Usecase
- ユーザー作成・パスワード更新・ユーザー検索・ユーザー削除のようなアプリケーションの操作に対する処理
のような違いがあります。
また、UsecaseはRepository interfaceに依存します。 インターフェースに依存させることで、DI(依存性の注入)を利用してDBの切り替えが容易になったり単体テストを可能とすることができるからです。
下記の説明および図のServiceをUsecaseに、ServiceImplをUsecaseImplに読み替えるとイメージしやすいです。
業務ロジックは、アプリケーションで使用する業務データの参照、更新、整合性チェックおよびビジネスルールに関わる各種処理で構成される。
業務データの参照および更新処理をRepository(またはO/R Mapper)に委譲し、Serviceではビジネスルールに関わる処理の実装に専念することを推奨する。
したがって、今回は先ほど実装したUserRepositoryの持つCRUDに関わるメソッドをUserUsecaseImplから呼び出すことになります。
type UserParams struct {
id int
name string
address Adress
mail string
passWordHash string
roleId int
}
type UserUsecase interface {
Search(params SearchParams) ([]User, error)
Save(params UserParams) (*User, error)
Update(params UserParams) (*User, error)
Delete(userId number) error
}
type UserUsecaseDeps struct {
UserRepository repository.UserRepository // Repositoryインターフェースへ依存させる
}
func NewUserUsecaseImpl(deps UserUsecaseDeps) *UserServiceImpl {
return &UserUsecaseImpl{
UserUsecaseDeps
}
}
func (usi *UserServiceImpl) Search(params SearchParams) ([]User, error){
// 何かしらの処理
users, err := usi.UserRepository.Search(params SearchParams)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return nil, err
}
// 何かしらの処理
return users, nil
}
// Save、Update、Deleteメソッドは省略
まとめ
以上が、Domain層とUsecase層です。
次回はPresentation層、Infrastructureについてまとめます。