在Go中实践Clean Architecture

简要概括

我在探索中使用Golang实现了清晰架构,并将其保留下来以备将来使用。如果我认为有改进的地方,我会随时更新。

ツッコミや個人的な意見は大歓迎です。

中国的本地化改写:“干净架构”是指一种软件设计原则。

我会省略在这里重新解释,而建议你阅读这篇Qiita文章会更好。

image.png

实践

包装目录结构

由于肝脏控制依赖关系,我们为每个圆创建了一个目录。
此外,用例被独立切分,重点放在中心考虑。

.
├── 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)
}

总结

通过上述的实施,我们可以整理如下。

image.png
bannerAds