5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

株式会社カオナビAdvent Calendar 2023

Day 11

Goのinternalパッケージを使ったPackage by Featureを意識した構成

Last updated at Posted at 2023-12-10

この記事は カオナビ 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.CampanyCreateservice.UserCreate のように
重複回避と、機能を識別のためprefix(もしくはsuffix)をつける必要があると思います。

internalを使用することで各機能が独立するため、他のパッケージとの重複を考慮する必要がないためservice.Createのような命名で良くなります。

IDEで余計なものが補完されない

userの機能実装中はuserから使用可能な実装のみが候補に上がるので快適:open_hands:

IDEでディレクトリツリーのスクロールが少なく済む

レイヤーごとに分けられていると、開発が進んでファイル数が多くなった状態でcontrollerからserviceパッケージに移動するときにそこそこスクロールが必要な場合があります。
基本的にinternal配下に必要なコードがまとまっているので、そんなにスクロールしなくてもコードの全体を見通せます:open_hands:

デメリット

各パッケージに重複コードが生まれる

例えばcampanyの存在チェック処理のようなものがcanmapy, userの両パッケージに実装されるなど。

これに関しては以下のように進めています。

  • パッケージ間でのコード重複は許容する
  • ある程度進めながら、本当に共通にすべきと判断したら共通化する

パッケージ間で実装方法が異なる場合がある

パッケージの切り方や、interfaceの切り方など、実装者によって多少ブレが発生する場合がある。

これに関しては以下のように進めています。

  • パッケージに閉じていれば多少の違いは許容する
  • パッケージに閉じていれば程度コードの全体把握はできる

5
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?