GolangでClean Architectureの実装に挑戦したみたので整理しておく。
内容は概ねスライドの内容を踏襲している。
理解しきれていないところがあったり、自分の解釈、考えを記述しているので、正しくない部分もあるかもしれない。
LTをする機会があったのでスライドを貼っておく。
Dive to clean architecture with golang
ソースはこれ。
bmf-san/go-clean-architecture-web-application-boilerplate
MVCのパターンでの実装もtagを切って残してある。
github - bmf-san/Rubelという今はメンテはしていないが、このブログを運用しているCMSアプリケーションがある。
このアプリケーションをリプレースすべく、選定した言語がgoで、アーキテクチャも見直そうということでClean Architectureを採用する方針となった。
なぜClean Architectureを採用しようと考えたかというと、個人で長い付き合いをしていくことのできるアプリケーションのアーキテクチャとして、
ライブラリやその他技術に依存しないようなアーキテクチャパターンが最適解なのではないかと考えたからである。
RubelはLaravelやReactといったフレームワークを採用しているが、フレームワークにどっしりと乗っかる形で実装してしまっているため、比較的モダンで変化(バージョンアップ)の早いそれらのフレームワークのバージョンアップに追随していく時間が惜しく感じた。
本来、CMSの機能追加や機能改善に力を注ぎたいはずが、今後長く運用していていきたいアプリケーションで、本質的ではない部分の開発に時間を注ぐのは合理的だと考えることができなかった。
フレームワークやライブラリ、その他技術への依存を極力減らして、goの標準ライブラリを十分に使いつつ、開発していくことができれば、保守性の高いアプリケーションがつくれるのではないかと考えた。
スクラッチこそ大正義みたいな気概を持っていたりするが、サービス開発のようなビジネス要件に即対応が求められるようなそういったアプリケーションではないのと、開発目的が学習要素を含む部分もあるのである程度理にかなっているとは思う。
自分の目的としては個人開発で取るべき最適に近い戦略が取れているような気はしているが、運用フェーズに乗っかってからでないと見えないところはまぁあるだろうと思っている。
現在鋭意開発中のRubelリプレースはこちら。
Clean Architectureの考えが生まれる前まで、過去いくつかのアーキテクチャのアイデアが存在していた。
これらのアイデアは共通して「関心事の分離」という目的を持っていて、
といったあらゆるものへの依存性脱却とテスタビリティを追求していく。
Clean Architectureで調べるとよく見るあの図はこちらの元ネタを参照。
cleancoder.com - The Clean Architecture
各レイヤーについて説明していく。
上記のレイヤー間の制約について。
冒頭で紹介したソースと同じだが再掲。
bmf-san/go-clean-architecture-web-application-boilerplate
./app/
├── database
│ ├── migrations
│ │ └── schema.sql
│ └── seeds
│ └── faker.sql
├── domain
│ ├── post.go
│ └── user.go
├── go_clean_architecture_web_application_boilerplate
├── infrastructure
│ ├── env.go
│ ├── logger.go
│ ├── router.go
│ └── sqlhandler.go
├── interfaces
│ ├── post_controller.go
│ ├── post_repository.go
│ ├── sqlhandler.go
│ ├── user_controller.go
│ └── user_repository.go
├── log
│ ├── access.log
│ └── error.log
├── main.go
└── usecases
├── logger.go
├── post_interactor.go
├── post_repository.go
├── user_interactor.go
└── user_repository.go
8 directories, 22 files
レイヤーとディレクトリの対応は以下ような形になっている。
Layer | Directory |
---|---|
Frameworks & Drivers | infrastructure |
Interface | interfaces |
Usecases | usecases |
Entities | domain |
Clean Architectureを実装する前にDIP(依存関係逆転の原則)というルールを知っておく必要がある。
SOLID原則の一つで、抽象は詳細に依存すべきではないというモジュール間の制約についてルールである。
このルールの詳細については割愛するが、Clean Architectureの文脈では、このルールは依存方向を外側から内側に保つため、インターフェースを活用することでDIPを守り、レイヤー間の制約も守る。
愚直に各レイヤーのルールに従って実装すると依存方向が内側から外側に向いてしまうような事態が発生する。
その事態のときにインターフェースを定義し、抽象への依存をすることで依存方向を守っていく、というのは実装の肝になる部分である。
Golangには「インターフェースを受け入れて、構造体を返す」という考え方がある。
これはDIPの実装に親和性がある考え方だと思う。
package examples
// Logger is an interface which will be used for an argument of a function.
type Logger interface {
Printf(string, ...interface{})
}
// FooController is a struct which will be returned by function.
type FooController struct {
Logger Logger
}
// NewFooController is a function for an example, "Accept interfaces, return structs".
// Also, this style of a function take on a role of constructor for struct.
func NewFooController(logger Logger) *FooController {
return &FooController{
Logger: logger,
}
}
Golangではよく見かける基本的な実装パターンかと思う。
インターフェースに依存させることで変更に強く、テストの書きやすいコードを書くことができる(はず)
GolangでのDIP例。
DIPではないコード。
package examples
// sqlHandler is a struct for handling sql.
type sqlHandler struct{}
// Execute is a function for executing sql.
func (sqlHandler *sqlHandler) Execute() {
// do something...
}
// FooRepository is a struct depending on details.
type FooRepository struct {
sqlHandler sqlHandler
}
// Find is a method depending on details.
func (ur *FooRepository) Find() {
// do something
ur.sqlHandler.Execute()
}
DIPを考慮したコード。
package examples
// SQLHandler is an interface for handling sql.
type SQLHandler interface {
Execute()
}
// sqlHandler is a struct which will be returned by function.
type sqlHandler struct{}
// NewSQLHandler is a function for an example of DIP.
// This function depend on abstruction(interface).
// This pattern is an idiom of constructor in golang.
// You can do DI(Dependency Injection) by using nested struct.
func NewSQLHandler() SQLHandler {
// do something ...
// sqlHandler struct implments SQLHandler interface.
return &sqlHandler{}
}
// Execute is a function for executing sql.
// A sqlHanlder struct implments a SQLHandler interface by defining Execute().
func (s *sqlHandler) Execute() {
// do something...
}
// FooRepository is a struct depending on an interface.
type FooRepository struct {
SQLHandler SQLHandler
}
// Find is a method of FooRepository depending on an interface.
func (ur *FooRepository) Find() {
// do something
ur.SQLHandler.Execute()
}
インターフェースを間に挟むことで依存関係が変化し、結果として依存の方向性が逆転するような形になっている。
Before
SQLHandler
↑
FooRepository
After
SQLHandler
↓
SQLHandler Interface
↑
FooRepository
Clean Architectureの実例では、infrastructureとinterfacesのコードがそれに当たる。
bmf-san/go-clean-architecture-web-application-boilerplate
実際にClean Architectureに取り組む際はいきなり実装しないでまずはコードリーディングや写経から入ると実装を理解しやすいのではないかと思う。
コードリーディングする際は自分は外側から内側に向かってコードを読んでいくのがわかりやすいと感じた。
main.go
↓
router.go・・・Infrastructure
↓
user_controller.go・・・Interfaces
↓
user_interactor.go・・・Use Cases
↓
user_repository.go・・・Use Cases
↓
user.go・・・Domain
関連書籍