Click here for English version
*追記:Student Goで発表しました。
クリーンアーキテクチャとは
以下を実現することで、関心の分離をするアーキテクチャパターンです。
詳しくは様々な記事で説明されているので、今エントリでは割愛し実装パターンに絞って紹介します。
サンプルアプリケーション
↓サンプルコード
仕様は、/users
にPOSTすることでユーザー登録するだけのapiです。
基本はmanuelkiessling/go-cleanarchitectureやhirotakan/go-cleanarchitecture-sampleを参考にしていますがこれらはDBアクセスに直接sqlドライバーを使用しているため、今回はORMを使用した実装パターンを作ってみました。ORMライブラリはgormを使用しています。
app設計概要
何層に分けるかは自由なのですが、原文に従って4層に分けました。
この時、外側から内側に向かって単一方向に依存することを徹底します。
ディレクトリ構成
├── adapter │ ├── controllers │ ├── gateway │ └── interfaces ├── domain ├── external │ └── mysql ├── main.go └── usecase └── interfaces
各ディレクトリは、それぞれ以下の層の役割をしています。
ディレクトリ | 層 |
---|---|
external | frameworks & drivers |
adapter | interface adapters |
usecase | app business rules |
domain | enterprise business rules |
依存関係の徹底(重要)
実装の説明に入る前に、大事なルールを一つ説明します。
前述したとおり、依存関係は外側から内側へ単一方向へ保つ必要があります。
しかしながらどんなプログラムにも入力と出力があり、内側で処理した結果を外側へ出力するということが頻繁に起こります。
つまり、内側から外側へ依存したい場面が必ず現れます。その矛盾を依存関係逆転の原則を用いて解決することが、クリーンアーキテクチャをクリーンに保つための鍵となります。
依存関係逆転の原則(DIP)とは
簡単に言うと、内側は外側に依存するのではなく、抽象に依存するべきであるという原則です。
よくわからないと思うので、実際にコードを見てみましょう。
func (i *UserInteractor) Add(u domain.User) (int, error) { return i.UserRepository.Store(u) }
これは、内から2番目の層、app business rules層
の実装です。
この層の外側から渡されたユーザデータを、外側にあるDBに保存しようとしています。
ここでやってしまいがちなのが、そのまま外側の層にアクセスしてしまうことです。
しかし外側に直接アクセスしてしまうと依存関係が内→外に向いてしまうので避けたいところです。
そこで依存関係逆転の原則を使います。Goではこれをinterface
を定義することで実現します。
type UserRepository interface { Store(domain.User) (int, error) FindByName(string) ([]domain.User, error) FindAll() ([]domain.User, error) }
同じ層にrepositoryインターフェースを定義し、このインターフェースに依存するようにします。
そして具象は外側で定義しておき、実行時に外側から渡してあげることで外→内の依存関係を保つことができます。
これが依存関係逆転の原則です。
ここを見ると分かりやすいかと思います。
サンプルコードでは、抽象に依存するためのinterface
を各層のinterfacesディレクトリにまとめてあります。
各層の実装
内側の2層はプロジェクトによって結構変わってくると思うので、ここでは外側の2層に絞って説明していきます。
Frameworks & Drivers
DBアクセス
以下のようなDB接続等の外部との仲介実装はこの層で完結するようにします。
var db *gorm.DB func Connect() *gorm.DB { var err error db, err = gorm.Open("mysql", "root:@tcp(db:3306)/hoge") if err != nil { panic(err) } db.Table("users").CreateTable(&gateway.User{}) return db } func CloseConn() { db.Close() }
ルーティング
http通信処理もこの層で完結します。今回はWAFにginを使用していますが、この層が独立しているので差し替えが容易になっています。
また、DBのコネクションやlogger等の具象型もここで内側の層に渡すようにしています。
こうすることで、前述した依存関係逆転の原則を実現しています。
var Router *gin.Engine func init() { router := gin.Default() logger := &Logger{} conn := mysql.Connect() userController := controllers.NewUserController(conn, logger) router.POST("/users", func(c *gin.Context) { userController.Create(c) }) Router = router }
Interface Adapters
ORMのマッパーもここで定義します。
ここでは、DB用に最適化された型をドメインロジック用に最適化された型に変換することで、インピーダンス・ミスマッチを解決することに徹しています。
adapter層は抽象に依存しているとはいえ、この層で定義するinterface
は外側のライブラリにある程度依存してしまいます。
原則では、
「抽象」は実装の詳細に依存してはならない
とされていますが、この層は変換が目的なので、ある程度どちらのことも知っているのが自然かと思います。
ここに関してもっと良い実装パターンをご存知の方がいらしたらご教授願いたいです。
type ( UserRepository struct { Conn *gorm.DB } User struct { gorm.Model Name string `gorm:"size:20;not null"` Email string `gorm:"size:100;not null"` Age int `gorm:"type:smallint"` } ) func (r *UserRepository) Store(u domain.User) (id int, err error) { user := &User{ Name: u.Name, Email: u.Email, } if err = r.Conn.Create(user).Error; err != nil { return } return int(user.ID), nil }
まとめ
見ての通り、user登録するだけのapiでもこんな大きなプロジェクトになってしまいます。
抽象化することで関心を分離することができますが、そこまでして分離する必要があるかはよく考える必要がありそうです。
基本的に抽象化するとコードは複雑化し、直感的ではなくなるので、アプリケーションの規模が小さい場合は効力を発揮しない場合が多いと思います。
何か間違いがあればご指摘頂けると大変助かります。