ソフトウェア開発
2026/5/242 分で読める

欲張りコード」を捨てれば開発は楽になる:実戦から学ぶ単一責任の原則(SRP)の力

欲張りコード」を捨てれば開発は楽になる:実戦から学ぶ単一責任の原則(SRP)の力

長年、クラシックなモノリスシステムから複雑なマイクロサービス、モバイルアプリに至るまで、あらゆるプロジェクトに携わる中で、私はある苦い真実に行き着きました。本番環境で発生する重大なバグの大部分は、アルゴリズムの質の低さではなく、1つの場所にあまりにも多くの責任を詰め込みすぎていることに起因するということです。

私たちはしばしば、「利便性」という心理的な罠に陥ってしまいます。

「せっかくこのロジックハンドラーの中にいるんだから、ついでにログを数行追加して、外部サービスへのイベント発行もパパッと済ませてしまおう」

その結果はどうでしょうか。最初の2週間は完璧に動作します。しかし3ヶ月が経ち、ビジネスがスケールして変更要求が押し寄せる頃には、その「利便性」の代償としてコードはフランケンシュタインの怪物(God Object)へと変貌を遂げています。ここを修正すればあっちが壊れ、チームの誰もドミノ倒しを恐れてコードに触ろうとしなくなります。

本日、ただの「動くコードを書く人」と「本物のソフトウェアエンジニア」を分ける最も根源的な境界線である単一責任の原則(SRP: Single Responsibility Principle)について徹底的に解剖してみましょう。

1. 実践的な視点で捉えるSRP:教科書的な定義は忘れよう

ネットでSRPを調べると、教科書にはこう書かれています。 「クラスやモジュールを変更する理由は、常に1つだけでなければならない」

非常にアカデミックな響きですが、大規模なシステムを構築する際、私はより実践的にこう定義しています。 「関数やクラスは、正確に1つのビジネス境界(Context/Boundary)に対して責任を持ち、1つの特定の役者(Actor)のためだけに奉仕すべきである」

システムを「レストラン」に例えてみましょう。

  • シェフ(コアロジックを書く開発者): 美味しい料理を素早く作ることだけに集中する。

  • レジ係(データベース/会計): 会計処理を正確に行うことだけに集中する。

もしシェフが麺を炒めながら、急いでフロアに出てお客様のクレジットカードを決済し、「ついでに」ホウキを持って床掃除まで始めたらどうなるでしょうか。ピークタイム(高負荷時)を迎えた瞬間、システムは間違いなくクラッシュします。麺は焦げ付き、決済はミスを連発し、お客様は店を出て行ってしまうでしょう。

コードの振る舞いも、これと全く同じです。

2. 「未来のレガシーコード」の設計図

エンジニアのキャリアの中で、少なくとも3回は遭遇したことがあるであろう、典型的な注文処理関数を直接見てみましょう。

TypeScript

// 警告:これはオンコールでの徹夜を約束する片道切符です!
async function processOrder(order: any) {
  // 1. ビジネスロジックのバリデーション
  if (!order.items || order.items.length === 0) {
    throw new Error("Invalid order items");
  }

  // 2. 価格と税金の計算(コアロジック)
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.quantity;
  }
  if (order.vipCode) {
    total = total * 0.85; // VIP向け15%割引
  }

  // 3. データの永続化(データベース操作)
  const db = await getDatabaseConnection();
  await db.query("INSERT INTO orders ... VALUES ...", [order.id, total]);

  // 4. サイドエフェクト / 通知(外部連携)
  const emailClient = new EmailClient({ provider: "SendGrid" });
  await emailClient.send(order.email, "Order Confirmed", `Total: ${total}`);

  return { success: true, amount: total };
}

なぜこの関数がシニアエンジニアの視点から「地雷」に見えるのか?

  • 保守性の崩壊(Maintainability Disaster): ある日、ビジネスチームから「コスト最適化のためにSendGridをやめてAWS SESに切り替えよう」と言われ、この関数を開いて修正します。翌日、会計チームから「この商品の消費税の計算式を変更してほしい」と言われ、また全く同じ関数を開きます。単一の関数が多すぎるステークホルダーの利害を背負い込むと、デグレード(先祖返り)の発生率は跳ね上がります。

  • 息の詰まるユニットテスト(Suffocating Unit Tests): 「15%のVIP割引ロジックが正しく動作するか」をテストしたいだけなのに、データベース接続とメールクライアントの両方をモックせざるを得なくなります。テストコードを書く時間がコアロジックを書く時間の5倍もかかり、結果としてチームはユニットテストそのものを放棄することになります。

  • 再利用性ゼロ(Zero Reusability): もし別の画面(例:購入前のチェックアウトプレビュー)で「購入前に合計金額だけをプレビューしたい」という要望があった場合、この関数は使えません。なぜなら、呼び出した瞬間にDBへ挿入され、メールが送信されてしまうからです。結局、計算ロジックを別の場所にコピペすることになり、コードベースの肥大化と腐敗が始まります。

3. シニアが実践するリファクタリング:単一責任を取り戻すための「分割統治」

この問題を解決するには、責任のレイヤー(Layers of Responsibility)を抽出する必要があります。メインの関数をワークフローの制御のみを行う「オーケストレーター(Orchestrator)」へと変貌させ、専門的なタスクはそれぞれの専門家(サービス/リポジトリ)に委譲します。

TypeScript

// 1. ドメインサービス:価格計算のみを担当(副作用のない純粋関数ロジック)
class PricingEngine {
  public calculateTotal(items: OrderItem[], vipCode?: string): number {
    let total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    if (vipCode) {
      total = total * 0.85;
    }
    return total;
  }
}

// 2. データアクセスレイヤー:データベース通信のみを担当
class OrderRepository {
  public async save(orderId: string, totalAmount: number): Promise<void> {
    const db = await getDatabaseConnection();
    await db.query("INSERT INTO orders ...", [orderId, totalAmount]);
  }
}

// 3. インフラストラクチャサービス:サードパーティ連携(メールサーバー)のみを担当
class NotificationService {
  public async sendOrderConfirmation(email: string, amount: number): Promise<void> {
    const emailClient = new EmailClient({ provider: "AWS_SES" }); // AWSへの切り替えを想定
    await emailClient.send(email, "Order Confirmed", `Total: ${amount}`);
  }
}

クリーンアップされ、エンタープライズ対応となったコアワークフローの実行部分は以下の通りです。

TypeScript

// クリーンで明示的、エンタープライズ開発に適した形
class OrderApplicationService {
  constructor(
    private pricingEngine: PricingEngine,
    private orderRepo: OrderRepository,
    private notificationService: NotificationService
  ) {}

  public async execute(order: any): Promise<boolean> {
    // 1. エントリーポイントでの基本バリデーション
    if (!order.items || order.items.length === 0) return false;

    // 2. 専門家にタスクを委譲
    const totalAmount = this.pricingEngine.calculateTotal(order.items, order.vipCode);
    
    await this.orderRepo.save(order.id, totalAmount);
    
    await this.notificationService.sendOrderConfirmation(order.email, totalAmount);

    return true;
  }
}

4. クリーンさの代償:トレードオフは何か?

私は常にチームに伝えています。*「ソフトウェアアーキテクチャに銀の弾丸(特効薬)はない。すべての選択はトレードオフである」*と。

  • コスト: ファイル数が増えることに気づくでしょう。1つのファイルだったものが4つになります。上から下まで一息に読む代わりに、フローを追うために異なるクラスを行ったり来たりする必要があります。一見すると、小さなプロジェクトにとっては「オーバーエンジニアリング」のように見えるかもしれません。

  • ROI(投資対効果): しかし、プロジェクトが50や100の機能にまで膨れ上がったとき、システムの崩壊を防ぎ止めるのはまさにこの組織的構造です。

    • PricingEngine のユニットテストは、I/O(DB、ネットワーク)のボトルネックがない純粋関数(Pure Function)になったため、瞬時に(1ミリ秒未満で)実行でき、記述も非常に容易になります。

    • 将来的にインフラ(DBの変更やメールプロバイダの切り替えなど)を変更する必要が生じても、コアとなるビジネスロジックに影響を与えるリスクは完全にゼロになります。