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

第51章:副作用のない関数 🧼✨

副作用のない関数

〜何度実行しても同じ結果が出る、テストしやすいロジック〜


0. 今日のゴール 🎯😊

この章を読み終えると、こんな状態になれます👇

  • 「副作用ってなに?」が説明できる 🧠
  • “計算だけする関数” と “保存や通知をする処理” を分けられる ✂️
  • ドメイン(ビジネスルール)を テストしやすい形 にできる ✅
  • AIにコードを生成させても、暴走しにくい形 を保てる 🤖🧷

1. 「副作用」ってなに?💥

ざっくり言うと…

関数を呼んだら、戻り値以外にも世界が変わっちゃうこと 🌍💫

たとえばこんなの👇(全部 “副作用” になりがち)

  • DBに保存する 🗄️
  • ファイルに書く 📄
  • ネットワーク通信する 🌐
  • メール送る ✉️
  • DateTime.Now を読む(時間は毎回変わる)⏰
  • Guid.NewGuid() を作る(毎回変わる)🆔
  • 乱数を使う 🎲
  • static 変数を更新する 🧨
  • Console.WriteLine で出力する 🖥️

「え、時間読むのも副作用なの?」って思うよね🙂 でも 同じ入力でも結果が変わる なら、テストが急にめんどくさくなるのです…😇


2. 副作用のない関数(=“純粋な関数”)ってなに?🫧

副作用のない関数は、こういう性質を持ちます👇

✅ ルール1:同じ入力 → 同じ出力

  • 何回呼んでも結果が同じ 😺

✅ ルール2:戻り値以外で世界を変えない

  • DB保存しない、ログ出さない、外部に触らない 🙅‍♀️

3. DDDでなぜ大事?🏰✨

DDDでは、いちばん守りたいのは ドメインのルール(利益が出る部分)だよね💎

そのドメインが…

  • DBや外部APIにべったり
  • 時刻や乱数に依存
  • ログ出しながら状態を変える

…みたいになると、ルールの検証(テスト)が地獄 になります🔥😵‍💫

逆に、ドメインを「計算だけ」に寄せると…

  • ユニットテストが秒で書ける ⚡✅
  • AIが生成しても壊れにくい 🧱🤖
  • 将来DBを変えても影響が少ない 🔁

っていう、1人開発で最強のメリットが出ます💪✨


4. ダメな例(副作用あり)→ 良い例(副作用なし)に直す 🛠️

4-1. ダメな例:割引計算が “今の時刻” に依存してる 😵

public static decimal CalcTotalWithCampaign(decimal subtotal)
{
// 19時以降は5%引き(例)
if (DateTime.Now.Hour >= 19)
return subtotal * 0.95m;

return subtotal;
}

これ、テストしようとすると… 「19時以降じゃないとテスト落ちる」みたいな 時間ガチャ が発生します🎰😇


4-2. 良い例:必要な情報を引数でもらう(純粋)✨

public static decimal CalcTotalWithCampaign(decimal subtotal, DateTime now)
{
if (now.Hour >= 19)
return subtotal * 0.95m;

return subtotal;
}

これならテストが超ラク👇💡

using Xunit;

public class CampaignTests
{
[Fact]
public void After19_Discounted()
{
var now = new DateTime(2026, 1, 2, 19, 0, 0);
var total = Calc.CalcTotalWithCampaign(1000m, now);
Assert.Equal(950m, total);
}

[Fact]
public void Before19_NotDiscounted()
{
var now = new DateTime(2026, 1, 2, 18, 59, 0);
var total = Calc.CalcTotalWithCampaign(1000m, now);
Assert.Equal(1000m, total);
}
}

public static class Calc
{
public static decimal CalcTotalWithCampaign(decimal subtotal, DateTime now)
{
if (now.Hour >= 19)
return subtotal * 0.95m;

return subtotal;
}
}

時間を“読む”のをやめて、時間を“受け取る” ようにするだけで勝ちです🏆🥳


5. DDDっぽくする:ドメインは「計算」、外側が「保存」🧅✨

5-1. ドメイン側(副作用なし)🫧

public sealed record OrderLine(string ItemName, int Quantity, decimal UnitPrice);

public sealed class Order
{
private readonly List<OrderLine> _lines = new();

public IReadOnlyList<OrderLine> Lines => _lines;

public void AddLine(OrderLine line)
{
if (line.Quantity <= 0) throw new ArgumentException("数量は1以上");
if (line.UnitPrice < 0) throw new ArgumentException("単価は0以上");
_lines.Add(line);
}

// ✅ 純粋ロジック:合計計算(副作用なし)
public decimal CalcSubtotal()
=> _lines.Sum(x => x.UnitPrice * x.Quantity);

// ✅ 純粋ロジック:ルールに従って割引計算(副作用なし)
public decimal CalcTotal(DateTime now)
{
var subtotal = CalcSubtotal();
var discounted = ApplyNightDiscount(subtotal, now);
return discounted;
}

private static decimal ApplyNightDiscount(decimal subtotal, DateTime now)
=> now.Hour >= 19 ? subtotal * 0.95m : subtotal;
}

ここにはDBもAPIも出てきません🙅‍♀️ だからテストが簡単で、AIに生成させても崩れにくいです🤖🧱✨


5-2. 外側(Application/Infrastructure)で副作用をやる 🌐🗄️

public interface IOrderRepository
{
Task SaveAsync(Order order, CancellationToken ct);
}

public sealed class PlaceOrderUseCase
{
private readonly IOrderRepository _repo;

public PlaceOrderUseCase(IOrderRepository repo)
{
_repo = repo;
}

public async Task<decimal> PlaceAsync(Order order, DateTime now, CancellationToken ct)
{
// ✅ まずは純粋計算
var total = order.CalcTotal(now);

// ✅ 最後に副作用(保存)
await _repo.SaveAsync(order, ct);

return total;
}
}

イメージはこれ👇🙂

  • 内側(ドメイン):計算・ルール 💎
  • 外側(アプリ/インフラ):保存・送信・表示 📦

6. よくある落とし穴あるある 😭⚠️

😵「ログ出すだけだし…」

ログも副作用です🪵 ドメインに入れたくなったら、外側でやる or イベントにするのが安全🙆‍♀️

😵「staticにキャッシュ置いちゃえ」

テストが壊れる原因になりがち🧨 (並列テスト、順番依存、環境依存…)

😵「Guid.NewGuid() をドメインで生成」

ID生成が必要なら「外側で作って渡す」か、方針を決めて一箇所に寄せると安心🆔✨


7. “副作用を追い出す” 3ステップ 🧹✨

DDD初心者でも、この手順だけ覚えればOKです😊

  1. その関数が触ってる外部要素をリストアップ

    • 時刻、乱数、DB、HTTP、ファイル、static、Console…📝
  2. 外部要素を “引数” にして渡す

    • DateTime.Nownow を引数へ ⏰➡️📦
  3. 保存や通知は “外側” に移す

    • ドメイン:計算
    • 外側:保存・送信・表示

8. ミニ演習 🧪✨(手を動かすと一気に分かるよ!)

演習A:送料計算を “純粋関数” にしてみよう 📦🚚

仕様👇

  • 小計が3000円以上なら送料無料
  • それ未満は送料500円
  • ただし「北海道」は送料+300円

まずは関数だけ作る(副作用なし)🎯

public static int CalcShippingFee(int subtotal, string prefecture)
{
if (subtotal >= 3000) return 0;

var fee = 500;
if (prefecture == "北海道") fee += 300;
return fee;
}

次にテストを3本書いてみてね✅😊 (送料無料/通常/北海道)


9. AIに頼むときのコツ(プロンプト例)🤖📝✨

AIは放っておくと、DBやログまで混ぜがちです😇 最初から “縛り” を書くと成功率が上がるよ!

✅ そのままコピペで使える指示テンプレ

次の仕様をC#で実装してください。
ただし「ドメインロジックは副作用なし(純粋関数)」にしてください。

- DateTime.Now / Guid.NewGuid / 乱数 / DBアクセス / HTTP / Console出力は禁止
- 必要な値(現在時刻など)は引数で受け取る
- まず純粋関数(計算)を書き、次にxUnitのユニットテストを3本書く
- コードは読みやすさ優先で、短すぎる省略はしない
仕様:<ここに仕様>

これ、CopilotでもCodex系でもめちゃ効きます💪😺✨


まとめ 🎀

  • 副作用のない関数は「同じ入力→同じ出力」で「世界を変えない」🫧
  • ドメインは 計算・ルール に寄せると、1人開発が超ラクになる✨
  • 時刻や乱数は 読むな、受け取れ ⏰➡️📦
  • 保存や通知は 外側 に追い出す 🗄️➡️🚪

次の章(第52章)では、さらにテストと相性が良い Resultパターン(例外じゃなく戻り値でエラーを扱う)に進むよ〜😊🧩