メインコンテンツにスキップ

Goのレスポンス生成、複雑にしすぎてませんか?YAGNI原則で劇的に改善する方法

はじめに:善意から生まれた「複雑すぎる」コード

「詳細なデバッグ情報があったほうが良い」「パフォーマンスも計測できるようにしよう」「将来の拡張性も考慮して…」

こうした開発者の善意や先見の明が、時としてコードを複雑怪奇なモンスターに育て上げてしまうことがあります。今回の物語は、まさにそのような経緯で生まれた、243行にも及ぶ「多機能すぎる」レスポンス生成処理を、設計原則に則って劇的に簡素化したリファクタリングの記録です。

問題のコード:メタデータ満載、243行のモンスター関数

問題のコードは、APIレスポンスを生成するためだけのパッケージでした。しかし、その内部は過剰な機能で肥大化していました。

// ❌ 悪い例:pkg/utils/response.go(243行の複雑な実装)
package utils

// ... (6つの複雑な構造体定義) ...
type EnhancedAPIResponse struct {
    Message   string                 `json:"message"`
    Data      interface{}            `json:"data,omitempty"`
    Error     string                 `json:"error,omitempty"`
    Metadata  ResponseMetadata       `json:"metadata"` // 誰が使う?
    // ...
}

type ResponseMetadata struct {
    StatusCode     int               `json:"status_code"`
    ProcessingTime float64           `json:"processing_time_ms"` // 本当に必要?
    ServerInfo     ServerInfo        `json:"server_info"`
    DataValidation ValidationInfo     `json:"data_validation"`
    Performance    PerformanceInfo   `json:"performance"`
}

// ... (さらに4つのメタデータ用構造体) ...

// メイン関数(100行以上の複雑な処理)
func CreateEnhancedResponse(statusCode int, message string, data interface{}, err string) events.APIGatewayProxyResponse {
    // 1. 複雑なメタデータ構築
    // 2. reflectパッケージを駆使した過剰なデータ検証
    // 3. 詳細すぎるログを6回も出力
    // 4. 複雑なフォールバック処理
    // 5. パフォーマンス情報を埋め込むためにレスポンスを再マーシャリング(!!)
    // 6. 10個以上のHTTPヘッダーを設定
    // ...
    return events.APIGatewayProxyResponse{ /* ... */ }
}

なぜこのコードは「悪」なのか。

問題点詳細
パフォーマンス1回のレスポンス生成に50-70msもかかっていました。反射処理、過剰なログ、再マーシャリングなどが原因で、本来のビジネスロジックよりも遅いという本末転倒な事態に。
過剰な機能フロントエンドで実際に使われていたのはmessage, data, errorの3つだけ。苦労して実装したメタデータは誰にも使われていませんでした
保守性の低さコードが長大で複雑なため、少し修正するだけでも全体を理解する必要があり、デバッグも困難。まさに「触りたくないコード」の典型です。
テストの複雑さ50行以上のアサーションが必要な、複雑で実行の遅いテストになっていました。

解決策:YAGNI原則「本当に必要なものだけを作る」

この問題を解決する指針は、YAGNI (You Aren’t Gonna Need It) 原則です。「将来必要になるかもしれない」という予測に基づいて機能を実装するのではなく、「今、本当に必要なものだけを実装する」という考え方です。

Step 1: 本当に必要な機能を特定する

まず、このレスポンス生成処理に本当に必要な機能を洗い出しました。

  • HTTPステータスコード
  • メッセージ (message)
  • 成功時のデータ (data)
  • エラー時の詳細 (error)
  • 基本的なHTTPヘッダー (Content-Typeなど)

結論:メタデータ、パフォーマンス計測、過剰な検証はすべて不要。

Step 2: シンプルな構造体と基本関数を設計する

不要なものをすべて削ぎ落とし、必要最小限の構造体と基本関数を再設計しました。

// ✅ 改善案:シンプルで必要十分な実装
package utils

import (
	"encoding/json"
	"net/http"

	"github.com/aws/aws-lambda-go/events"
)

// シンプルなレスポンス構造
type APIResponse struct {
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
	Error   string      `json:"error,omitempty"`
}

// CreateResponseはレスポンス生成の基本となるシンプルな関数
func CreateResponse(statusCode int, message string, data interface{}, errStr string) events.APIGatewayProxyResponse {
	response := APIResponse{
		Message: message,
		Data:    data,
		Error:   errStr,
	}

	body, marshalErr := json.Marshal(response)
	if marshalErr != nil {
		// シンプルなフォールバック
		statusCode = http.StatusInternalServerError
		body = []byte(`{"message":"Response serialization failed","error":"JSON marshal error"}`)
	}

	return events.APIGatewayProxyResponse{
		StatusCode: statusCode,
		Headers:    map[string]string{"Content-Type": "application/json"},
		Body:       string(body),
	}
}

Step 3: 開発者体験(DX)を向上させるヘルパー関数を追加する

基本関数をラップし、よく使うパターンのための便利なヘルパー関数を用意します。これにより、APIハンドラー側のコードがより簡潔で読みやすくなります。

// ✅ 使いやすいヘルパー関数群
func CreateSuccessResponse(message string, data interface{}) events.APIGatewayProxyResponse {
	return CreateResponse(http.StatusOK, message, data, "")
}

func CreateErrorResponse(statusCode int, message string, err string) events.APIGatewayProxyResponse {
	if statusCode < 400 {
		statusCode = http.StatusInternalServerError
	}
	return CreateResponse(statusCode, message, nil, err)
}

func CreateNotFoundResponse(message string) events.APIGatewayProxyResponse {
	return CreateErrorResponse(http.StatusNotFound, message, "Resource not found")
}

// ... 他にも `CreateValidationErrorResponse` など、用途に応じたヘルパーを用意

Step 4: 簡潔になったハンドラーの実装

ヘルパー関数のおかげで、APIハンドラーのコードは驚くほどシンプルになります。

// ✅ 改善後のハンドラー:意図が明確で簡潔
func handleGetUser(userID string) events.APIGatewayProxyResponse {
	user, err := getUserByID(userID)
	if err != nil {
		if errors.Is(err, ErrUserNotFound) {
			return utils.CreateNotFoundResponse("User not found") // 1行でエラーレスポンス
		}
		return utils.CreateErrorResponse(500, "Failed to retrieve user", err.Error())
	}
	return utils.CreateSuccessResponse("User retrieved successfully", user) // 1行で成功レスポンス
}

劇的な改善効果:数値で見るシンプルさの価値

項目Before (複雑)After (シンプル)改善率
パフォーマンス50,000 ns/op1,200 ns/op42倍 高速化
コード量243行110行52% 削減
メモリ使用量1.5 MB/op0.1 MB/op93% 削減
テストコード50行以上15行70% 削減

まとめ:「完璧さ」とは、何も削れなくなった状態である

完璧とは、これ以上付け加えるものがなくなった時ではなく、これ以上削るものがなくなった時に達成される。 ― アントワーヌ・ド・サン=テグジュペリ

今回のリファクタリングは、まさにこの言葉を体現するものでした。機能を「加える」のではなく、不要なものを「削る」ことで、コードはより速く、より軽く、より強くなったのです。

シンプル設計のためのチェックリスト

  • YAGNI: その機能は「今」本当に必要か。「将来使うかも」という予測で作っていないか。
  • KISS (Keep It Simple, Stupid): もっと単純な実装方法はないか。標準ライブラリで解決できないか。
  • No Reflection: パフォーマンスが重要な箇所で、安易にreflectパッケージを使っていないか。
  • Measure, Don’t Guess: パフォーマンス改善は、推測ではなく計測に基づいて行っているか。

複雑さに立ち向かう最良の武器は、いつだってシンプルさです。