パッケージ分割とインポート整理レビュー:レビューアー視点の設計評価基準

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しない。
    • servicedomaininfra/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してるだけだから問題ない」という油断はレビューアーには禁物です。パッケージ構造は常に設計意図の反映物として読解してください。