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

第9章:SRPの分割パターン①:入力・判断・出力を分ける 📥🧠📤✨

この章はひとことで言うと… **「I/O(入出力)とロジック(判断)を分離して、読みやすく&テストしやすくする」**回だよ〜!😊💕


1) 今日できるようになること 🎯✨

  • 「入力・判断・出力」がごちゃ混ぜになってるコードを見つけられる👀💥
  • 判断ロジックを “副作用なし(Pure)” の形に抜き出せる🧼✨
  • 抜き出した判断ロジックを 単体テスト できるようになる🧪✅
  • 変更に強くなる(UI変更・表示変更・ルール変更がぶつからない)💪🌈

※ちなみにC# 14 は .NET 10 でサポートされていて、Visual Studio 2026 ならそのまま使えるよ〜🧡 (Microsoft Learn) .NET 10 は LTS で、3年サポート(〜2028/11/10)だよ📅✨ (Microsoft for Developers)


2) なんで分けるの?(SRPの“変更理由”で考える)🧠💡

入力・判断・出力のパイプライン

「入力・判断・出力」が1つのメソッドに混ざると、こうなるよ〜😇💦

  • 画面の文言を変えたい → ロジックの中まで触って事故る💥
  • 送料ルールが変わった → 表示コードも一緒に触る羽目😵‍💫
  • テストしたい → Console 依存で テストしにくい&遅い🐢💦

だから、SRPの考え方でこう分けるのが鉄板💎✨

  • 入力(Input):外から情報をもらう(Console / API / 画面 / ファイル…)📥
  • 判断(Decision):業務ルールで計算・判定する(できればPure)🧠
  • 出力(Output):結果を外へ出す(Console / 画面 / APIレスポンス…)📤

変更理由もキレイに分かれるよ〜😊✨

  • UI変更→入力/出力だけ
  • ルール変更→判断だけ
  • 表示形式変更→出力だけ

3) 見分けるコツ:副作用(Side Effect)を探す🔎⚡

判断ロジックから 追い出したいもの はこれ👇(ほぼI/Oだと思ってOK)

  • Console読み書き🖥️
  • DB/ファイル/ネットワークアクセス🌐
  • 現在時刻(DateTime.Now)や乱数🎲
  • 環境変数・設定読み込み⚙️

判断はできるだけ 「引数 → 計算 → 戻り値」 にするのがコツだよ🧼✨


4) まずは“混ざったコード”😈🧱(Before)

例:ミニECの「送料+合計」をConsoleで計算するやつ🛒✨ (この段階はわざと混ぜます!)

using System;
using System.Globalization;

Console.WriteLine("🛒 ミニEC:送料込み合計を計算するよ!");
Console.Write("小計(円)を入力してね:");
var subtotalText = Console.ReadLine();

Console.Write("商品点数を入力してね:");
var itemCountText = Console.ReadLine();

Console.Write("配送先は国内?海外? (JP/INT):");
var destText = (Console.ReadLine() ?? "").Trim().ToUpperInvariant();

if (!decimal.TryParse(subtotalText, NumberStyles.Number, CultureInfo.CurrentCulture, out var subtotal) ||
!int.TryParse(itemCountText, out var itemCount) ||
itemCount <= 0 ||
(destText != "JP" && destText != "INT"))
{
Console.WriteLine("入力が変だよ…😢");
return;
}

decimal shippingFee = 0;

if (destText == "JP")
{
shippingFee = subtotal >= 5000m ? 0m : 500m;
}
else
{
shippingFee = 2000m + (200m * itemCount);
if (subtotal >= 12000m) shippingFee = 0m;
}

var total = subtotal + shippingFee;

Console.WriteLine($"送料:{shippingFee:N0}円");
Console.WriteLine($"合計:{total:N0}円");
Console.WriteLine("完了✅");

どこが「入力・判断・出力」?🧩

  • 入力:ReadLineしてるところ📥
  • 判断:送料計算の if/switch 🧠
  • 出力:WriteLineしてるところ📤 全部ひとつに詰まってるのが問題〜!😵‍💫💦

5) 分割していこう!🧹✨(After)

ステップA:判断の材料を「データ」にまとめる📦

入力で集めた値を、判断へ渡す形にするよ😊

public enum Destination
{
Japan,
International
}

public readonly record struct OrderRequest(decimal Subtotal, int ItemCount, Destination Destination);
public readonly record struct OrderResult(decimal ShippingFee, decimal Total);

ステップB:「判断」だけの計算機を作る🧠✨(Console禁止!)

ここが超重要! Console 1行も書かないのがポイントだよ🙅‍♀️🖥️

public static class ShippingFeeCalculator
{
public static decimal Calculate(in OrderRequest order)
{
if (order.ItemCount <= 0)
throw new ArgumentOutOfRangeException(nameof(order.ItemCount));

return order.Destination switch
{
Destination.Japan => order.Subtotal >= 5000m ? 0m : 500m,
Destination.International => InternationalFee(order.Subtotal, order.ItemCount),
_ => throw new ArgumentOutOfRangeException(nameof(order.Destination))
};
}

private static decimal InternationalFee(decimal subtotal, int itemCount)
=> subtotal >= 12000m ? 0m : 2000m + 200m * itemCount;
}

public static class OrderPricing
{
public static OrderResult Calculate(in OrderRequest order)
{
var shipping = ShippingFeeCalculator.Calculate(order);
return new OrderResult(shipping, order.Subtotal + shipping);
}
}

ステップC:「入力」を専用にする📥✨

(入力の細かいバリデーション整理は次章でガッツリやるから、ここでは軽めでOK👌)

using System.Globalization;

public static class ConsoleOrderInput
{
public static bool TryRead(out OrderRequest order)
{
Console.Write("小計(円):");
var subtotalText = Console.ReadLine();

Console.Write("商品点数:");
var itemCountText = Console.ReadLine();

Console.Write("配送先 (JP/INT):");
var destText = (Console.ReadLine() ?? "").Trim().ToUpperInvariant();

if (!decimal.TryParse(subtotalText, NumberStyles.Number, CultureInfo.CurrentCulture, out var subtotal))
{
order = default;
return false;
}

if (!int.TryParse(itemCountText, out var itemCount) || itemCount <= 0)
{
order = default;
return false;
}

Destination? destination = destText switch
{
"JP" => Destination.Japan,
"INT" => Destination.International,
_ => null
};

if (destination is null)
{
order = default;
return false;
}

order = new OrderRequest(subtotal, itemCount, destination.Value);
return true;
}
}

ステップD:「出力」を専用にする📤✨

public static class ConsoleOrderOutput
{
public static void Show(in OrderResult result)
{
Console.WriteLine();
Console.WriteLine($"送料:{result.ShippingFee:N0}円");
Console.WriteLine($"合計:{result.Total:N0}円");
}
}

仕上げ:Mainは“つなぐだけ”🤝✨

Main は薄〜く!これが気持ちいい💖

Console.WriteLine("🛒 ミニEC:送料込み合計を計算するよ!");

if (!ConsoleOrderInput.TryRead(out var order))
{
Console.WriteLine("入力が変だよ…😢");
return;
}

var result = OrderPricing.Calculate(order);
ConsoleOrderOutput.Show(result);

Console.WriteLine("完了✅");

6) テストが爆速で書ける!🧪🚀

判断がPureになったから、Consoleなしでテストできるよ〜!最高!🥳💕

using Xunit;

public class ShippingFeeCalculatorTests
{
[Fact]
public void Japan_Subtotal5000OrMore_IsFree()
{
var order = new OrderRequest(5000m, 1, Destination.Japan);
Assert.Equal(0m, ShippingFeeCalculator.Calculate(order));
}

[Fact]
public void Japan_SubtotalBelow5000_Is500()
{
var order = new OrderRequest(4999m, 1, Destination.Japan);
Assert.Equal(500m, ShippingFeeCalculator.Calculate(order));
}

[Fact]
public void International_Default_IsBasePlusPerItem()
{
var order = new OrderRequest(1000m, 3, Destination.International);
Assert.Equal(2600m, ShippingFeeCalculator.Calculate(order)); // 2000 + 200*3
}

[Fact]
public void International_Subtotal12000OrMore_IsFree()
{
var order = new OrderRequest(12000m, 10, Destination.International);
Assert.Equal(0m, ShippingFeeCalculator.Calculate(order));
}
}

7) 🤖AI(Copilot/Codex系)に頼むときの“いい聞き方”例💬✨

コピペで使えるよ〜😊💕

  • 「このメソッドを 入力/判断/出力 に分割して。判断は 副作用ゼロ にして」🧠
  • 「判断部分だけを 純粋関数 にして、引数と戻り値の形を提案して」🧼
  • 「今の送料計算の 境界値テスト を xUnit で列挙して」🧪
  • 「UI変更(文言・表示形式)が来てもロジックが壊れない構成にして」📤✨

※出てきたコードはそのまま信じず、テスト通して確認ね✅😺


8) できてるかチェック✅🧡(超大事)

  • 判断ロジックのクラス/メソッドに Console / HTTP / DB が出てこない?🙅‍♀️
  • 判断が「引数 → 戻り値」になってる?🧠➡️📤
  • 送料ルール変更が来たとき、判断だけ直せる?🔧
  • 表示変更が来たとき、出力だけ直せる?🎨
  • 入力方式がConsole→APIに変わっても、判断は使い回せる?🔁✨

9) 練習問題🎓✨(手を動かすやつ!)

問1:出力の変更📤

「送料:xxx円」を「Shipping Fee: xxx JPY」にしたい! 👉 どのファイルだけ直せばいい?(答え:出力だけになるのが理想🩷)

問2:入力の変更📥

配送先を「JP/INT」じゃなくて「1/2」で選ばせたい! 👉 判断に手を入れずに、入力だけで対応できる?😊

問3:ルール追加🧠

「国内で小計が3000円未満なら手数料100円追加」を入れてみて! 👉 変更が判断だけで済んだら勝ち🏆✨(テストも追加してね🧪)


まとめ🧾✨

  • SRPは「1クラス1機能」じゃなくて、1つの変更理由にまとめる感覚だよ😊
  • まずは鉄板の分割:入力📥 / 判断🧠 / 出力📤
  • 判断をPureにすると、読める・壊れにくい・テストできるの三拍子🥳💕

次の第10章は、ここで軽く流した「検証(バリデーション)をどこに置く?」を、気持ちよく整理していくよ〜✅🧾✨