第27章:UseCaseを増やしても崩れない“型”を作る📐✨
今まで作ってきた InputPort / Interactor / OutputPort / Presenter / Repository の流れ、1個なら作れるけど…… ユースケースが増えてくると、だんだんこうなりがち👇😵💫
- 「毎回ファイル配置がバラバラ」📁💥
- 「命名が揺れる(Createなの?Addなの?)」🤯
- 「Interactorが肥大化して “なんでも屋” になる」🧟♂️
- 「Controller/Presenterが混ざって境界が溶ける」🫠
この章は、それを防ぐために “増やしても崩れない型(テンプレ)” を作ります💪💖 しかも、AI(Copilot / Codex系)に 雛形生成を手伝わせやすい形 にします🤖🪄
※2026年1月時点の最新として、.NET 10(LTS, 2025年11月リリース)&C# 14 前提で書いてるよ📌✨ (Microsoft Dev Blogs)
1) この章のゴール🎯💞
ユースケースを追加するとき、毎回「悩む」をゼロに近づける✨
- ✅ 置き場所が一瞬で決まる(フォルダ構成が固定)📦
- ✅ クラス名が迷わない(命名規則が固定)🏷️
- ✅ Interactorの責務が膨らまない(やることが固定)🧱
- ✅ 依存ルールが自然に守られる(型が境界を守る)🛡️
2) “型”の全体図(これを毎回コピペ脳で作る🧠📋)
ユースケース1つにつき、この6点セットを作るのが型です👇✨
- Request(入力データ)📨
- InputPort(入口インターフェース)🔌⬅️
- Interactor(手順の本体)🧱
- Response(出力データ)📦
- OutputPort(出口インターフェース)🔌➡️
- Presenter(外側向けに整形)🎤 +(必要なら)Repository/Gateway interface(外部I/Oの出口)🚪
「Dependency Rule:依存は内側へ」って話の、まさに実戦版だね⭕➡️ (blog.cleancoder.com)
3) 迷わないフォルダ構成(Featureフォルダ方式)📁✨

“ユースケース名でフォルダを切る” のがいちばん迷子になりにくいです😊💕
例:メモアプリで CreateMemo を作るなら👇
-
UseCases/Memos/CreateMemo/CreateMemoRequest.csICreateMemoInputPort.csCreateMemoInteractor.csCreateMemoResponse.csICreateMemoOutputPort.cs
Presenterはアダプタ層に置くので👇
Adapters/Presenters/Memos/CreateMemoPresenter.cs
この時点で 「どこに何置く?」が消えます🫶✨
4) “結果の形”を統一して、毎回の悩みを消す🍱✨
ユースケースごとに 「成功はこれ、失敗は例外?エラー?戻り値?」って揺れると崩れます💥
そこでこの章では、出力を Result型で統一しちゃいます✌️😆
- 成功:
Result.Ok(value)🎉 - 失敗:
Result.Fail(code, message)⚠️
共通のResult(UseCasesに1回だけ作る)🧩
namespace CleanMemo.UseCases.Abstractions;
public sealed record Error(string Code, string Message);
public readonly record struct Result<T>(T? Value, Error? Error)
{
public bool IsSuccess => Error is null;
public static Result<T> Ok(T value) => new(value, null);
public static Result<T> Fail(string code, string message)
=> new(default, new Error(code, message));
}
5) “型”の雛形(CreateMemoで完成形を見せるよ✨)🧱💕
ここから コピペして名前だけ変えるのが正解です😆🫶
(1) Request(入力)📨
namespace CleanMemo.UseCases.Memos.CreateMemo;
public sealed record CreateMemoRequest(
string Title,
string Body
);
(2) InputPort(入口)🔌⬅️
namespace CleanMemo.UseCases.Memos.CreateMemo;
public interface ICreateMemoInputPort
{
Task HandleAsync(CreateMemoRequest request, CancellationToken ct = default);
}
(3) Response(出力の中身)📦
namespace CleanMemo.UseCases.Memos.CreateMemo;
public sealed record CreateMemoResponse(
Guid MemoId
);
(4) OutputPort(出口)🔌➡️
※出力は Resultで統一✨
using CleanMemo.UseCases.Abstractions;
namespace CleanMemo.UseCases.Memos.CreateMemo;
public interface ICreateMemoOutputPort
{
Task PresentAsync(Result<CreateMemoResponse> result, CancellationToken ct = default);
}
(5) Interactor(本体)🧱
※Interactorは「手順」だけ。HTTPもDBも知らない🙂↔️✨
using CleanMemo.UseCases.Abstractions;
namespace CleanMemo.UseCases.Memos.CreateMemo;
public sealed class CreateMemoInteractor : ICreateMemoInputPort
{
private readonly IMemoRepository _repo;
private readonly ICreateMemoOutputPort _output;
public CreateMemoInteractor(IMemoRepository repo, ICreateMemoOutputPort output)
{
_repo = repo;
_output = output;
}
public async Task HandleAsync(CreateMemoRequest request, CancellationToken ct = default)
{
// 1) 入力を使ってドメインを作る(例:Memoエンティティの生成)
// ※ここでは雛形として最小にしてるよ
if (string.IsNullOrWhiteSpace(request.Title))
{
await _output.PresentAsync(
Result<CreateMemoResponse>.Fail("Validation.TitleEmpty", "タイトルが空っぽだよ🥺"),
ct
);
return;
}
var newId = Guid.NewGuid();
// 2) 保存(外部I/Oは Repository に任せる)
await _repo.AddAsync(newId, request.Title, request.Body, ct);
// 3) 出力
await _output.PresentAsync(
Result<CreateMemoResponse>.Ok(new CreateMemoResponse(newId)),
ct
);
}
}
(6) Repository(外部I/Oの出口)🚪
※「UseCaseが必要な操作だけ」を置くのがコツ✂️✨
namespace CleanMemo.UseCases;
public interface IMemoRepository
{
Task AddAsync(Guid id, string title, string body, CancellationToken ct = default);
Task<bool> ExistsAsync(Guid id, CancellationToken ct = default);
Task UpdateTitleAsync(Guid id, string title, CancellationToken ct = default);
Task ArchiveAsync(Guid id, CancellationToken ct = default);
}
6) Presenter(Adapter側)🎤✨(超重要!)
Presenterは OutputPortを実装して、外側(APIレスポンス等)に変換します🔄
using CleanMemo.UseCases.Abstractions;
using CleanMemo.UseCases.Memos.CreateMemo;
namespace CleanMemo.Adapters.Presenters.Memos;
public sealed class CreateMemoPresenter : ICreateMemoOutputPort
{
// Controllerが取り出せるように保持する(例)
public object? ViewModel { get; private set; }
public Task PresentAsync(Result<CreateMemoResponse> result, CancellationToken ct = default)
{
ViewModel = result.IsSuccess
? new { ok = true, memoId = result.Value!.MemoId }
: new { ok = false, error = result.Error!.Code, message = result.Error!.Message };
return Task.CompletedTask;
}
}
ここまでが「型」👏🥰 この型を守る限り、ユースケースが増えても構造が崩れにくいです🛡️✨
7) ミニ課題:テンプレで2ユースケース追加しよう🎮💕
課題A:UpdateMemoTitle(タイトル変更)✍️
- Request:
MemoId, NewTitle - Response:
MemoId - 失敗例:Memoが存在しない / タイトルが空
課題B:ArchiveMemo(アーカイブ)🗃️
- Request:
MemoId - Response:
MemoId - 失敗例:Memoが存在しない
作り方は同じです😆✨ フォルダだけ増やして、名前を置換して、中身の手順だけ書く!
8) AI(Copilot/Codex)に雛形を作らせるコツ🤖🪄
AIに投げるときは「型」をそのまま指示すると事故が減ります👍💕
おすすめプロンプト例💬✨
- 「
UpdateMemoTitleをこの構造で生成して:Request/InputPort/Interactor/Response/OutputPort。出力はResult<T>で統一。UseCases層にASP.NET型やEF型を絶対に入れないこと。」 - 「Interactorは Repository と OutputPort だけに依存。ControllerやIActionResultは禁止。」
AIがやりがちな事故あるある🚨
- ❌ Interactorが
IActionResultを返す - ❌ UseCases内で
Microsoft.AspNetCore.*を参照しちゃう - ❌ EF Coreの
DbContextを直に触る - ❌ RequestにAPI DTOをそのまま流し込む
このへんは Dependency Rule違反になりやすいので要注意だよ🙂↔️🛡️ (blog.cleancoder.com)
9) “型が崩れてない?”チェックリスト✅✨
ユースケースを追加したら、これだけ見てね👀💕
- ✅ UseCasesプロジェクトが ASP.NET / EF Core を参照してない
- ✅ Interactorの依存は Repository + OutputPort + Domain だけ
- ✅ 変換(DTO↔Request、Response↔DTO)は Adapter側にある
- ✅ 例外でドーンじゃなく、
Result.Fail()に落としている
まとめ🎀✨
この章のポイントはこれだけ👇💖
- ユースケース追加が辛くなる原因は **“毎回の揺れ”**😵💫
- 揺れを消すには 型(テンプレ)を固定する📐
- 出力は Resultで統一すると、成功/失敗の扱いがブレない🍱
- AIには **「型+禁止事項」**までセットで渡すと強い🤖✨
次に第28章で、この「型」で増やしたUseCasesが ちゃんと外側を知らないまま保ててるか、完成チェックしていこうね✅🥰