この記事は カオナビ Advent Calendar 2023(シーズン2) 11日目です。
最近はinternalパッケージを使用した開発をしているので、どのような構成でやっているかを書きます。
internalパッケージ
Goでは先頭を大文字にした関数・interface等はどこからでも参照できます。
ただモジュール開発などをしていると内部で共通化のためにパッケージを切り出したいが、外部から参照はさせたくないということはよくあります。
そこで以下のようにinternalパッケージを使用することで、配下パッケージの実装を外部パッケージから参照できなくすることができます。
以下の構成の場合はa/b/b.go
からはa/b/internal
を参照できますが、
a/a.go
からはinternalパッケージ配下は参照できなくなります。
a/
├ b/
│ ├ internal/
│ │ ├ d/
│ │ │ └ d.go
│ │ └ c.go
│ └ b.go
└ a.go
過去のプロジェクト構成
当初はPackage by Layerと言われるパターンになっており
以下のようなcontroller等のレイヤーでパッケージを切って開発が行われていました。
./
├ controller/
│ ├ company.go
│ └ user.go
├ model/
│ ├ company.go
│ └ user.go
└ service/
├ company.go
└ user.go
この構成は極端ですがパッケージは以下に全ファイルが配置されるので、時間経過とともに全体の見通しが悪くなってしまいます。
以下のように更に機能ごとのパッケージを作成して進めることもできますが、
これだとパッケージ名の重複が頻発して結構開発しにくいなーと感じました。
./
├ controller/
│ ├ company/
│ │ └ create.go
│ └ user/
│ ├ create.go
│ └ update.go
├ model/
│ ├ company.go
│ └ user.go
└ service/
├ company/
│ └ create.go
└ user/
├ create.go
└ update.go
internalパッケージを使った構成
以下がinternalパッケージを使った構成イメージになります。
./
├ company/
│ ├ public.go
│ └ internal/
│ ├ controller/
│ │ └ controller.go
│ ├ model/
│ │ └ company.go
│ └ service/
│ └ service.go
├ core/
│ └ io
│ └ io.go
└ user/
├ public.go
└ internal/
├ controller/
│ └ controller.go
├ model/
│ ├ company.go
│ └ user.go
└ service/
└ service.go
各機能単位でパッケージを作成し、直下にinternalパッケージを配置することで
各機能の内部実装を他のパッケージから隠蔽して開発ができます。
この構成で例えばAPIを作る場合、最低でもControllerのinterfaceはパッケージ外に公開する必要があるため、Controllerのinterfaceを返す関数をpublic.goに実装します。
もし機能間で共通利用したい関数があればcoreのように別パッケージに切り出します。
メリット
各機能が独立
companyとuserの実装は独立するので、各internalパッケージ内の変更は他パッケージに影響を与える事はありません。
interface名等を簡潔にできる(かも)
レイヤー単位でパッケージを切る場合、service.CampanyCreate
や service.UserCreate
のように
重複回避と、機能を識別のためprefix(もしくはsuffix)をつける必要があると思います。
internalを使用することで各機能が独立するため、他のパッケージとの重複を考慮する必要がないためservice.Create
のような命名で良くなります。
IDEで余計なものが補完されない
userの機能実装中はuserから使用可能な実装のみが候補に上がるので快適
IDEでディレクトリツリーのスクロールが少なく済む
レイヤーごとに分けられていると、開発が進んでファイル数が多くなった状態でcontrollerからserviceパッケージに移動するときにそこそこスクロールが必要な場合があります。
基本的にinternal配下に必要なコードがまとまっているので、そんなにスクロールしなくてもコードの全体を見通せます
デメリット
各パッケージに重複コードが生まれる
例えばcampanyの存在チェック処理のようなものがcanmapy, userの両パッケージに実装されるなど。
これに関しては以下のように進めています。
- パッケージ間でのコード重複は許容する
- ある程度進めながら、本当に共通にすべきと判断したら共通化する
パッケージ間で実装方法が異なる場合がある
パッケージの切り方や、interfaceの切り方など、実装者によって多少ブレが発生する場合がある。
これに関しては以下のように進めています。
- パッケージに閉じていれば多少の違いは許容する
- パッケージに閉じていれば程度コードの全体把握はできる