🎄

Goのプロジェクト構成について

2024/11/29に公開

Go のコミュニティーで昔から話題になっているテーマだと思いますので、自分なりの考えを共有したいと思います。プロジェクトの内容によって構成などはかなり違ってくると思いますが、ここでは DDD を使ったWebサーバーについて紹介します。よくあるユースケースではないかと思います。

フィードバックやコメントなど大歓迎です!意見交換もしていきたいです!

ファイルとネームスペース

Go ではプロジェクト構成がそのままネームスペースになります。不便だと思う方もいるかも知れませんが、僕は非常に使いやすいと思っています。他の言語ではしばしば、プロジェクトの構成をすでに大まかに決めているか、もしくはコードを書く前に決めるようにすることが基本だったりします。しかし、Go ではコードを書きながらプロジェクトの構成、パッケージの追加・削除を行うことが一般的です。

よくインターネットで Go の初心者が、「プロジェクトをどのように構成すれば良いのか?」と悩んでいるところを見かけます。ググってみると「Standard Go Project Layout」という Git リポジトリが見つかるでしょう。ただ、こちらのプロジェクト構成テンプレートを推薦する意見はなかなか見つかりません。「これはおすすめしない」「オフィシャルじゃないし」「勝手に Standard だと言っているだけ」などの声ばかりです。

僕も推薦はしません。なぜ推薦しないのかというと、やはりオフィシャルな方針がないからです。「開発者の使いやすいやり方」が最も良い構成だという考えです。1つ1つのプロジェクトに合う構成はもちろんありますが、すべてのプロジェクト共通のベストは当然ありません。

そのため、大きなプロジェクトの構成は様々ですが、くわしく見ていくと、なぜそのような構成になっているのかという理由がわかります(大体のものは笑)。

よく見る構成

例えば、以下のような構成をよく見かけます。特に DDD を使っているプロジェクトは、このような形になりやすいと思います。

.
├── application/
│   ├── view/
│   │   └── user.go
│   └── service/
│       └── user.go
├── cmd/
│   ├── api/
│   │   └── main.go
│   └── batch/
│       └── main.go
├── domain/
│   ├── command/
│   │   ├── user.go
│   │   └── something.go
│   ├── entity/
│   │   ├── user.go
│   │   └── something.go
│   └── value/
│       ├── user.go
│       └── something.go
└── infrastructure/
    ├── handler/
    │   └── user.go
    ├── mysql/
    │   ├── user.go
    │   └── something.go
    └── s3/
        └── user.go

ここでは、あえてファイル名をすべて同じにしています。
もちろん、domainmodel になったり、エンティティがそのまま親パッケージに入ったり、バリエーションは様々ありますが、見慣れた構成ではないでしょうか?

ネームスペースは以下のようになります。

service.UserSerive
entity.User
value.UserID
command.CreateUser
mysql.UserRepository

何がエンティティで何が値オブジェクトなのか、他にどのようなエンティティがあるのかも簡単に調べることができるようになっています。

ただ、個人的にもっと使いやすい構成があると思います。

関心の分離 vs. 振る舞いの局所性

本題に移る前に、ここでは導入として、最近流行りの htmx を生み出した Carson Gross 氏による「振る舞いの局所性(Locality of Behaviour, LoB)」を説明していきます。この考え方が、Goにも使えるのではないかと思ったのです。

「振る舞いの局所性」は以下のように定義されています。

あるコード単位の振る舞いは、その単位のコードだけを見ても可能な限り明らかでなければならない。

これと相反する考えである「関心の分離(Separation of Concerns, SoC)」は、おそらくどのプログラマーも聞いたことがある原則だと思います。定義は以下の通りです。

関心の分離とは関心(責任・何をしたいのか)毎に分離された構成要素で構築することを指す。

(Wikipedia)

HTML で例えると

  • SoC では HTML、CSS、Javascript をきちんと分けて書くこと
  • LoB では HTML、CSS、Javascript を混ぜて書くこと

LoB はまだ新しい考え方なので、違和感を抱く人もいるでしょう。一度、次のように考えてみてください。

HTML 要素にどのようなイベントハンドラーがついているのかをすぐに参照する方法はあるか?

ブラウザのデバッガーでは参照できるようになっていますが、(生のJavascriptの場合)コードを見るだけではなかなかわかりにくいと思います。

また、CSS も複数のファイルをインクルードすることでスタイルが被るなどの問題によって、レイアウトが崩れることがよくあるのではないでしょうか。逆に CSS も Javascript も直書きすれば、その HTML を見るだけで(ブラウザを利用せずとも)何が起こるのかが一目でわかるようになります。ちなみに Tailwind は割と LoB 的な考え方だと思います。

少し極端な例かもしれませんが、Goにも同じような考え方を適用することができるのではないかと考えました。

Go における LoB(振る舞いの局所性)

僕の考える、今のところ最も使い心地が良いプロジェクト構成は以下のようになります(少し省略しています)。

.
├── user/
│   ├── commands.go
│   ├── entity.go
│   ├── repository.go
│   └── values.go
├── something/
│   ├── commands.go
│   ├── entity.go
│   ├── repository.go
│   └── values.go
└── infrastructure/
    └── mysql/
        ├── user.go
        └── something.go

こうすることで、ユーザーに関するコードはパッケージに分散されるのではなく user ディレクトリーにまとめられます。
ネームスペースも、前述の例よりも自然な言語になります:

user.User
user.ID
user.Repository
user.CreateCommand

簡単なビューも user.Viewuser.ProfileView のように実装可能です。user.Repository はインターフェースになっていて、実装は infrastructure/mysql/user.go に入っている構成です。

また、ユーザーが DDD でいう集約(Aggregate)であれば、所属するエンティティをサブディレクトリにまとめる設計も可能です。

.
└── user/
    ├── ...
    └── comment/
        ├── entity.go
        ├── repository.go
        └── value.go

こうすることで、構成を見るだけでユーザーに所属しているものがわかるようになります。また、同じエンティティ名が他にも存在している場合も、UserCommentOperatorComment などのようなネーミングを避けることができます。

代替案

やはり entity.Uservalue.UserID のほうが良いと思う方もいるかもしれません。その場合、例えばディレクトリを一層追加することによってネームスペースを合わせることも可能です。

.
└── user/
    ├── command/
    │   └── commands.go
    ├── entity/
    │   └── entity.go
    ├── repository/
    │   └── repository.go
    └── value/
        └── values.go

こうすることによるメリットもあります。通常、entity パッケージにすべてのエンティティを置いた場合、entity. という文字列を入力すると、オートコンプリートでとても長いリストが表示されると思いますが、そのほとんどは関係ないエンティティですよね。

上記の構成では、インポートさえ合っていればユーザーに関連するエンティティしか表示されないようになっています。

デメリットとしては、インポート名が被ったり、自動インポートが間違ったパッケージをインポートしてしまう可能性があります(しかし、そもそもインポート名が被った場合、設計に何かしらの問題がある可能性が高いと思われます)。知らないうちに間違った型を使っていたということも考えられますが、きちんと型エイリアスを活用することでコンパイルエラーになるようにもできるので、大きな問題にはならないはずです。

最後に

以上が僕の提案でした。既存のプロジェクトではおそらく取り入れにくいと思いますし、そこまで工数をかけなくても良いところだと思いますが、新規プロジェクトではぜひ参考にしていただけると嬉しいです。

ここまで読んでいただきありがとうございました。

コメントやフィードバックをお待ちしております!

Finatext Tech Blog

Discussion