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

24章:UseCase内の“読み書き”の考え方📚✍️

この章はね、UseCase(Interactor)が“手順書”としてキレイに育つコツをつかむ回だよ〜!🥰 特に「読み取り(Query)」と「書き込み(Command)」をゴチャ混ぜにしないと、あとから爆速で楽になる💨💖


1) この章のゴール🎯✨

  • UseCaseの中で 「読む」🧐 と「書く」✍️ を分けて設計できる
  • 必要最小のI/O(DBアクセス等)だけにして、UseCaseが太らない🍔❌
  • 「GetMemo」「UpdateMemo」を Ports/Models/Interactor で追加できる✅✨

2) まず結論💡:読み📚と書き✍️は“目的が別”だから分けて考える😍

Command vs Query

✅ 書き込み(Command)✍️

  • ルール(不変条件)を守って状態を変えるのが仕事💪

  • だから、だいたいこうなる👇

    1. Entityを取得
    2. Entityのメソッドで更新(=ルールが効く)
    3. 保存
    4. 結果をOutputPortへ

✅ 読み取り(Query)📚

  • 見たい情報を取り出すのが仕事👀✨
  • ルールで状態を変えないなら、**Entityを無理に作らず投影(Projection)**でOKな場面が多いよ👍
  • 例:一覧、検索、詳細表示など

現代の .NET は .NET 10 がLTSで、C# 14 もそこで動くよ〜🧁(Visual Studio 2026 と .NET 10 SDK で試せる) (Microsoft Learn) EF Core 10 も .NET 10 前提で、こちらもLTSだよ🗄️✨ (Microsoft Learn) ちなみに 2026/01/13 に .NET 10 の更新も出てるよ📦 (マイクロソフトサポート)


3) UseCase内のI/O(読み書き)で守る“3つの約束”🤝💖

約束①:UseCaseは「手順」に集中🧾✨

  • DBの都合(JOINとか最適化)を考え込みすぎない
  • ただし 必要な形で取れるように、Gateway/Repositoryのメソッドを“目的ベース”に設計するのはOK🙆‍♀️

約束②:I/Oは“必要最小回数”にする🔌

  • 例:Updateで同じIDを何回も取りに行かない
  • 例:一覧で1件ずつ追加取得(N+1地獄)を生みやすい形にしない💣

約束③:「読む用」「書く用」の出口を分ける(または分けられる形にする)🚪✨

  • 初心者のうちは 1つのRepositoryにまとめてもOK
  • でも、慣れてきたら Read用/Write用を分けると爆伸びする📈💕

4) 実装してみよ〜!🛠️✨(GetMemo / UpdateMemo)

ここでは「メモ帳アプリ」を前提に、UseCaseを2つ追加するよ📒💖 (第23章で作った Repository interface がある想定で進めるね!)


4-A) まずは “Read(GetMemo)” 📚✨

✅ 設計のコツ(Read)

  • 見せたい形(ReadModel)で返すのが気持ちいい😍
  • Domain Entity をそのまま返すと、UI都合が入りやすいから注意⚠️

例:ReadModel(表示用の形)📦

public sealed record MemoDetailReadModel(
string Id,
string Title,
string Body,
bool IsArchived,
DateTimeOffset UpdatedAt
);

✅ Read用Gateway(例)

public interface IMemoQueryService
{
Task<MemoDetailReadModel?> GetDetailAsync(string id, CancellationToken ct);
}

“QueryService”って名前にしておくと、これは読み取り専門だよ📚って伝わりやすいよ〜🥰


4-B) 次に “Write(UpdateMemo)” ✍️🔥

✅ 設計のコツ(Write)

  • 更新は必ずEntityを通す(ルールを守るため)🛡️
  • RequestModelは必要最小🍱✨
  • 「どう更新するか」の判断は UseCase、 「更新して良いか」の最終判断は Entity(不変条件)💎

5) 初級向け:Repositoryを1つで始める版(簡単で迷子にならない🥺💖)

✅ Write側のRepository(Core側interface)

public interface IMemoRepository
{
Task<Memo?> FindByIdAsync(MemoId id, CancellationToken ct);
Task SaveAsync(Memo memo, CancellationToken ct);
}

✅ Read側も同じRepositoryに生やす(最初はアリ!)

public interface IMemoRepository
{
Task<Memo?> FindByIdAsync(MemoId id, CancellationToken ct);
Task SaveAsync(Memo memo, CancellationToken ct);

// Read向け(最初はここに置いてOK)
Task<MemoDetailReadModel?> GetDetailAsync(string id, CancellationToken ct);
}

ただし、ReadModelが増えてくると太りやすいから、後で分離できる形にしておくと安心だよ🫶✨


6) 中級に一歩:Read と Write を分ける版(UseCaseが育ちやすい🌱💖)

✅ Write用(Entityが戻る)

public interface IMemoCommandRepository
{
Task<Memo?> FindByIdAsync(MemoId id, CancellationToken ct);
Task SaveAsync(Memo memo, CancellationToken ct);
}

✅ Read用(ReadModelが戻る)

public interface IMemoQueryService
{
Task<MemoDetailReadModel?> GetDetailAsync(string id, CancellationToken ct);
}

7) UseCase(Ports/Models/Interactors)サンプル✨

7-A) GetMemo UseCase 📚

InputPort / Request

public interface IGetMemoInputPort
{
Task HandleAsync(GetMemoRequest request, CancellationToken ct);
}

public sealed record GetMemoRequest(string MemoId);

OutputPort / Response

public interface IGetMemoOutputPort
{
Task PresentAsync(GetMemoResponse response, CancellationToken ct);
}

public sealed record GetMemoResponse(
bool Found,
MemoDetailReadModel? Memo
);

Interactor(読み取りは QueryService だけ呼ぶ)

public sealed class GetMemoInteractor : IGetMemoInputPort
{
private readonly IMemoQueryService _query;
private readonly IGetMemoOutputPort _output;

public GetMemoInteractor(IMemoQueryService query, IGetMemoOutputPort output)
{
_query = query;
_output = output;
}

public async Task HandleAsync(GetMemoRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.MemoId))
{
await _output.PresentAsync(new GetMemoResponse(false, null), ct);
return;
}

var memo = await _query.GetDetailAsync(request.MemoId, ct);

await _output.PresentAsync(
new GetMemoResponse(memo is not null, memo),
ct
);
}
}

7-B) UpdateMemo UseCase ✍️

InputPort / Request

public interface IUpdateMemoInputPort
{
Task HandleAsync(UpdateMemoRequest request, CancellationToken ct);
}

public sealed record UpdateMemoRequest(
string MemoId,
string NewTitle,
string NewBody
);

OutputPort / Response

public interface IUpdateMemoOutputPort
{
Task PresentAsync(UpdateMemoResponse response, CancellationToken ct);
}

public sealed record UpdateMemoResponse(
bool Succeeded,
string? ErrorMessage
);

Interactor(Entityを取って、Entityメソッドで更新して、保存✨)

public sealed class UpdateMemoInteractor : IUpdateMemoInputPort
{
private readonly IMemoCommandRepository _repo;
private readonly IUpdateMemoOutputPort _output;

public UpdateMemoInteractor(IMemoCommandRepository repo, IUpdateMemoOutputPort output)
{
_repo = repo;
_output = output;
}

public async Task HandleAsync(UpdateMemoRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.MemoId))
{
await _output.PresentAsync(new UpdateMemoResponse(false, "IDが空だよ〜🥺"), ct);
return;
}

var id = new MemoId(request.MemoId);
var memo = await _repo.FindByIdAsync(id, ct);

if (memo is null)
{
await _output.PresentAsync(new UpdateMemoResponse(false, "メモが見つからないよ〜🔎💦"), ct);
return;
}

// ここが超重要💖:ルールは Entity に効かせる!
var result = memo.UpdateContent(request.NewTitle, request.NewBody);

if (!result.Succeeded)
{
await _output.PresentAsync(new UpdateMemoResponse(false, result.ErrorMessage), ct);
return;
}

await _repo.SaveAsync(memo, ct);
await _output.PresentAsync(new UpdateMemoResponse(true, null), ct);
}
}

※ Entity側の UpdateContent は「タイトル空禁止」とか「長さ制限」とかを守る場所だよ💎✨ こうすると UseCase が “if地獄” になりにくい〜🥰


8) AI(Copilot / Codex)に頼むと捗るポイント🤖✨

✅ 使いどころ①:「ReadModelの設計」📦

  • 「画面に必要な項目だけ」を列挙してもらう
  • 余計なフィールドを削るチェックも頼める✂️

✅ 使いどころ②:「Repositoryのメソッド名」🧠

  • 「目的ベースになってる?」(例:FindByIdAsync より GetDetailAsync の方が“用途”が伝わる場面もある)

✅ 使いどころ③:「I/O増えすぎ監査」🔍

  • 「このUseCase、DBアクセス何回?減らせる?」ってレビュー役にする😆

(プロンプト例💬)

このInteractorはClean ArchitectureのUseCaseです。
I/O(Repository呼び出し)が増えすぎないか、責務が混ざってないかレビューして、
改善案を3つください。修正例コードもください。

9) ミニ課題🎮✨(手を動かすと理解が爆伸び!)

課題①:GetMemoに「Found=false時の理由」を足す📝

  • 例:ID空 / 見つからない で分ける
  • ただし HTTPステータスの話はしない(Presenterの仕事だよ😉)

課題②:UpdateMemoに「Archive」も追加📦

  • UpdateMemoRequest に IsArchived を足す?
  • それとも UseCase を分けて ArchiveMemo にする?(迷ったら分けるのが安全💖)

課題③:Readを“一覧用ReadModel”にする📚

  • MemoListItemReadModel(Id, Title, UpdatedAt だけ)みたいにしてみよ〜✨

10) よくある事故💥(ここだけ避ければ勝ち!🏆)

  • ❌ ReadでEntityを返して、UI都合のプロパティがEntityに増える
  • ❌ UpdateでEntityを取らずに、DTOをそのままSaveしてルールが抜ける
  • ❌ UseCaseが「検索条件の組み立て」や「表示整形」までやり始める
  • ❌ Repositoryが“万能化”して、何でも取れる/何でも保存できるAPIになる(地味に崩壊コース😇)

11) 次章へのつながり💳✨

この章で「読み📚 / 書き✍️」整理できたら、次は トランザクション境界だよ〜!🔥 「1ユースケース=1トランザクション」の感覚を入れると、整合性が超強くなる🛡️💖


必要なら、この第24章の内容に合わせて「GetMemo/UpdateMemo のPresenter例」もセットで作るよ🎤✨