こんにちは、@tkitsunaiです。
この記事を Replaceプロジェクトを終えて総括 Part0 - 89tech blog のPart1扱いにしちゃおうかと思ってます。
上記の記事にもあるようにAPIサーバをゼロから作る機会があり、APIサーバはFull Golangで書いたのでPackage構成などについて今後のためにも書き残しておこうと思います。
全体の実装パターンはClean Architectureを実装パターンとして採用しましたので、ある程度各layerを意識した名前にしています。
そして出来上がった構成はこんな感じです。
. ├── adapter │ ├── controller │ ├── gateway │ ├── port │ └── out ├── config ├── core ├── domain │ ├── domain_name_a │ ├── domain_name_b │ └── ddd.go ├── infrastructure │ ├── rest │ └── driver ├── port │ └── out ├── repository ├── testutil └── usecase └── interactor
それぞれをClean Architectureでの4layerにわけてどんなものを置いているのか解説していきます。
Frameworks & Drivers layer
. ├── infrastructure │ ├── rest │ └── driver
RESTのためのHandler定義や、DB接続のためのDriverが置いてあります。frameworkにgin-gonicを採用したので、ginに依存したhandlerはここに置くことでそれ以下のlayerにginのルールを持ち込まず独立できています。
細かいhttp clientを拡張したものなどはpackageを作るか迷った挙句、infrastructure直下にgoファイルを置くような構成をとりました。
Adapter Interface / Usecase, middle layer
. ├── port │ └── out ├── repository
Usecase layerとAdapter layerはDIPによって抽象化されているrepositoryやportに依存するため、interface定義をここに別packageとして置いています。
最初はusecase packageに入れていましたが、Adapter layerが参照しやすいように別に切り出すことにしました。usecaseやadapterのことも考えるinterfaceになるので実際に書くときはここからコーディングしていくことが多いです。
Adapter Interface layer
. ├── adapter │ ├── controller │ ├── gateway │ ├── port │ └── out
adapter内は何度も構成を変更しており、まだまだ試行錯誤中です。 controllerはhandlerから受け取るパラメタやRequestBodyをdomain layerに定義しているValue objectやEntityに変換する処理を担うコードを置いています。したがって、domainオブジェクトに変換すると同時にパラメタのValidationの役割も担います。
DIPによって抽象化されているrepositoryに依存しているため、gateway内にはrepositoryの実装コードを配置してます。
前は、gateway/persistence/rdb
やgateway/persistence/cache
などを作っていましたが、全体の見通しが悪くなりつつあったので一旦フラットに置いてみて不都合が出てきたら変えようとしています。
portはpresenter的な役割を担うコードが中心に配置されてます。現時点ではinput-portは過剰だと考えoutput-portの実装しか置いていません。このoutput-portでdomainをjson変換するようなconverterの実装やメール通知やSlack通知をするための実装をしてます。
Usecase layer
. └── usecase └── interactor
ここでもDIPによって抽象化されているrepositoryに依存させます。 主にControllerから呼ばれるUsecaseのInteractor実装をここに書いています。DDDの文脈でよくあるApplicationやServiceなどの名前があまり好きじゃないのでInteractorという名前は結構気に入っています。
ここでのコードをrepositoryとセットで最初に書くことが多いのと、ほぼ確実にinterfaceへの依存しかないのでテストもMockしやすくて一番好きなlayerです。ただそのときそのときのDDDやClean Architectureの理解度で書いていたこともあり、油断するとdomain logicがすぐにこのlayerに漏れ出ていることが多く、一番好きなlayerだけど何度も苦汁を飲まされているlayerです。
Entities(domain) layer
いつもdomain layerって呼んでいます。
. ├── domain │ ├── domain_name_a │ ├── domain_name_b │ └── ddd.go
最初はdomain直下にdomain modelをそのまま置いていたのですが、集約の分け方やコンテキストによって変化する名前をつけるのにgolangだと単語_単語.go
が一般的な命名規則になるので見通しが悪い状態になったりしたので、小さなbounded context単位でpackageを作るように階層を増やしました。
また、チーム内でdomainへの理解度に差があったりしたので、小手先ですがddd.goで簡単にentity, aggregate, valueobject なのかを識別させるように以下のようなコードを用意して自分が何を作っているのかの道しるべの手助けをしています。
type Entity interface {} type Aggregation interface {} type ValueObject interface {}
なんの実装も持たないinterfaceですが、使い方的には例としてuser
というentity modelがあった場合には、interfaceの埋め込みして使います。
type User struct { domain.Entity id Identifier name Name }
最後に
Goでのプロジェクトを紹介するサンプルプロジェクトを参考にしながら、全体的にパッケージとして成熟させて今の形にまずは落ち着いています。今のところ大きな問題は起こってないはず...
まだまだ試行錯誤の段階で、パッケージ構成については正解があるものではないと思っています。ぜひTwitterなどで意見お待ちしております。