クラスター社のGo製WebAPIのパッケージ構成について

ちょうど二年前のアドベントカレンダーでこんな記事を書いていたようです。

https://qiita.com/kyokomi/items/8350b6aa10ec06aee554

現状はどうしてるのか当時を振り返りながらまとめようかなと思います :muscle:

vendoringについて

依存管理は、depを使ってます。(depが出るまではglide使ってました)

https://github.com/golang/dep

2年前にくらべると色々と便利になって普通に使っていけるようになりました。

当時はまだGo1.5とかでしたね...

主に使ってるライブラリ

とりあえず以下使ってますという感じ

  • goa
  • gorm

詳細はこちら
https://qiita.com/kyokomi/items/dcd8384a0a042d72d22d

パッケージ構成とか

もともとDDD本に出てくるレイヤードアーキテクチャを意識してたんですが、いつの間にかオニオンアーキテクチャ風になっていたようです。
現時点では、この形が自分の中ではベストかなと思ってます。

├── application
├── domain
│   ├── file
│   ├── friendship
│   └── user
├── infrastructure
│   ├── adapter
│   │   ├── auth
│   │   ├── aws
│   │   ├── cache
│   │   ├── firebase
│   │   └── rdbms
│   └── repository
└── presentation
      ├── controller
      ├── middleware
      └── view
パッケージ名 対応するオニオンアーキテクチャでの名称 説明
presentation/controller User Interface goaでgenerateしたcontroller(httpのHandler)。ここからapplication Serviceを呼び出す
presentation/middleware requestHeaderのチェックとかいわゆるmiddleware
presentation/view User Interface goaで定義したresponseへmappingを行って書き込む
application Application Service domain/xxxxのRepositoryインターフェース経由でドメインオブジェクトの取得や更新を行います
domain Domain Model ビジネスロジック。特定のinfrastructureやviewに依存しない純粋な仕様を定義
infrastructure/adapter Infrastructureから外に出ていく部分のadapter RDBとかRedisなどのCache、外部のAPIなどとやり取りする
infrastructure/repository Infrastructure domain/xxxxのRepositoryインターフェースを実装したstruct

呼び出しの流れ

図にするとこんな感じの呼び出しの流れになります。(気がむいたら清書します :pray:

friendShip(フレンド関連)の機能を例にしてます。(字が汚い...)

image.png

※図は雰囲気で書いた図なのでUMLとかみたいなちゃんとしたやつではないです :pray:

矢印は基本的にデータの流れですが、インフラストラクチャのfriendShipRepositoryからドメインサービスのfriendShipRepositoryのインターフェースに向かってる矢印だけ継承を示しています

ポイント

  • domain/xxxxのRepositoryがinterfaceになることで、中身の実装を意識しないで呼び出せるようになってる(依存関係の逆転)
  • RDBでは、テーブルが2つあるので2つobjectを渡す〜みたいなことがないようにRepositoryのI/Fの設計をしていくのが大事
  • goaでgenerateするControllerはApplicationServiceを呼び出して結果をviewに渡すだけに務めることでほぼコードを見なくていい
  • RDBの設計や過去の負債などの都合で変なデータ含まれるけど〜みたいなのは、Repositoryのインターフェースを実装したとこに定義するmapperで吸収する(腐敗防止層的な)

mapperについて

各層でのデータのやり取りで変換するコードをガリガリ書いちゃうと可読性が下がるので適宜mapperを書いている(こんなやつ)

globalなメソッドにすると利用範囲が曖昧になるので空のstruct作ってメソッド生やして整理してます。(別パッケージにするまでもない系)

type friendShipConvert struct {
}

var friendShipConverter = friendShipConvert{}

func (cnv friendShipConvert) toFollowTables(userID user.Identifier, followUsers friendship.Users) []*rdbms.UserFollowTable {
    result := make([]*rdbms.UserFollowTable, len(followUsers))
    for i := range followUsers {
        result[i] = &rdbms.UserFollowTable{
            UserID:       userID.String(),
            FollowUserID: followUsers[i].ID.String(),
        }
    }
    return result
}

func (cnv friendShipConvert) toBlockTables(userID user.Identifier, blockUsers friendship.Users) []*rdbms.UserBlockTable {
    result := make([]*rdbms.UserBlockTable, len(blockUsers))
    for i := range blockUsers {
        result[i] = &rdbms.UserBlockTable{
            UserID:      userID.String(),
            BlockUserID: blockUsers[i].ID.String(),
        }
    }
    return result
}

社内共通ライブラリ

普通に ClusterVR/go みたいなリポジトリを作ってそれをdepでvendoringしてます。
念のため rdbms/v1 みたいなバージョン切り替えできるディレクトリ掘ってpackage名を rdbms にするとかやってるんですが、v2が作られる目処がないのでいまのところ本当に必要だったのかは謎です :thinking:

├── goa
│   └── middleware
│       └── v1
├── kvs
│   └── v1
├── mqtt
│   └── v1
├── rdbms
│   └── v1
└── sentry
    └── v1

あえて不満をあげるなら

各レイヤー間でのデータのmappingで毎回for文とか筋肉で書くのがちょっと面倒なくらいです :innocent: :innocent:

おまけ

CIについて

CircleCI -> Wercker -> CircleCI 2.0という感じの歴史を辿ってます。

Werckerは高いわりに結構とまるのでストレスが結構溜まってたんですが、CircleCI 2.0でDockerイメージが使えるようになっていい感じになり移行しました。

今は平和です :smile:

ローカル開発

docker-composeで必要infrastructureやマイクロサービス化したinternal-apiなどをまとめて起動して動作確認する感じでやってます。

ずっと起動してるとPCのバッテリーが急速に減るのが悩み...

所感

ずっと書こう書こうと思って書いてなかったので、2年越しにようやくというお気持ち...

実装時に考えるのはdomain/xxxxをどういう粒度でつくるべきか?とかがメインであとは単純作業なのでサクサクと実装が進んで良い感じです。
一旦あまり考えずにパパッと作って違和感がでてきたら整理してリファクタリングという流れをやっていていい感じにワークしてます。

以上、来年?はどうなってるか楽しみですね。