LoginSignup
5
0

More than 1 year has passed since last update.

クリスマスなので Clean Architecture 用の自動コード生成ツールを作りたくなった話

Last updated at Posted at 2021-12-17

:christmas_tree: はじめに

この記事は 限界開発鯖 Advent Calendar 2021の18日目の記事です。

前回(17日目)はmerunnoさんの「Discord APIに今後来る変更」でした。
次回(19日目)はkosakae256さんの「MLP-mixerが美味しい季節になってきました」です。

:raised_hands: 自己紹介

「ぬーべすと」と申します。Go言語を推してるただのオタクです。
好きなことは設計、嫌いなことは実装です。
設計だけしたら、誰かが勝手に実装してくれないかなと常に願っています。

:star2: 概要

もうすぐクリスマスですね。皆さんはクリスマスといったら何を連想しますか?
そうです!クリスマスと言ったらやっぱり Clean Architecture ですね!!
ということで、タイトルにもあるように自動コード生成ツールを作りたくなりました。が、怠惰ゆえ実装は間に合いそうにないので構想だけ書きます。言語は当然ですがGoを採用しました。

:cross: 免罪符

私は設計は好きですが、設計についてはド素人です。
訂正などありましたら優しくこっそり伝えて頂けると嬉しすぎて成仏:innocent:します。

:crying_cat_face: 動機

Clean Architecture ってルールに従うだけでいい感じに仕上がるので気に入ってます。
しかし、そんな Clean Architecture も弱点があると思います。

では何かサービスを一つ実装するとしましょう。ドメイン領域内において、私がよく作る順番は以下の通りです。
1. Input Port
2. Output Port
3. Entity
4. Repository
5. Interactor

あとは Controller, View Model, Presenter に加えて View, Repository の実装に加えてDIだって必要です。しかし機能追加のたびにこれらを実装していては疲れますね。猫になるしかないです。

また、ディレクトリ構成も何かしらのルールに従ったものにしなければ、好き勝手にファイルを置いてしまい滅茶苦茶になります。

ドメインモデルの定義を気軽にできて、尚且つディレクトリ構成のルールを継続的に守れるようにしたいです。ということで、こういった問題の解決こそプログラムにやらせましょう!

:mag: 設計思想

実際には Clean Architecture と若干異なった設計思想を採用するつもりでした。しかし、記事を公開して炎上したら後悔しか残らないので、この記事の中では Clean Architecture を意識して書こうと思います。

:gear: 生成されるコードの関係図

用語

UseCase

システムのユースケースを表現した、 Interface です。
実装されていない場合でも、Mock さえあればテストを行えます。

DTO

ユースケースにおいて、ドメイン内部と外部がやり取りするための DTO(Data Transfer Object) です。 Data Structure なのでビジネスロジックを含みません。

Interactor

UseCase の実装が Interactor です。
Entity を利用してユースケースを達成させることが目的となります。

Repository

データを永続化するための Interface です。リポジトリパターンを利用して、ビジネスロジックからDBなど詳細の実装を分離しています。

Entity

Entity とはアプリケーションの登場人物をモデル化したものです。

:construction_site: ディレクトリ構成

以下のようなツリー構造を採用しています

  • project
    • domain
      • xxx
        • model
        • usecase
        • application
        • provider
        • domain
          • yyy
            • model
            • usecase
            • application
            • provider
      • zzz
        • model
        • usecase
        • application
        • provider

ドメインに親子関係を設けています。これによってWebAPIを実装する際、コードに対応しやすいです。

例えば
/users/hoge/items/fuga
上記の様なエンドポイントを実装するのであれば

  • project
    • domain
      • users
        • domain
          • items

のような構造を取ります。勿論、必ずしもこの様に構造を対応させる必要はないので、必要に応じて変更します。

:package: パッケージごとの役割

model

ここには EntityRepository(interface) が含まれます。
Entity と同じ場所に Repository を入れたくない場合は別の場所に persistence などの名前で作って入れるのもありかなと思います。
また、Repository を実装した Mock も生やします。

usecase

UseCase(interface) と DTO(InputData, OutputData など) が含まれます。必要であれば Presenter(interface) も生えると思います。
ここにはビジネスロジックは含まれず、 Interface 及び、 Data Structure のみが置かれるはずです。
また、UseCase を実装した Mock も生やします。

application

Interactor が含まれます。 MockRepository を利用して、テストケースを用意すると幸せになります。

provider

UseCase をDIする為のコードがここに含まれます。利用するDI用のパッケージによって書き方は変わると思いますが、 Interactor のコンストラクタを登録します。

:arrow_right: 依存のルール

依存関係に関してですが、あくまで Clean Architecture のルールに基づきます。例えば 子ディレクトリにある Interactor が、親ディレクトリの Entity に依存するのは問題ないです。

OK: child.Interactor --> parent.Entity
NG: child.Entity ------> parent.Interactor

  • parent
    • model(Entity)
    • domain
      • child
        • application(Interactor)

このディレクトリ構成を採用すると、依存関係が少し分かりにくいというのが課題です。特に Clean Architecture の知識が無いと、依存ルールで交通事故が発生します。

:muscle: 生成されるコード

言語:Go
Mockライブラリ:testify/mock
DIライブラリ:uber-go/dig

※ファイル名は適当に書いてます

Entity の作成

「エンティティ名」や「フィールド」を指定し、Entityを定義します。

hoge.go
type Hoge struct {
    // something
}

func NewHoge() (Hoge, error) {
    return Hoge{}, nil
}

Repository の作成

「リポジトリ名」と「実装させたいメソッド」を指定し、Repositoryを定義します。

repository.go
type Repository interface {
    // something
}

また、 MockRepository を作成します。

mock.go
type MockRepository struct {
    mock.Mock
}

UseCase, Interactor の作成

「ユースケース名」と「入出力データ」を指定し、UseCase Interface と DTO を定義します。

dto.go
type InputData struct {
    // something
}

type OutputData struct {
    // something
}
usecase.go
type UseCase interface {
    Handle(InputData) OutputData
}

また、 MockInteractor の作成します。

mock.go
type MockInteractor struct {
    mock.Mock
}

func (i *MockInteractor) Handle(input InputData) OutputData {
    ret := i.Called(input)
    return ret.Get(0).(OutputData)
}

さらに、実際に利用する Interactor のスケルトンコードを用意します。

interactor.go
type Interactor struct{}

func (i *Interactor) Handle(input usecase.InputData) usecase.OutputData {
    return OutputData{}
}

func NewInteractor() usecase.UseCase {
    return &Interactor{}
}

加えて、 Interactor のコンストラクタである NewInteractor を provider 内で利用するコードを生成します。

provide.go
func Provide(c *dig.Container) {
    c.Provide(application.NewInteractor)
}

:pray: まとめ

上記の様に、プログラムにでも書けるようなコードを自動生成することで、開発者はビジネスロジックの実装に集中できます。また、命名規則やローカルルールなどを、体系化することも可能です。

Clean Architecture は、設計上のメリットは大きいですが、いざ実装してみると辛いことが多く、結果的に負担が大きく感じるケースもあるでしょう。そういった場合に自動コード生成ツールを利用することで、開発効率の改善に大いに貢献できるかと思います。

ここまで読んで頂きありがとうございます :bow:
次回、kosakae256さんの記事「MLP-mixerが美味しい季節になってきました」も是非ご覧ください。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0