Goのパッケージ分割とimport整理レビュー:依存設計と責務分離の可視化
パッケージ分割とインポート整理レビュー:レビューアー視点の設計評価基準
Goではパッケージ構成が「論理設計」と「物理設計」を兼ねます。import文が単なるライブラリ参照ではなく、設計の依存方向を宣言しているという意識が重要です。
レビューアーは import
の並びを読むのではなく、パッケージの責務境界と依存構造が妥当か?を読み取る必要があります。
この記事では、良い実装例からダメな実装例までを通じて、レビュー観点を深堀りします。
良い実装例:パッケージ境界と依存構造が明示された構成
まずは理想的な構成例から確認します。
// domain/user.go
package domain
type User struct {
ID string
Name string
}
// service/user_service.go
package service
import (
"context"
"myapp/domain"
"myapp/infra/logger"
)
type UserService struct {
repo UserRepository
logger logger.Logger
}
func (s *UserService) CreateUser(ctx context.Context, user domain.User) error {
s.logger.Info("creating user")
return s.repo.Save(ctx, user)
}
// infra/logger/logger.go
package logger
type Logger interface {
Info(msg string)
Error(msg string)
}
// infra/logger/zerolog.go
package logger
import "github.com/rs/zerolog"
type ZeroLogger struct {
log zerolog.Logger
}
func (z *ZeroLogger) Info(msg string) {
z.log.Info().Msg(msg)
}
func (z *ZeroLogger) Error(msg string) {
z.log.Error().Msg(msg)
}
// main.go
package main
import (
"context"
"myapp/domain"
"myapp/infra/logger"
"myapp/infra/repo"
"myapp/service"
)
func main() {
zerologger := logger.NewZeroLogger()
userRepo := repo.NewUserRepository()
userService := service.UserService{
repo: userRepo,
logger: zerologger,
}
user := domain.User{ID: "u1", Name: "Alice"}
userService.CreateUser(context.Background(), user)
}
技術的な設計ポイント解説
- 依存方向が整理されている
domain
は純粋なモデルのみ。どこもimportしない。service
はdomain
とinfra/logger
へ依存。責務に応じたインターフェース設計。infra
は具体実装のみ提供し、上位層に依存しない。
- interfaceでの依存反転が徹底されている
- loggerもrepositoryもinterfaceで注入され、infra実装は差し替え可能。
- Go Modulesでも適切に単一モジュール内に収まっている
- 複数のgo.mod管理を必要としない合理的な分割。
インフラ層の具体実装(zerologなど)はinfraパッケージ内に隔離し、サービス層に生のライブラリを漏らさない構成がレビューでは高評価されます。
良くない実装例
以下に典型的な設計不備のある構成例と、それに対するレビュー指摘コメントを提示します。
パターン1:インフラ依存の逆流
// domain/user.go
package domain
import "myapp/infra/logger"
type User struct {
ID string
}
func (u *User) Create() {
logger.Info("creating user")
}
@Reviewerドメイン層がinfra/loggerに依存しており、依存方向が逆転しています。ドメイン層は純粋なビジネスロジックに限定し、副作用は上位層で処理させるように責務を整理してください。
パターン2:汎用パッケージの肥大化
// util/common.go
package util
import (
"fmt"
"time"
)
func Log(msg string) {
fmt.Println(msg)
}
func FormatDate(t time.Time) string {
return t.Format("2006-01-02")
}
@Reviewer汎用的な`util`パッケージは責務が曖昧になりやすいため、機能別に分割しましょう。ログ処理はloggerパッケージ、日付整形はtimeutilなど専用化が望ましいです。
パターン3:循環参照寸前の結合
// domain/user.go
package domain
type User struct {
ID string
Name string
}
func (u *User) Validate() bool {
return u.Name != ""
}
// service/user_service.go
package service
import "myapp/domain"
type UserService struct{}
func (s *UserService) Register(user domain.User) bool {
if !user.Validate() {
return false
}
// 保存処理略
return true
}
@Reviewerサービス層がドメイン層の内部バリデーションに依存し始めています。バリデーションはどの層の責務かを明確化し、共通バリデーションとして切り出す方が疎結合性を保てます。
パターン4:Go Modules管理の曖昧さ
// go.mod (myapp)
module github.com/example/myapp
replace github.com/example/mycore => ../mycore
@Reviewerこの`replace`が一時的ローカル開発用か、恒久的な構造設計なのか不明確です。意図を明文化してください。継続利用するならドキュメント整備を推奨します。
レビュー観点チェックリスト
観点 | チェックポイント |
---|---|
依存方向 | 下層→上層の逆流がないか? |
責務分離 | ドメイン・サービス・インフラの役割は明確か? |
汎用パッケージ | util/commonなど肥大していないか? |
インターフェース抽象 | 依存反転が適切に行われているか? |
循環参照 | 暗黙的に循環しそうな型依存が発生していないか? |
Go Modules設計 | replaceの目的が明文化されているか? |
総括
パッケージ分割とimport整理のレビューは、「依存方向の健全性を監視する設計レビュー」と捉えるのが実務的です。
レビューアーはimport文の並びから、責務・疎結合・保守性の意図を読み取り、構造の崩壊を早期に発見していく役割を担います。
「importしてるだけだから問題ない」という油断はレビューアーには禁物です。パッケージ構造は常に設計意図の反映物として読解してください。