Goの巨大ファイルをどう分割する?単一責任原則で600行のコードをリファクタリングする方法
はじめに:あなたのコードベースに「成長痛」はありませんか。
プロジェクトの初期段階ではクリーンだったはずのファイルが、機能追加を重ねるうちに、いつの間にか数百行を超える「巨大ファイル」に変貌していた…。これは、多くの開発者が経験する「成長痛」です。
1つのファイルにあらゆる機能が詰め込まれた巨大ファイルは。
- 認知負荷の増大:コードを理解するだけで一苦労
- 変更リスクの増大:1つの修正が予期せぬ副作用を生む
- チーム開発のボトルネック:コンフリクトが頻発し、開発速度が低下
など、多くの問題を引き起こします。
今回は、実際のプロダクションコードで発生した「600行超の巨大Cognitoクライアント」を例に、単一責任原則(Single Responsibility Principle, SRP) に基づいて、この巨大ファイルをクリーンで保守性の高い複数のファイルに分割する具体的なリファクタリング手法を解説します。
問題のコード:1つの構造体に集約された、あまりにも多くの「責任」
問題のclient.go
は、AWS Cognitoが提供する「認証」「パスワード管理」「ユーザー管理」「管理者機能」など、多岐にわたる機能をたった1つの構造体 CognitoClient
で実装していました。
// ❌ 悪い例:pkg/cognito/client.go(600行超)
package cognito
type CognitoClient struct {
// ...
}
// --- 認証の責任 ---
func (c *CognitoClient) SignUp(...) { /* ... */ }
func (c *CognitoClient) SignIn(...) { /* ... */ }
// --- パスワード管理の責任 ---
func (c *CognitoClient) ChangePassword(...) { /* ... */ }
func (c *CognitoClient) ForgotPassword(...) { /* ... */ }
// --- ユーザー管理の責任 ---
func (c *CognitoClient) GetUser(...) { /* ... */ }
func (c *CognitoClient) UpdateUser(...) { /* ... */ }
// --- 管理者機能の責任 ---
func (c *CognitoClient) AdminConfirmUser(...) { /* ... */ }
func (c *CognitoClient) AdminDeleteUser(...) { /* ... */ }
// --- 内部実装の責任 ---
func calculateSecretHash(...) { /* ... */ }
func isValidEmail(...) { /* ... */ }
なぜこの巨大ファイルは「悪」なのか。
単一責任原則 は、「クラス(構造体)を変更する理由は、1つでなければならない」と教えています。しかし、このCognitoClient
を変更する理由は無数に存在します。
- 認証フローが変われば、変更が必要。
- パスワードポリシーが変われば、変更が必要。
- ユーザー属性が変われば、変更が必要。
- 管理機能が追加されれば、変更が必要。
このように、複数の変更理由が同じファイルに集中することが、保守性を著しく低下させる根本原因です。
解決策:責任に基づき、ファイルを分割・再構築する
この問題を解決する鍵は、CognitoClient
が抱える責任を分離し、それぞれを独立したコンポーネントとして再設計することです。
Step 1: 責任の境界を見つける
まず、既存のメソッドを「何をするための機能か。」という観点でグループ化し、責任の境界線を引きます。
- 認証サービス: サインアップ、サインイン、トークンリフレッシュなど
- パスワードサービス: パスワード変更、リセットなど
- ユーザーサービス: ユーザー情報の取得、更新、削除など
- 管理者サービス: 管理者権限でのユーザー操作など
Step 2: 改善後のパッケージ構造を設計する
責任の境界に基づいて、新しいファイル構造を設計します。
// ✅ 改善後のパッケージ構造
pkg/cognito/
├── auth_service.go # 認証サービスの責任
├── password_service.go # パスワードサービスの責任
├── user_service.go # ユーザーサービスの責任
├── admin_service.go # 管理者サービスの責任
├── base.go # 全サービスで共有される共通基盤(クライアント、設定など)
└── manager.go # 全サービスを統合し、外部に単一の窓口を提供
Step 3: 共通基盤をbase.go
に抽出する
各サービスで共通して利用されるAWSクライアント、設定、ユーティリティ関数(SecretHash計算など)をbase.go
に抽出します。
// ✅ pkg/cognito/base.go
package cognito
// 共通設定
type Config struct { /* ... */ }
// 共通クライアント(各サービスに埋め込まれる)
type BaseClient struct {
client *cognitoidp.CognitoIdentityProvider
config *Config
}
// 共通ユーティリティ
func (b *BaseClient) CalculateSecretHash(username string) (string, error) { /* ... */ }
Step 4: 各責任を独立したサービスとして実装する
抽出した共通基盤を使い、各責任を独立したサービス(構造体)として実装します。
認証サービスの例:
// ✅ pkg/cognito/auth_service.go
package cognito
type AuthService struct {
*BaseClient // 共通基盤を埋め込む
}
func NewAuthService(baseClient *BaseClient) *AuthService {
return &AuthService{BaseClient: baseClient}
}
func (s *AuthService) SignUp(req *SignUpRequest) (*SignUpResult, error) {
// 認証に関するロジックのみを記述
}
func (s *AuthService) SignIn(req *SignInRequest) (*SignInResult, error) {
// 認証に関するロジックのみを記述
}
同様に、PasswordService
、UserService
、AdminService
も実装します。
Step 5: 全サービスを統合するManager
を作成する
分割した各サービスを統合し、利用側(APIハンドラーなど)に単一の便利な窓口を提供するManager
をmanager.go
に作成します。
// ✅ pkg/cognito/manager.go
package cognito
// CognitoManagerは全てのCognitoサービスを統合管理する
type CognitoManager struct {
Auth *AuthService
Password *PasswordService
User *UserService
Admin *AdminService
}
func NewCognitoManager(client *cognitoidp.CognitoIdentityProvider, config *Config) *CognitoManager {
baseClient := NewBaseClient(client, config)
return &CognitoManager{
Auth: NewAuthService(baseClient),
Password: NewPasswordService(baseClient),
User: NewUserService(baseClient),
Admin: NewAdminService(baseClient),
}
}
Step 6: Manager
を利用して機能を呼び出す
利用側は、CognitoManager
を通じて、目的のサービスに簡単にアクセスできます。
// ✅ 改善後の利用例
func main() {
cognitoMgr := cognito.NewCognitoManager(client, config)
// 認証機能を使いたい場合
signUpResult, err := cognitoMgr.Auth.SignUp(...)
// パスワード機能を使いたい場合
err = cognitoMgr.Password.ChangePassword(...)
}
リファクタリングがもたらす絶大な効果
項目 | Before (巨大ファイル) | After (分割後) |
---|---|---|
可読性 | 600行のコードをすべて読まないと全体像が掴めない。 | auth_service.go など、関心のあるファイルだけを読めば良い。 |
保守性 | パスワードポリシーの変更が、認証機能に影響を与えるリスクがあった。 | password_service.go の変更は、他のサービスに影響を与えない。 |
チーム開発 | 複数人が同じファイルを編集し、コンフリクトが頻発。 | 各担当者が異なるファイルを編集するため、コンフリクトが激減。 |
テスト | 300行を超える巨大なテストファイル。 | TestAuthService など、サービスごとに分離されたテストが書ける。 |
まとめ:コードの「整理整頓」で保守性の高い未来を
単一責任原則は、物理的な世界の「整理整頓」と同じです。道具(機能)を、その役割ごとに適正な道具箱(ファイル・構造体)に分けておくことで、必要なものをすぐに見つけられ、安全に使うことができます。
巨大ファイル化を防ぐためのチェックリスト
- この変更の「理由」は何か。: ファイルを修正する理由が複数あるなら、分割を検討しよう。
- このファイルは何をする場所か。: ファイルの責務を一言で説明できないなら、責務が多すぎるサイン。
- ファイルは200行を超えていないか。: 明確なルールはないが、1つのファイルが長くなりすぎたら危険信号。
- チームでコンフリクトが頻発していないか。: 特定のファイルで衝突が多発する場合、そのファイルの責務が大きすぎる可能性がある。