メインコンテンツまでスキップ

第11章:SRP実戦💪✨ サービス肥大化をスッキリさせる🧼🫧

今日は 「でっかくなりすぎた OrderService(サービスクラス)」 を、SRP(単一責務)で 読みやすく・直しやすく・怖くない 形にしていくよ〜😊🌸 (Visual Studio 2026 は .NET 10 と C# 14 をそのまま使える前提でOKだよ✨) (Microsoft Learn)


1. 今日のゴール🎯✨

章の終わりに、これができるようになろう😊👍

  • 「このクラス、責務いくつある…?」 を言葉にできる
  • ✅ 肥大化サービスを “役割ごとに小さく分ける” 手順がわかる
  • ✅ 変更が来ても 直す場所が局所化 されて「怖くない」状態になる🛡️✨

2. あるある💥「OrderService が全部やってる」問題😵‍💫

Before: messy closet labeled 'Fat Service'. After: neat organizer labeled 'Validator', 'Calculator', 'Repository'.

たとえばミニECで、最初はこうなる👇

  • 入力チェック(バリデーション)✅
  • 金額計算(税、送料、割引)💰
  • 決済API呼び出し💳
  • 配送ラベル作成📦
  • DB保存💾
  • メール送信📧
  • ログ出力🪵

ぜーんぶ OrderService が抱えると…

  • 😭 仕様追加のたびにOrderServiceが爆伸び
  • 😱 ちょっと直したら決済が壊れた(関係ないはずなのに!)
  • 🧪 テストが書きにくい(外部APIやDBが混ざりすぎ)

ここをSRPでスッキリさせるよ🧹✨


3. SRPのコツ再確認📌(超重要✨)

SRPのキモはこれ👇

「変更理由(変更したくなる理由)が1つ」になるようにする 🎯

「メソッドが1つ」じゃないよ〜!🙅‍♀️ “このクラスは何の都合で変わる?” を聞くのがコツだよ😊


4. 進め方(安全ルート)🛣️🧪✨

肥大化サービスを分割するときは、順番が超大事!💡

ステップ0:まず“壊してない”保証を作る🧪

  • いきなり分割しない!
  • 先に 最低限のテスト(キャラクタリゼーションテスト) を置くと安心😊

ステップ1:変更理由をメモする📝

  • 「この処理、何のため?」「何が変わったら直す?」を箇条書き

ステップ2:まず Extract Method(メソッド抽出)で塊を作る🧱

  • Visual Studio の Extract Method が強い💪 ショートカット:Ctrl + RCtrl + M (Microsoft Learn)

ステップ3:塊をクラスへ引っ越し🚚✨

  • “かたまり”ができたら、新しいクラスに移す
  • 型を別ファイルにするのは Move type to matching file が便利だよ🙌 (Microsoft Learn)

ステップ4:最後に OrderService を「段取り係」にする🎬

  • OrderService は オーケストレーター(流れだけ書く) にする✨

5. 実例:Before(肥大化OrderService)😈🧱

「わざとツラい」例ね😂(イメージ掴む用に短くしてるよ)

public sealed class OrderService
{
public async Task<Guid> PlaceOrderAsync(OrderRequest request)
{
// 1) validate
if (request.CustomerId == Guid.Empty) throw new ArgumentException("CustomerId is required");
if (request.Items.Count == 0) throw new ArgumentException("Items is empty");
if (request.Items.Any(i => i.Quantity <= 0)) throw new ArgumentException("Quantity must be positive");

// 2) calculate price
decimal subtotal = request.Items.Sum(i => i.UnitPrice * i.Quantity);
decimal tax = Math.Round(subtotal * 0.10m, 2);
decimal shippingFee = subtotal >= 5000 ? 0 : 500;
decimal total = subtotal + tax + shippingFee;

// 3) call payment gateway
var paymentResult = await PaymentGateway.ChargeAsync(request.PaymentToken, total);
if (!paymentResult.Success) throw new InvalidOperationException("Payment failed");

// 4) create shipment
var shipmentLabel = await ShippingApi.CreateLabelAsync(request.ShippingAddress, request.Items);

// 5) save to db
var orderId = Guid.NewGuid();
await Db.InsertOrderAsync(orderId, request.CustomerId, subtotal, tax, shippingFee, total, shipmentLabel.TrackingNumber);

// 6) send email
await EmailSender.SendAsync(
to: request.CustomerEmail,
subject: "Thanks for your order!",
body: $"OrderId={orderId}, Total={total}, Tracking={shipmentLabel.TrackingNumber}"
);

// 7) logging
Console.WriteLine($"Order placed: {orderId}");

return orderId;
}
}

これ、変更理由が多すぎ るよね😵‍💫 税率変わる、送料ルール変わる、決済API変わる、メール文言変わる、DB項目増える…全部ここ!💥


6. 変更理由を “見える化” しよう📝✨(ここがSRPの勝ち筋!)

まずはこう分けて考えるよ😊

かたまり何が変わったら直す?分けたくなるクラス名例
入力チェック入力ルール・制約OrderValidator
金額計算税率・送料・割引PricingCalculator
決済決済プロバイダ仕様PaymentProcessor
配送配送API・ラベル仕様ShippingCoordinator
永続化DBスキーマ・保存方法OrderRepository
通知メール/通知仕様OrderNotifier

この表が作れたら、ほぼ勝ち🏆✨


7. 🤖AI活用タイム(超おすすめ)✨

7.1 責務の洗い出し(SRP用プロンプト)🧠

Copilot / Codex にこれ👇

  • 「この PlaceOrderAsync の責務(変更理由)を箇条書きで10個出して。似てるものはグループ化して、クラス分割案も出して」

7.2 クラス名10個案(アウトラインのAIメモそのまま💖)

  • 「分割後のクラス候補名を10個提案して。役割も1行で添えて」

👉 “命名” はAIが得意だから、ここは頼ってOKだよ😊✨


8. リファクタ手順(実戦)🧼✨

8.1 まず Extract Method で「塊」を作る🧱

Visual Studioで、責務ごとに範囲選択 → Extract Method✨ ショートカット:Ctrl + RCtrl + M (Microsoft Learn)

おすすめの抽出メソッド名(例)👇

  • ValidateRequest(request)
  • CalculateTotals(request)
  • ChargePayment(request, total)
  • CreateShipmentLabel(request)
  • SaveOrder(...)
  • SendConfirmation(...)

ここで一回 コミット(または最低でも保存&テスト実行)すると安心だよ😊🫶


8.2 次に「塊」をクラスへ移す🚚✨

抽出したメソッドを、新しいクラスへ移すよ。 最初は “インターフェースなし” でもOK!(DIPは後の章でやるからね😉)

After(分割後のイメージ)🌈

public sealed class OrderService
{
private readonly OrderValidator _validator = new();
private readonly PricingCalculator _pricing = new();
private readonly PaymentProcessor _payment = new();
private readonly ShippingCoordinator _shipping = new();
private readonly OrderRepository _repository = new();
private readonly OrderNotifier _notifier = new();

public async Task<Guid> PlaceOrderAsync(OrderRequest request)
{
_validator.Validate(request);

var totals = _pricing.Calculate(request.Items);

await _payment.ChargeAsync(request.PaymentToken, totals.Total);

var label = await _shipping.CreateLabelAsync(request.ShippingAddress, request.Items);

var orderId = await _repository.SaveAsync(request, totals, label);

await _notifier.SendConfirmationAsync(request.CustomerEmail, orderId, totals, label);

return orderId;
}
}

見て〜!✨ OrderService が “流れ” だけ になって、めっちゃ読みやすい😊🎀


9. 新しくできたクラス(例)🧩✨

9.1 Validate担当✅(OrderValidator)

public sealed class OrderValidator
{
public void Validate(OrderRequest request)
{
if (request.CustomerId == Guid.Empty) throw new ArgumentException("CustomerId is required");
if (request.Items.Count == 0) throw new ArgumentException("Items is empty");
if (request.Items.Any(i => i.Quantity <= 0)) throw new ArgumentException("Quantity must be positive");
}
}

9.2 金額計算担当💰(PricingCalculator)

public sealed class PricingCalculator
{
public OrderTotals Calculate(IReadOnlyList<OrderItemRequest> items)
{
decimal subtotal = items.Sum(i => i.UnitPrice * i.Quantity);
decimal tax = Math.Round(subtotal * 0.10m, 2);
decimal shippingFee = subtotal >= 5000 ? 0 : 500;
return new OrderTotals(subtotal, tax, shippingFee);
}
}

public readonly record struct OrderTotals(decimal Subtotal, decimal Tax, decimal ShippingFee)
{
public decimal Total => Subtotal + Tax + ShippingFee;
}

9.3 別ファイル化の小ワザ📄✨

クラスが同一ファイルに増えてきたら、型名にカーソルを置いて Ctrl + .Move type to matching file が便利だよ🙌 (Microsoft Learn)


10. 「分割できた!」の判定チェック✅✨(SRPセルフテスト)

分割した各クラスに対して、これを聞いてみてね😊

  • 「このクラスが変更される理由って、1つに言える?」🎯
  • 「送料の仕様が変わったら、直すのは PricingCalculator だけ?」📦
  • 「メール文言変わったら、直すのは OrderNotifier だけ?」📧
  • 「決済会社を変えたら、直すのは PaymentProcessor だけ?」💳

“だけ” が言えるほどSRPが効いてる よ✨


11. ありがちな失敗😅💦(回避策つき!)

失敗1:細かくしすぎて迷子🥺

  • 1メソッド1クラス、みたいにやると逆につらいことあるよ〜 ✅ 回避:「変更理由が同じものは一緒」 にする

失敗2:引数が増えすぎて地獄📦📦📦

✅ 回避:OrderContext / PlaceOrderContext みたいな 1つの箱 にまとめる(入れ物を作る)✨

失敗3:OrderService がまだ“何でも屋”のまま😵‍💫

✅ 回避:OrderServiceの中に「計算式」「メール本文」「SQL」が残ってないかチェック👀


12. 演習(やってみよ〜!)🧪🎀

演習A:分割の第一歩(15分)⏳

  1. ValidateRequestCalculateTotals を Extract Method
  2. OrderValidatorPricingCalculator を作って移す
  3. OrderService が「流れだけ」になってるか確認✨

演習B:仕様追加でSRPの効果を体感😆💡

新要件:送料ルール変更

  • 「6000円以上で送料無料」に変える📦

👉 直すのが PricingCalculator だけなら勝ち〜!🏆✨


13. まとめ🌸✨

  • SRPは “変更理由を1つにする” 原則🎯

  • 肥大化サービスは、まず メソッド抽出 → 役割クラスへ移動 が安全🧼

  • Visual Studio のリファクタ(Extract Method / Rename / Move type)を使うと速いよ✨

  • そして、Visual Studio 2026 は .NET 10 / C# 14 を標準でサポート✨ (Microsoft Learn)

  • .NET 10 は LTS で、長く使えるのも安心材料だよ〜😊🫶 (Microsoft for Developers)


次の章(第12章)で、いよいよ OCP(拡張に開く/変更に閉じる) に入って「追加が来ても既存を壊しにくい」設計に進むよ🚪✨ その前に、いまの OrderService をあなたの題材(ミニEC or 本貸出)に合わせて、“あなたのコード版” の分割案も一緒に作れるよ😊💖