目的
Clean Architecture の勉強会をした際の内容を残します。記事の内容としては ブログの内容を基に説明+サンプルコード の構成となります。
Clean Architecture とは
まず Clean Architecture についの説明です。Clean Architecture は Robert C.Martin のブログが基になります。
概要
Clean Architecture はアーキテクチャパターンの1つであり、アプリケーションを円形のレイヤで分割し、外側のレイヤが内側のレイヤに依存するのが特徴です。
よく使われるレイヤ定義としては4つあり、内側から順に以下の通りです。
- Enterprise Business Rules
- Application Business Rules
- Interface Adapters
- Frameworks & Drivers
上記のレイヤ以外にも必用に応じてレイヤを追加/削除をしても構いませんが、外側のレイヤが内側のレイヤに依存する原則を変えてはいけません。
Enterprise Business Rules
企業固有(*)のビジネスルールをカプセル化したレイヤ。(Entities) ※原文は「Enterprise wide」です。
このレイヤは一般的で高レベルな実装であり、メソッドを持つオブジェクトでもデータとロジックの集まりでもよいです。
大切なのはビジネスルールの変更でしか影響を受けない事であり、ページネーションやセキュリティ等の技術要素に影響を受けることはあってはならないです。
Application Business Rules
アプリケーション固有のビジネスルールをカプセル化したレイヤ。(UseCases)
個人的にわかりやすいのはユースケースシナリオを実装するイメージ。ユーザーからの入力を基にアプリケーション固有のロジックを実行したりビジネスロジックを実行します。
このレイヤもアプリケーション固有のビジネスルール変更かビジネスルールの変更でしか影響を受けない。例えばコマンドツールだったのがWebアプリケーションになっても影響を受けてはいけないです。しかし、もしWebアプリケーションになったことでページネーションの仕様が追加された場合には使い方(アプリケーション固有のビジネスルール)が変更されているため、影響を受けることになります。
Interface Adapters
Enterprise Business Rules と Application Business Rules を内側とした際に、その外側の世界とを繋ぐためのレイヤ。
このレイヤの役割はデータ変換であり、外部からのデータを内部に適したデータ構造に変換し、逆に内部からのデータを外部に適したデータ構造に変換します。(Interface Adapters だけは Adapter の役割を持っているため Adaptee のデータ構造を知っている必要があります。フレームワークで吸収可能なケースもあり得ます。)
このレイヤは、例えばコマンドツールだったのがWebアプリケーションに変更されるケースやファイルストレージだったのがRDBに変更されるケースに影響を受けます。
なお、RDBの実装に固有なコードもあり得ますが、その場合には可能であればパッケージ等で薄く切り出して依存関係逆転の原則で呼び出すか、或いは切り出すメリットが少なければ潔く依存コードを混ぜてしまっても良いかと思います。
補足
Clean Architechture を初めてWebアプリケーションで試してみる際には、このレイヤの実装方法が一番戸惑いが多いと感じます。
これはWebがRequest/Responseの同期型であることと、ボブおじさんの図にController/Presenterがあることが影響していると考えられます。Requestを受け取る処理をControllerとして実装し、Responseを応答する処理をPresenterとして実装するようなケースです。
そのような設計をしても問題なく動作させることは出来ますが、必用でない限りControllerで応答をしたほうが分かりやすいです。
ポイントとしては、ユースケースの途中で非同期的に応答を返す必要があるか、或いはアプリケーション全体で適応しやすいものを選択すると良いかと思います。
Frameworks & Drivers
アプリケーションで利用するデータベースやフレームワークやツール、ドライバー等で構成されたレイヤ。
ざっくり表現すると外側の世界の技術要素の事で、ユーザーが直接操作をするWebUIのケースやWeb用フレームワークのケース等があり得ます。
サンプルコード
ショッピングアプリケーションの注文確定をイメージしたサンプルコードです。 ⇒ サンプルコード
Enterprise Business Rules
biz/order
, biz/picking
, biz/payment
に実装しています。
ポイントはどのようなアプリケーションであろうとも不変なロジックを抽出することです。WebやFaxで変わるようではEntitiesにはなりません。
このロジック自体は業務特有のためサンプル実装の意味が薄いので、今回のサンプルではコメント出力だけしています。
ロジックは 注文内容のチェック, 注文された商品のピッキング, 注文の支払い です。
なお Clean Architecture ではレイヤ内の設計については言及しておらず、例えばDDDを用いた設計をしてもOKです。
Application Business Rules
app/order
に実装しています。
このレイヤではユースケースを実現します。ユースケースにはセッションや権限のチェック、Entitiesのロジックを呼び出します。
サンプルではユーザーが注文確定処理を実行したユースケースを想定し、ユーザーセッションのチェックと注文オブジェクトの構築、レスポンスデータの生成をおこなっています。
func OrderNow(ctx context.Context, input Input) (*Output, error) {
fmt.Println("ユーザーが注文を確定するユースケース")
session, ok := usersession.FromContext(ctx)
if !ok {
return nil, errors.New("session not found")
}
for k, v := range input.OrderDetails {
fmt.Println(k, v)
}
o := order.Order{
ID: uuid.New().String(),
Details: input.OrderDetails,
UserID: session.UserID,
}
if err := order.Validate(ctx, o); err != nil { // 注文内容のチェック
return nil, err
}
event.ConfirmedOrder.Publish(ctx, o)
return &Output{OrderID: o.ID}, nil // 同期(request/response) であれば戻り値で結果を返す設計の方が簡潔で直感的
}
注意点として、Webアプリからモバイルアプリにアプリケーションが移植されたとしても、それだけではこのレイヤのロジックが変更される根拠にはなりません。しかし、モバイルアプリに変わったことで使われ方(ユースケースシナリオ)が変わるのであれば、このレイヤのロジックが変更される可能性があります。
Interface Adapters
ifadapter/example
に実装しています。
このレイヤでは外部から入力されたデータ構造をユースケース用のデータ構造に変換します。そのためWebアプリからモバイルアプリに変わったことでデータ構造が変化する場合にはこのレイヤが変更されます。
サンプルではHTTPリクエストを受け取ってユースケースを呼び出しています。
func UserConfirmOrder(ctx context.Context, req http.Request) {
session := usersession.UserSession{UserID: "bob"}
ctx = usersession.NewContext(ctx, &session)
// HTTPのデータ構造をユースケースの入力データに変換する
body, err := ioutil.ReadAll(req.Body)
if err != nil {
log.Fatalln(err)
}
defer req.Body.Close()
orderItems := new(map[string]uint)
if err := json.Unmarshal(body, orderItems); err != nil {
log.Fatalln(err)
}
input := order.Input{
OrderDetails: *orderItems,
}
// ユースケースの実行
output, err := order.OrderNow(ctx, input)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(output)
}
// 本来ならHTTPレスポンスを返す
}
サンプルではHTTPサーバを動かさずに、main.goから直接呼び出す実装になっています。
ctx := context.Background()
req := http.Request{
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{
"book": 3,
"food": 8
}`))),
}
example.UserConfirmOrder(ctx, req)
Frameworks & Drivers
今回のサンプルには含まれません。
このレイヤはアプリケーションの外の世界、或いはそれらを設定情報になります。
プログラムに組み込まれる具体的な例としては、MySQLに接続するためのドライバーをロードする処理や、Frameworkの設定ファイルがあります。
ポイントとしては外の世界の技術要素を閉じ込める事を意識して設計する事です。Interface Adapters はデータ構造の変換が責務でしたが、Frameworks & Drivers は設定や処理の隠ぺいが責務となります。
所感
Clean Architecture の最大の魅力は、レイヤを円に分けることでパッケージ構成と責務が分かりやすくなることだと思います。
Hexagonal Architecture や Onion Architecture と同じような事を言ってはいても、用いる図や用語によってアプリケーション全体の見通し易さは大きく変わる印象です。