kitslog

何かを書く

Go with Clean-Architecture


author: "@tkitsunai" date: 2018-05-21 next: /tutorials/github-pages-blog prev: /tutorials/automated-deployments title: Serverside Golang with Clean Architecture tags: ["golang","architecture"] categories : ["blog"]

description : About the Clean Architecture of software written in Go code.

ここ暫くのプロダクト開発ではGo + Clean Architectureを採択することが多くなりました。 何故この採択が増えたのか、Go + Clean Architectureを選ぶことについて改めて触れておくことにします。

まずはGolangとClean Architectureについて知らない人のためにほんの少しだけ説明します。

Go

Goの魅力は何でしょうか?

静的型付け、言語としてのシンプルさ、コンパイルの速度、並列プログラミング、GCアルゴリズムの最適化、実行バイナリ配布

何故Golangを選ぶのかを挙げるとしたら上記の理由が殆どになるでしょうが、それのどれらも本質的ではありません。

「そもそもGolangである必要はあるのか。」という質問において、答えはNOでもあるしYESでもあります。

言語毎のパラダイムの違いなどはあれど、信念を持って開発に臨むことでそういった問題はどれも些細な問題に成り下がるからです。

少し話が逸れました。今回はClean ArchitectureとGolangのお話をしますが、このコンテキストにおいて何故Golangを選択したのかについて説明するにはGolangの特性について軽く触れておく必要があります。

Golangのimport cycle

C++なども同じ言語仕様ですがGolangでは循環参照が禁止されています。

package aaa

import "bbb"

func A() {
    println("aaa.A()")
}
func CallB() {
    bar.B()
}
package bbb

import "aaa"

func B() {
    println("bbb.B()")
}
func CallA() {
    bar.A()
}

上記のようなパッケージ、aaaとbbbがお互いにimportしている状態ではコンパイルエラーになります。

今回のClean Architecuteにおいて、この循環参照の禁止という制約が良い働きをサポートします。

Clean Architecture

Clean ArchitectureがRobert Martinによって発表されてからはや6年ほどが経過しています。

参考:https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

Clean Architectureとは考え方でありフレームワークです。

Domain Driven Designにおいてソフトウェアの複雑性と戦うために生み出されたレイヤードアーキテクチャの派生の一つです。

Clean Architectureの最も特徴的なことのひとつに、その各レイヤーの依存関係のルールが挙げられます。

CleanArchitecture

図にある通り、4つ(フレームワーク層→アダプター層→ユースケース層→ドメイン層)から構成されており、それぞれ円の中心にしか依存関係を保ってはいけないというルールがあります。

アプリケーションにおけるドメイン層が他に全く依存しないことで、フレームワークの書き方や本質的ではないアプリケーションロジックの影響を受けることを最小限に軽減します。

同じレイヤードアーキテクチャ仲間である、Hexagonal ArchitectureOnion Architectureも同じように依存関係に対してルールを持ちますが、Clean Architectureにおいては依存関係の方向性を単一方向にすることにより長けています。

Golang + Clean Architecture(CA)

Golangの特徴とCAの特徴を踏まえた上で、GolangでCAを採用すると、単方向を守りやすくなります。

以下は私のプロジェクトでよく採用するプロジェクトのファイル構成です。

Project Root
├── adapter
│   ├── controllers
│   │   └── user.go
│   ├── persistent
│   │   └── repository
│   │       └── user.go
│   └── presenters
│       └── user.go
├── domain
│   └── user.go
├── external
│   └── api
│       └── router.go
├── main.go
└── usecase
    ├── interactor
    │   └── user.go
    └── repository
        └── user.go

かなり省略しています。細かいところはプロジェクトによって変わりますが、そこまで大きくは変わりません。

上記のプロジェクトはUserのデータをCRUD出来るようなAPI Serverを想定します。

依存関係は、CAに則ると上記のフォルダ構成ではexternal→adapter→usecase→domainとなります。

各レイヤー毎を軽く見ていきます。

[external]

├── external
│   └── api
│       └── router.go

externalはフレームワークやWeb、Viewを位置する外界と繋ぐレイヤーです。 APIのルーティングなどはまさに外界と繋ぐ場所になるためexternalのレイヤーに配置します。

そしてrouter.goでは恐らく以下のようなコードブロックを書きます。(疑似コードです)

package api

import (
    "hoge/adapter/controllers"
    "hoge/adapter/presenters"
)

type Router struct {
    Router
}

func (r *Router) router() {

    r.GET("/user/:userId", func(c *Context) {
        result := controllers.User.GetUser(c.Param("userId"))
        presenters.User.Get(result)
    })

}

依存関係から整理すると、外界はAdapter層を利用(依存)することが出来ます。

上記コードではrouterがadapter層であるcontrollerとpresenterを利用しており、他に依存がありません。

routerはコントローラという名前の入り口と、それにパラメタを渡し、戻ってきた結果をそのままプレゼンターに横流しします。

関心の分離や単一責任の原則の観点で見ることも出来ますが、重要なのは依存しているレイヤー(import句)が"adapter"のみになっていることです。

external層はCAに則っていますね。では次のレイヤーを見ていきます。

[adapter]

├── adapter
│   ├── controllers
│   │   └── user.go
│   ├── persistent
│   │   └── repository
│   │       └── user.go
│   └── presenters
│       └── user.go

adapter層は名前の通り外界と繋ぐ役割を果たします。DBから値を取得するrepository実装やrouterからの入り口はここに実装されます。

ここでは、controllerについてだけ触れます。

package controllers

import (
    "hoge/usecase/interactor"
)

type User struct {
    Interactor interactor.User
}

func (u *User) Get(userId string) (*domain.User, error) {
    return u.Interactor.Get(userId)
}

Controllerの役割を省略してますが、コントローラでは本来入力値のチェックなども行いますが、基本的には呼ぶのはUsecase層のInteractorを呼ぶだけです。

ここでも依存関係は内側であるUsecase層に限定することが出来ています。 (戻り値であるdomainパッケージのuserについてはControllerが利用という意味で本質ではないので言及はしていません。)

次にUsecase層です。

[usecase]

└── usecase
    ├── interactor
    │   └── user.go
    └── repository
        └── user.go

RepositoryはAdapterレイヤーにあるRepository実装クラスの抽象です。CAの単方向のルールに則るためにはUsecase層はAdapter層に依存することは出来ないため、DIP(依存関係逆転の原則)を利用することで抽象に依存し、Usecase層は単方向を守ることが可能になっています。

大抵はRepositoryの実装クラスを外側、アプリケーションのエンドポイントに近い位置でDIするためUsecase層ではアプリケーションロジックに集中することが出来ます。

コードブロックは省きますが、ここではControllerから呼び出されるinteractorの処理の内、repository経由で取得したドメインオブジェクトやドメインオブジェクトを利用します。

[domain]

├── domain
│   └── user.go

Domain層は、円の中心に存在しアプリケーションの核となる部分です。ドメインドメインロジックを充実させます。

Domain層にはAtomicなコードを書いていくことを心がけ、シンプルで明瞭なコードにします。

よくDomainオブジェクトがデータベースのModel Entityと混在しているサンプルアプリを見ますが、クリーンアーキテクチャの思想に則った場合にはアンチパターンではないかと考えています。

(もちろん規模やケースバイケースにもよるとしか言えません)

Golangのimport cycleの働き

私のチームではじめてのClean Architectureを採用したプロジェクトでは、Clean Architectureを学習するために多くの時間を費やしました。

Golangでやることが決まり、Clean Architectureにしてみたいという状態から依存関係を守らせるのにimport cycleの仕組みが一役買っていました。

見るべきポイントとしては、

「今自分がどのレイヤーを書いているのか」

「円形の図を見た時、自分のレイヤーは外側に依存(import)していないか」

を頑なに繰り返すことでよりClean Architecuteの単一方向のルールの制約を実現させます。

まとめ

import cycleの制約をあえて活かすように設計することで依存関係の方向を纏めることができました。

Clean Architectureは簡易的なアプリケーションにおいてもコードベースがかなり増えるため、プロジェクトの規模などを適切に判断し、用法・用量を守って正しく使いましょう。