概要
手探りしながら、 Golang でクリーンアーキテクチャを実装しました。
今後のために残しておきます。こうしたほうがいいな、って思ったら適宜更新していきたい。
ツッコミや、個人的にはこう思う、みたいなの大歓迎です。
クリーンアーキテクチャとは
ここで新しく説明するよりも、このQiitaを読んだほうが良いので割愛します。
実践
パッケージ・ディレクトリ構造
肝は依存関係をコントロールすることにあるので、円毎にディレクトリを用意しました。
また、usecase は単独で切り出し、中心に考えます。
.
├── adapters # 緑色
├── app # 赤色
├── domain # 黄色
├── external # 青色
└── usecases # 赤色(定義)
これを元に、ファイルを順番に作っていきます。
ユースケースの抽出
今回はよく見かける「ユーザ登録」を例に考えていきます。
この時点では処理の内容を深堀りする必要はないので、大雑把なパラメタと名前だけ決めます。
ユースケース名 | 処理内容 |
---|---|
サインアップ | メールアドレスとパスワードを受け取ってユーザを登録する |
決まったら、usecases 配下に interface を配置します。
.
├── adapters
├── app
├── domain
├── external
└── usecases
└── sign_up_usecase.go
ソースコードはこちら。
UseCase の Interface と、Input / Output を定義しておきます。
package usecases
type ISignUpUseCase interface {
SignUp(input SignUpUseCaseInput) (output *SignUpUseCaseOutput, err error)
}
type SignUpUseCaseInput struct {
Email string
Password string
}
type SignUpUseCaseOutput struct {
}
UseCase内の処理を考える
サインアップ内で何をするか考えます。一旦はシンプルに以下とします。
- メールアドレスが他のユーザに登録されていないかチェックする
- ユーザを保存する
ドメインを定義する
~~ドメインがよくわかってないので、~~処理内容から、とりあえず重要そうなモノを取り出します。
-
ユーザ
を保存する
これをドメインとして定義します。
.
├── adapters
├── app
├── domain
│ └── user
│ └── user.go
├── external
└── usecases
└── sign_up_usecase.go
ドメインはこのアプリケーションにとって最重要なものであるので、パッケージを専用で切ってみます。
ソースコードはこちら。
package user
type User struct {
Email string
Password string
}
UseCaseの実装準備
UseCaseの実装をしていきます。赤色の実装は app の下ですね。 実装として、 Interactor を用意します。
.
├── adapters
├── app
│ └── interactors
│ └── sign_up_interactor.go
├── domain
│ └── user
│ └── user.go
├── external
└── usecases
└── sign_up_usecase.go
ソースコードはこちら。
package interactors
import (
"errors"
"github.com/sat8bit/golang-clean-architecture/usecases"
)
type SignUpInteractor struct {
}
func NewSignUpInteractor() SignUpInteractor {
return SignUpInteractor{}
}
func (i SignUpInteractor) SignUp(input usecases.SignUpUseCaseInput) (output *usecases.SignUpUseCaseOutput, err error) {
return nil, errors.New("not implemented")
}
NewSignUpInteractor()
の返却型は ISignUpUseCase
であるべきな気もしていて、今絶賛悩み中です。
現状は、ISignUpUseCase
の実装が SignUpInteractor
である、みたいな表現は DI 側で表現しています。
話ついでにDIも追加しておきます。
.
├── adapters
├── app
│ └── interactors
│ └── sign_up_interactor.go
├── di
│ └── di.go
├── domain
│ └── user
│ └── user.go
├── external
└── usecases
└── sign_up_usecase.go
ソースコードはこちら。
今の時点では Injection も何もありません。
package di
import (
"github.com/sat8bit/golang-clean-architecture/app/interactors"
"github.com/sat8bit/golang-clean-architecture/usecases"
)
func SignUpUseCase() usecases.ISignUpUseCase {
return interactors.NewSignUpInteractor()
}
動作させたい!
UseCase を実行するだけの main を作っておきましょう。
今回はmainを作りますが、実際はUseCaseのテストコードを書けばよいです。
.
├── adapters
├── app
│ └── interactors
│ └── sign_up_interactor.go
├── di
│ └── di.go
├── domain
│ └── user
│ └── user.go
├── external
├── main
│ └── main.go
└── usecases
└── sign_up_usecase.go
ソースコードはこちら。
既存登録チェックがあるので、2回サインアップしてみるコードにします。
package main
import (
"github.com/sat8bit/golang-clean-architecture/di"
"github.com/sat8bit/golang-clean-architecture/usecases"
"log"
)
func main() {
u := di.SignUpUseCase()
input := usecases.SignUpUseCaseInput{Email: "example@mail.address.com", Password: "abcd1234"}
if _, err := u.SignUp(input); err != nil {
log.Printf("Error is %s", err)
} else {
log.Print("SignUp succeeded.")
}
if _, err := u.SignUp(input); err != nil {
log.Printf("Error is %s", err)
} else {
log.Print("SignUp succeeded.")
}
}
実行してみます。
sat8bit $ go run main/main.go
2019/09/13 21:58:15 Error is not implemented
2019/09/13 21:58:15 Error is not implemented
SignUpInteractor の実装
実装を予定している2つの処理は、何れも永続化データの操作になるので、UserRepository の準備を進めます。
UserRepository は緑の Gateway に位置するのですが、依存関係を緑→赤に保つために、まずは定義を app の内部で完結させます。
.
├── adapters
├── app
│ ├── gateways
│ │ └── user_repository.go
│ └── interactors
│ └── sign_up_interactor.go
├── di
│ └── di.go
├── domain
│ └── user
│ └── user.go
├── external
├── main
│ └── main.go
└── usecases
└── sign_up_usecase.go
ソースコードはこちら。
気をつけたいのは、UseCaseから見たときにどう使いたいかを定義します。
package gateways
import (
"github.com/sat8bit/golang-clean-architecture/domain/user"
)
type IUserRepository interface {
// Email からユーザを検索したい
FindByEmail(email string) (user *user.User, err error)
// User を保存したい
Save(user user.User) error
}
Interface が定義できたので、 SignUpInteractor に依存と実装を追加します。
ソースコードはこちら。
package interactors
import (
"errors"
"github.com/sat8bit/golang-clean-architecture/app/gateways"
"github.com/sat8bit/golang-clean-architecture/domain/user"
"github.com/sat8bit/golang-clean-architecture/usecases"
)
type SignUpInteractor struct {
userRepository gateways.IUserRepository
}
func NewSignUpInteractor(
userRepository gateways.IUserRepository,
) SignUpInteractor {
return SignUpInteractor{
userRepository: userRepository,
}
}
func (i SignUpInteractor) SignUp(input usecases.SignUpUseCaseInput) (output *usecases.SignUpUseCaseOutput, err error) {
if user, _ := i.userRepository.FindByEmail(input.Email); user != nil {
return nil, errors.New("email already exists")
}
user := user.User{
Email: input.Email,
Password: input.Password,
}
if err := i.userRepository.Save(user); err != nil {
return nil, err
}
return &usecases.SignUpUseCaseOutput{}, nil
}
これで、赤の円まで完成です。
SignUpInteractor
が、 app 配下と domain 配下にしか依存してないことがポイントです。
これによって、赤の円より外側、すなわちインターフェースやフレームワークの変更からビジネスロジックを守ることができます。
ちなみに、この時点だとDIが解決できないので動かせません。
次のリポジトリの実装まで我慢。
UserRepository の実装
今回はサンプルなので、変数にオンメモリで保持する UserRepository を実装します。
Gateway は緑の円になるので、 adapters 配下に用意します。
.
├── adapters
│ └── gateways
│ └── user_repository.go
├── app
│ ├── gateways
│ │ └── user_repository.go
│ └── interactors
│ └── sign_up_interactor.go
├── di
│ └── di.go
├── domain
│ └── user
│ └── user.go
├── external
├── main
│ └── main.go
└── usecases
└── sign_up_usecase.go
ソースコードはこちら。
あくまで動作させるためのサンプルなので、スレッドセーフでないとか、毎回Newしたらstore共有されないとかそういうのは一旦置いておいていただけると・・・
package gateways
import (
"errors"
"github.com/sat8bit/golang-clean-architecture/domain/user"
)
type UserRepository struct {
store map[string]user.User
}
func NewUserRepository() UserRepository {
return UserRepository{
store: map[string]user.User{},
}
}
func (r UserRepository) FindByEmail(email string) (user *user.User, err error) {
if u, ok := r.store[email]; ok {
return &u, nil
}
return nil, errors.New("user not already exists")
}
func (r UserRepository) Save(user user.User) error {
if _, ok := r.store[user.Email]; ok {
return errors.New("email already exists.")
}
r.store[user.Email] = user
return nil
}
続いてDIも修正します。
パッケージ名が gateways で被るので、Interface の方に別名をつけています。
package di
import (
"github.com/sat8bit/golang-clean-architecture/adapters/gateways"
gwif "github.com/sat8bit/golang-clean-architecture/app/gateways"
"github.com/sat8bit/golang-clean-architecture/app/interactors"
"github.com/sat8bit/golang-clean-architecture/usecases"
)
func SignUpUseCase() usecases.ISignUpUseCase {
return interactors.NewSignUpInteractor(
UserRepository(),
)
}
func UserRepository() gwif.IUserRepository {
return gateways.NewUserRepository()
}
起動してみましょう。
sat8bit $ go run main/main.go
2019/09/13 22:01:35 SignUp succeeded.
2019/09/13 22:01:35 Error is email already exists
うまく動いたようですね。
WebAPIにする
最後に、httpで叩けるように Controller を作成します。
Controller も緑の円になるので、 adapters 配下に用意します。
.
├── adapters
│ ├── controllers
│ │ └── sign_up_controller.go
│ └── gateways
│ └── user_repository.go
├── app
│ ├── gateways
│ │ └── user_repository.go
│ └── interactors
│ └── sign_up_interactor.go
├── di
│ └── di.go
├── domain
│ └── user
│ └── user.go
├── external
├── main
│ └── main.go
└── usecases
└── sign_up_usecase.go
ソースコードはこちら。
ここも最低限の動作をさせる参考程度です。
package controllers
import (
"encoding/json"
"fmt"
"github.com/sat8bit/golang-clean-architecture/usecases"
"io"
"net/http"
)
func NewSignUpController(signUpUseCase usecases.ISignUpUseCase) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
body := make([]byte, r.ContentLength)
length, err := r.Body.Read(body)
if err != nil && err != io.EOF {
w.WriteHeader(http.StatusBadRequest)
return
}
var j map[string]string
err = json.Unmarshal(body[:length], &j)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
_, err = signUpUseCase.SignUp(usecases.SignUpUseCaseInput{
Email: j["email"],
Password: j["password"],
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("%s", err)))
return
}
w.WriteHeader(http.StatusOK)
}
}
DI も書く。
package di
import (
"github.com/sat8bit/golang-clean-architecture/adapters/controllers"
"github.com/sat8bit/golang-clean-architecture/adapters/gateways"
gwif "github.com/sat8bit/golang-clean-architecture/app/gateways"
"github.com/sat8bit/golang-clean-architecture/app/interactors"
"github.com/sat8bit/golang-clean-architecture/usecases"
"net/http"
)
func SignUpController() http.HandlerFunc {
return controllers.NewSignUpController(
SignUpUseCase(),
)
}
func SignUpUseCase() usecases.ISignUpUseCase {
return interactors.NewSignUpInteractor(
UserRepository(),
)
}
func UserRepository() gwif.IUserRepository {
return gateways.NewUserRepository()
}
main も差し替え。
package main
import (
"github.com/sat8bit/golang-clean-architecture/di"
"net/http"
)
func main() {
c := di.SignUpController()
http.Handle("/signUp", c)
http.ListenAndServe(":8080", nil)
}
まとめ
以上の実装で、以下のように整理できました。