はじめに
DDDやクリーンアーキテクチャについて備忘録がてらまとめます。
明らかな誤りがあったらメッしてください。(お手柔らかに)
DDDについて
まずはDDD(ドメイン駆動設計)について記載します。
ざっくりとしたDDDの理解は下記の通りです。
- エンジニアも事業に対して関心を持つべし
- 事業ドメインを定義して(ここが難しい)ドメインモデルを設計する
ドメインモデルってORMのモデルと違うの?という数年前の私に向けて言うのであれば、「違います」が回答となります。
ドメインモデルはデータベースの構造とは関係なく、「データベースからデータの取得→ドメインモデルに変換」をすることになります。
クリーンアーキテクチャ
クリーンアーキテクチャはビジネスルール(ユースケース)を中心として、データベースやフレームワーク、UIは外側に配置します。
クリーンアーキテクチャはDDDを実現するために利用されることもありますが、必ずしもDDDに限定されるものではありません。
- DDD (ドメイン駆動設計) は、ソフトウェアの設計思想であり、複雑なビジネスルールを整理するための方法論
- クリーンアーキテクチャ は、アーキテクチャの設計原則であり、層の分離や依存関係の制御を目的とする
類似のアーキテクチャとの違い
クリーンアーキテクチャだけでなく、他にもオニオンアーキテクチャやヘキサゴナルアーキテクチャ(ポート&アダプタ)が有名です。
違いとして、層(レイヤー)を分けの仕方やどの層を中心とするかはアーキテクチャごとに異なります。
しかし、下記は共通しています。
- 依存性の注入(Dependency Injection)を使って疎結合にする
- 依存性逆転の法則(Dependency Inversion Principle)を使って、外側の層から内側の層へ依存させる
依存性の注入(Dependency Injection)
では、依存性の注入とはなんでしょうか。
ここで簡単な例を出します。
下記のnotifier
はフィールドとしてMessageSender
を保持しています。
Notify
が呼ばれた時にMessageSender.SendMessage
を実行します。
type Notifier struct {
sender MessageSender
}
func (n *Notifier) Notify(message string) {
emailSender := &EmailSender{}
notifier := Notifier{sender: emailSender}
notifier.Notify("Hello Email!")
}
このMessageSender
の生成処理をNotifier
で行うこともできますが、密結合となってしまいます。
そのため、Notifier
を使う時にMessageSender
を外部から渡します。
func main() {
// 依存性の注入
notifier := Notifier{sender: MessageSender{})
}
外部からMessageSender
を渡すことで、MessageSender
が切り替えやすくなりモックを使用したテストの記載がしやすいなどのメリットがあります。
その他にもMessageSender
をEmailとSMSで切り替えることも可能です。
type MessageSender interface {
SendMessage(message string)
}
// EmailSender
type EmailSender struct{}
func (e *EmailSender) SendMessage(message string) {
// Email送信処理
}
// SMS Sender
type SMSSender struct{}
func (s *SMSSender) SendMessage(message string) {
// SMS送信処理
}
func main() {
emailNotifier := Notifier{sender: &EmailSender{}}
smsNotifier := Notifier{sender: &SMSSender{}}
emailNotifier.Notify("Hello Email!")
smsNotifier.Notify("Hello SMS!")
}
依存性の注入をすることで疎結合になります。
依存性逆転の法則(Dependency Inversion Principle)
では、依存性逆転の法則とはなんでしょうか。
話を単純にするため、登場するのはプレゼンテーション層(Controller)、アプリケーション層(Usecase)、データ層(Repository)とします。
処理の流れとして、Controller→Usecase→Repositoryです。
詳細の実装は下記の通りです。
type IUserRepository interface {
GetUser(id int) string
}
type userRepository struct{}
func (r *userRepository) GetUser(id int) string {
// データ取得
return fmt.Sprintf("User%d", id)
}
func NewUserRepository() IUserRepository {
return &userRepository{}
}
type IUserUsecase interface {
GetUserName(id int) string
}
type userUsecase struct {
repo IUserRepository
}
func (u *userUsecase) GetUserName(id int) string {
return u.repo.GetUser(id)
}
func NewUserUsecase(repo IUserRepository) IUserUsecase {
return &userUsecase{repo: repo}
}
NewUserUsecase
では受け取ったUserRepository
を自身のフィールドとして保存して使います。
type UserController struct {
usecase IUserUsecase
}
func (c *UserController) GetUser(id int) {
name := c.usecase.GetUserName(id)
fmt.Println("User Name:", name)
}
func main() {
// 依存性の注入
repo := NewUserRepository()
usecase := NewUserUsecase(repo)
controller := UserController{usecase: usecase} // 直接構造体を生成
// 実行(本来はnet/http)
controller.GetUser(1)
}
main.go
で依存性の注入を実施します。
では、この実装のどのあたりが依存性の逆転なのでしょうか。
依存の方向は下記の図の通りになります。
依存性逆転の法則では、具体的な実装(クラス)ではなく、抽象(インターフェース)に依存することで、依存関係を制御するのが目的です。
例として、UserUsecase
がIUserRepository
に依存することでuserRepository
の具体的な実装に依存しません。
他にも、下記のようなメリットがあります。
- 疎結合
- 単体テストがしやすい
- 置き換えやすい
- 再利用しやすい
まとめ
このアーキテクチャの最大のメリットは、コードがスパゲッティのようになりにくいことだと思います。
中規模以上のシステムでは、ビジネスロジックの複雑性もそれなりにあり、人員もある程度必要となります。
その際に、決められたルールに従うことでクリーンな状態を保てます。
デメリットとしては、書くコードが多くなりがちな点です。
Interfaceを都度用意して、レイヤーを分けてと手間がかかります。
そのため、小規模なシステムや速度が優先される状況では向かないかもしれません。
参考書籍