あえてGo言語でClean Architectureを学ぶ

@aintek on Thu Dec 05 2019
14.2 min
はてな Facebook Twitter

目次

はじめに

最近巷で話題のGoらしさって話があると思いますが、
ここはあえてGoらしからぬClean ArchitectureをGoで学んでいこうという記事です。

対象

Go言語をある程度読めて、Clean Architectureに興味がある方

注意

Clean Architectureを採用しましょうって話ではありません。
各言語には思想があるので、その言語らしい書き方に沿うべきだと思っています。

ざっくりとしたアーキテクチャの目的

システムの関心の分離を行い、選択肢を残す(決定を遅らせる)ことが目的です。
またユースケースを中心として開発するため、フレームワークやツールに依存しません。

Clean Architectureとは

Robert Martin がブログで提唱したアーキテクチャ で 日本語訳も存在します。
各層の詳しい説明は以下の資料から確認してください。
ブログ:
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
日本語訳:
https://blog.tai2.net/the_clean_architecture.html

各レイヤのざっくりとした説明

Entities

最重要なビジネスルールとビジネスデータを持ったオブジェクト。

ビジネスをまとめているため、ソフトウェアにかかわらず現実的なビジネスの要件などを表します。

Usecases

アプリケーション固有のビジネスルール。

Entitiesのビジネスルールをいつ・どのように呼び出すといった流れを制御します。

Interface Adapter

UsecasesやEntitiesのデータをDBやWebで扱いやすいデータに変換するアダプタです。

逆も同様

Frameworks & Drivers

WebやDBといった詳細に関するレイヤ。ここではコードをあまり書かないらしいです。

実装してみる

簡単な説明が終わったので、各レイヤをGoで実装してみます。

今回はよくあるWebでのユーザ処理を例にします。

Entities

UserのEntityを作っていきます。IDや名前が存在し、パスワードのチェックを行うメソッドも定義しています。

これらはシステムにこだわらないビジネスの要件になっていると思います。

※ここでは以下のようなパスワードを想定しています。

“pbkdf2_sha2562400024000JMO9TJawIXB1$5iz40fwwc+QW6lZY+TuNciua3YVMV3GXdgkhXrcvWag=”


type User struct {
    ID string
    Name string
    Email string
    Password string
}
func (u *User) PasswordVerify(pw string) bool {
    s := strings.Split(u.Password, "$")
    cost, err := strconv.Atoi(s[1])
    if err != nil {
        return false
    }
    hash := pbkdf2.Key([]byte(pw), []byte(s[2]), cost, sha256.Size, sha256.New)
    b64Hash := base64.StdEncoding.EncodeToString(hash)

    return b64Hash == s[3]

}

Usecases

UserのUsecaseを作っていきます。
まずは各Interfaceを定義します。Repositoryを操作するUserInterfaceと
InputPort、OutputPortを定義しています。
レイヤの境界を超えるために、Input/Output Portが存在しています。Boundaryとも呼ばれます。

type (
    UserRepository interface {
        FindByEmail(string) (entities.User, error)
    }

    UserInputBoundary interface {
        Login(*LoginInput) 
    }

    UserOutputBoundary interface {
        Login(*LoginOutput, error)
    }
)

次にUsecaseを書きます

// NewUser UserのInputPortを返却
func NewUser(output UserOutputBoundary, repo UserRepository) UserInputBoundary {
	return &UserInteractor{
		UserOutput: output, 
		UserRepository: repo,
	}
}
// UserInteractor Userのユースケースを実装する
type UserInteractor struct {
	UserOutput UserOutputBoundary
	UserRepository UserRepository
}

// Login 
func (ui *UserInteractor) Login(input *LoginInput) { 
	user, err := ui.UserRepository.FindByEmail(input.Email)
	if err != nil {
		ui.UserOutput.Login(&LoginOutput{}, err)
		return 
	}

	if ok := user.PasswordVerify(input.Password); !ok {
		ui.UserOutput.Login(&LoginOutput{}, errors.New("verify failed"))
		return 
	}
	output := LoginOutput {
		ID: user.ID,
		Name: user.Name,
		Email: user.Email,
	}
	ui.UserOutput.Login(&output, nil)
}

そして、Input/Output を定義します。

type LoginInput struct {
	Email string
	Password string
}

type LoginOutput struct {
	ID       string
	Name     string
	Email    string
}

UsecasesはInput/Output Portが存在し、レイヤの境界をまたぐような実装になっています。
また、RepositoryにもInput/Output Portが存在するはずですが、
今回は説明が複雑になるので省略します。(本当は時間がありませんでした。すみません)

Interface Adapter

ここではWebの入力(Controller)と出力(Presenter)を実装します。
ControllerはContext interfaceのBindで入力を構造体に変換しています。
例えばWebではなくコマンドになった場合は、別のパッケージに違うControllerを定義したほうがいいと思っています。

// Controller

type Context interface {
	Bind(interface{}) error
}

// NewUserController
func NewUserController(out usecases.UserOutputBoundary, repo usecases.UserRepository) *UserController {
	interactor := usecases.NewUser(out, repo)
	return &UserController{
		Interactor: interactor,
	}
}

// Login 
func (controller *UserController) Login(c Context) {
	type userLoginRequest struct {
		Email    string `json:"email"`
		Password string `json:"password"`
	}
	req := userLoginRequest{}
	c.Bind(&req)

	input := usecases.LoginInput{
		Email: req.Email,
		Password: req.Password,
	}

	controller.Interactor.Login(&input)
}

次にPresenterを実装します。

type Context interface {
    JSON(int, interface{}) error 
}

type User struct{
    Context Context 
}

func (u *User) Login(output *user.LoginOutput, err error) {
	if err != nil {
		c.Error(404)
		return
	}
    c.JSON(200, output)
}

Frameworks & Drivers

最後にFrameworks & Driversを実装します。
今回はgo-chiというシンプルなHTTPルータを使用しています。

type context struct {
	w http.ResponseWriter
	r *http.Request
}

func (c context) Error(status int) {
	http.Error(c.w, http.StatusText(status), status)
}
func (c context) Bind(v interface{}) error {
	return json.NewDecoder(c.r.Body).Decode(&v)
}
func (c context) JSON(status int, v interface{}) error {
	res, err := json.Marshal(v)
	if err != nil {
		return err
	}
	c.w.WriteHeader(status)
	c.w.Header().Set("Content-Type", "application/json")
	c.w.Write(res)
	return nil
}

func NewContext(w http.ResponseWriter, r *http.Request) context {
	return context{
		w: w,
		r: r,
	}
}

var Router chi.Router

func init() {
	r := chi.NewRouter()
	r.Use(middleware.Recoverer)
    r.Use(render.SetContentType(render.ContentTypeJSON))

	r.Route("/auth", func(r chi.Router) {
		r.Post("/login", func(w http.ResponseWriter, r *http.Request) {
			context := NewContext(w, r)
	        userController := controllers.NewUserController(&presenter.User{Context: context}, &gateway.UserRepository{})
			userController.Login(context)
		})
	})
	Router = r
}

終わりに

Clean ArchitectureをGo言語で実装しました。
UsecasesのInput/Output Portの考え方は特徴的だと思います。

しかし、たったこれだけの処理をこれほどソースを書かなきゃいけないのは大変です。
Goらしさもない気がするので、GoでのClean Architectureはおすすめではないと思います。

参考

本家:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
本:https://www.amazon.co.jp/dp/4048930656

日別に記事を見る