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

第42章:CQSの超入門(更新と参照を混ぜない)🔀🚫

この章でできるようになること🎯

画像を挿入予定

  • 「状態を変える処理」と「読むだけの処理」を分けて考えられるようになる🙆‍♀️
  • テストが 書きやすい・読みやすい 形に自然と寄っていく🧪📘
  • 「地味に怖いバグ(読んだだけのはずなのに状態が変わる)」を防げる😇💥

まずCQSってなに?🧠✨

CQS(Command Query Separation)は超ざっくり言うと👇

  • Command(コマンド):状態を変える(保存・更新・削除など)✍️🧱
  • Query(クエリ):状態を変えずに返す(検索・取得など)🔎📦

「質問しただけで答えが変わるのはナシね!」って発想だよ🙂


(Martin Fowlerの定義もほぼこれで、Queryは副作用なし、Commandは値を返さない、という整理です) (martinfowler.com)


なんでTDDと相性いいの?🧪💕

TDDって「テスト=仕様」だから、読み物として分かりやすいのが正義📘✨ CQSを守ると、こういう嬉しさが出るよ👇

1) テストがシンプルになる🧁

  • Queryのテスト:返り値だけ見ればOK(状態が変わらない前提)
  • Commandのテスト:状態変化だけ見ればOK(保存された?増えた?など)

2) “え、これ呼んだだけで…?”事故が減る😇

たとえば GetOrCreate() みたいに 「取得するつもりで呼んだのに、裏で作ってた」みたいなやつ…あるある😵‍💫

3) 命名が自然に良くなる📝

  • Add... / Update... / Delete... → Commandっぽい
  • Get... / Find... / Search... → Queryっぽい 読むだけで心が落ち着く☺️🍵

“混ざる”と何がツラいの?💥

😱 悪い例:Queryっぽい顔して、裏で保存してる

  • GetLatestSummary() が内部で「アクセス記録」保存しちゃう
  • FindById() が「見つからなかったら作る」
  • Search() が「キャッシュ更新」で状態を変える

こうなるとテストは👇みたいに地獄化しやすい🫠

  • 「返り値」も「保存された回数」も「時刻」も「ログ」も…ぜんぶ気になる
  • 失敗した時に「どこが仕様違反?」が分かりづらい

ハンズオン:推し活グッズ管理でCQSを体に入れる🎀📦🧪

ここでは小さく👇を作るよ!

  • Command:グッズ登録する Register(...)
  • Query:グッズ取得する GetById(...) / 検索 Search(...)

さらに大事なルール👇

  • QueryはSaveしない(副作用なし)
  • Commandは結果を返さない(基本)✅ (※現実の折衷案も後で紹介するよ😉)

Step 0:テストの土台(“Fake”で呼ばれ方を記録)🧸🧪

モックフレームワーク無しでもいけるように、手作りFakeでいこう✨ (もちろん後でMoqでもOK👌)

public readonly record struct GoodsId(Guid Value);

public sealed record Goods(GoodsId Id, string Name);

public interface IGoodsRepository
{
void Save(Goods goods);
Goods? FindById(GoodsId id);
IReadOnlyList<Goods> Search(string keyword);
}

// 呼ばれた回数を記録できる Fake
public sealed class FakeGoodsRepository : IGoodsRepository
{
private readonly Dictionary<GoodsId, Goods> _store = new();

public int SaveCallCount { get; private set; }

public void Save(Goods goods)
{
SaveCallCount++;
_store[goods.Id] = goods;
}

public Goods? FindById(GoodsId id)
=> _store.TryGetValue(id, out var g) ? g : null;

public IReadOnlyList<Goods> Search(string keyword)
=> _store.Values
.Where(g => g.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase))
.ToList();
}

Step 1:Commandのテストを書く(まずRed)🚦🧪

「登録したら保存される」だけを仕様にするよ🙂

using Xunit;

public sealed class GoodsCommandTests
{
[Fact]
public void Register_saves_goods_once()
{
var repo = new FakeGoodsRepository();
var service = new GoodsService(repo);

var id = new GoodsId(Guid.NewGuid());

service.Register(id, "アクスタ");

Assert.Equal(1, repo.SaveCallCount);
Assert.NotNull(repo.FindById(id));
}
}

Step 2:最小実装でGreen✅✨

public sealed class GoodsService
{
private readonly IGoodsRepository _repo;

public GoodsService(IGoodsRepository repo) => _repo = repo;

// Command:状態を変える(戻り値なし)
public void Register(GoodsId id, string name)
{
var goods = new Goods(id, name);
_repo.Save(goods);
}

// Query:この後追加するよ!
}

Step 3:Queryのテストを書く(“副作用なし”を固定)🔎🚫🧪

ここが第42章のキモ💡 Queryを呼んでもSaveされない をテストで“釘打ち”するよ🔨✨

public sealed class GoodsQueryTests
{
[Fact]
public void GetById_does_not_save_anything()
{
var repo = new FakeGoodsRepository();
var service = new GoodsService(repo);

var id = new GoodsId(Guid.NewGuid());
service.Register(id, "ぬい");

var saveCountBefore = repo.SaveCallCount;

var goods = service.GetById(id);

Assert.Equal(saveCountBefore, repo.SaveCallCount); // 増えない!
Assert.NotNull(goods);
Assert.Equal("ぬい", goods!.Name);
}
}

そして最小実装👇

public sealed partial class GoodsService
{
// Query:状態を変えない(読むだけ)
public Goods? GetById(GoodsId id)
=> _repo.FindById(id);

public IReadOnlyList<Goods> Search(string keyword)
=> _repo.Search(keyword);
}

✅ この時点で「QueryはSaveしない」が自動テストで守られるようになったよ〜!🎉🧪


“でもさ、Commandの結果ほしい時あるよね?”問題😵‍💫➡️🙂

あるある!めっちゃある! CQSの原理は「Commandは値を返さない」だけど、現実では👇が欲しくなること多い👇

  • 追加したIDが欲しい
  • 成功/失敗の情報が欲しい

おすすめのやり方(初心者向けに安全)🛡️

✅ 1) IDは呼ぶ側で作って渡す(さっきの方式)🎁

  • Commandは void
  • Queryであとから取る

✅ 2) “結果”だけ返す(データ取得はQueryで)📦

たとえば CommandResult みたいに 「成功した?」とか「エラーは?」みたいな情報だけ返すのは、実務ではよくやるよ🙂 (ただし、ここで 取得用データまで返し始める と混ざりやすいので注意⚠️)


よくある落とし穴(チェックリスト)✅🧠

❌ Queryの中でやりがち

  • ログのつもりでDB更新しちゃう📝💥
  • “ついでにキャッシュ更新”で状態変更しちゃう🧊💥
  • “見つからないなら作る”をやっちゃう😇💥

👉 まずは学習段階では Queryは完全に読むだけ に固定しちゃうのが一番ラク!🍀


CQSとCQRSの違い(混乱しがちなので1分で)⏱️🙂

  • CQS:メソッド(またはクラス)内で「更新」と「参照」を混ぜない原則🔀🚫
  • CQRS:それをさらに大きくして、読みモデル/書きモデルを分けるアーキテクチャ寄りの話🏗️✨(複雑になりやすいから慎重に、って注意も有名) (martinfowler.com)

第42章は CQSだけ でOK!CQRSはまだ早いよ〜🧸


AI(Copilot / Codex等)の使いどころ🤖✨

この章はAIがめっちゃ役立つよ!

  • 「このメソッド、Command?Query?どっち?」って判定してもらう
  • “混ざってる責務” を見つけてもらう
  • 命名案を3つ出してもらう(意外と効く📝)

使えるプロンプト例👇(コピペOK)

  • 「このメソッドが状態変更している可能性を指摘して。副作用候補も列挙して」
  • 「この処理をCQSに従ってCommand/Queryに分離する案を3つ。命名も」
  • 「Queryが副作用を持たないことを保証するテスト観点を列挙して」

仕上げ課題(ミニ)🎀🧪

次の “混ざりメソッド” をCQSに直して、テストも分けてね👇

  • 例:RegisterAndGetAll()

    • Register(Command)と GetAll(Query)に分割する
    • Query側で SaveCallCount が増えないテストを追加する

まとめ🎉

  • CQSは「変える」と「見る」を分けるだけ!…だけど効果デカい💥✨
  • TDDだと テストが読みやすく・壊れにくく なる🧪📘
  • まずは学習では Query=副作用ゼロ を徹底すると一気に上手くいくよ🍀

最新メモ(本日時点の参照情報)🗓️🔍

  • .NET 10 の最新は 10.0.2(2026-01-13) として案内されています。 (Microsoft)
  • Visual Studio 2026 は 18.2.0(2026-01-13) が履歴に載っています。 (Microsoft Learn)
  • xUnit v3 のリリース一覧では 3.2.2 が掲載されています。 (xUnit.net)
  • C# 14 は .NET 10 SDK / Visual Studio 2026 で試せる形で案内されています。 (Microsoft Learn)

次はこの流れで、**「混ざってる既存コード」をCQSに直す練習問題(実案件っぽい例)**も作れるよ🙂✨ いく?🧪🎀