あえてGo言語でClean Architectureを学ぶ
目次
はじめに
最近巷で話題のGoらしさって話があると思いますが、
ここはあえてGoらしからぬClean ArchitectureをGoで学んでいこうという記事です。
対象
Go言語をある程度読めて、Clean Architectureに興味がある方
注意
ここでいう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_sha256JMO9TJawIXB1$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