在Go中实践Clean Architecture
简要概括
我在探索中使用Golang实现了清晰架构,并将其保留下来以备将来使用。如果我认为有改进的地方,我会随时更新。
ツッコミや個人的な意見は大歓迎です。
中国的本地化改写:“干净架构”是指一种软件设计原则。
我会省略在这里重新解释,而建议你阅读这篇Qiita文章会更好。

实践
包装目录结构
由于肝脏控制依赖关系,我们为每个圆创建了一个目录。
此外,用例被独立切分,重点放在中心考虑。
.
├── adapters # 緑色
├── app # 赤色
├── domain # 黄色
├── external # 青色
└── usecases # 赤色(定義)
根据这个开始,逐步创建文件。
提取用例
本次我们以常见的“用户注册”作为例子来讨论。
由于目前不需要深入研究处理的内容,因此我们只需要确定粗略的参数和名称。
一旦确定,将接口放置在用例下。
.
├── adapters
├── app
├── domain
├── external
└── usecases
└── sign_up_usecase.go
以下是源代码。
我们将定义 UseCase 的接口以及输入/输出。
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
源代码在这里。
目前还没有注入或任何其他内容。
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的主程序。虽然我们现在要创建主程序,但实际上只需要编写UseCase的测试代码即可。
.
├── adapters
├── app
│ └── interactors
│ └── sign_up_interactor.go
├── di
│ └── di.go
├── domain
│ └── user
│ └── user.go
├── external
├── main
│ └── main.go
└── usecases
└── sign_up_usecase.go
源代码在这里。
因为有现有的注册检查,所以将编写代码尝试进行两次注册。
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
注册互动器的实现
由於這兩個計劃中的處理都涉及到持久化數據的操作,所以我們將繼續準備UserRepository。
为了保持依赖关系从绿色到红色,UserRepository位于绿色的Gateway中,首先需要在应用程序内部完成定义。
.
├── 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
}
由于已经定义了接口,所以我们将在 SignUpInteractor 中添加依赖和实现。
(Paraphrase in Chinese: Now that the interface has been defined, we will add dependencies and implementation to 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 保存在内存中的变量。 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
这里是源代码。
这只是一个用来展示功能的示例,所以请暂时忽略线程安全或每次新建对象时不共享存储等问题。
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请求访问的控制器。
控制器也将成为绿色圆圈,并准备在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()
}
主要更换。
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)
}
总结
通过上述的实施,我们可以整理如下。
