第20章:ISP実戦「読み取り用・更新用を分ける」📖✍️✨
この章は 「読むだけの人に、書く道具を持たせない」 がテーマだよ〜😊 同じ“注文データ”でも、使う側(画面・バッチ・テスト…)で必要な操作って全然ちがうよね?そこを インターフェースでちゃんと分ける 練習をします💪💕
(ちなみに2026の最新前提として、.NET 10 は 2025/11/11 リリースの LTS、C# 14 は .NET 10 対応、Visual Studio 2026 に .NET 10 SDK が含まれるよ🆕✨) (Microsoft)
この章のゴール🎯✨
- 「太いインターフェース」が何を壊すのか体感する😵💫💥
- Read(参照) と Write(更新) を分けて、利用者の負担を減らす✂️💕
- 画面・バッチ・テストなど、利用者視点で“必要な約束だけ”作れる ようになる😊🌸
まず、ありがちな“太いインターフェース”😈📦
例:注文を扱う IOrderRepository が全部盛り…🍔🍟🥤
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<IReadOnlyList<Order>> SearchAsync(string keyword, CancellationToken ct = default);
Task SaveAsync(Order order, CancellationToken ct = default);
Task UpdateStatusAsync(Guid id, OrderStatus newStatus, CancellationToken ct = default);
Task DeleteAsync(Guid id, CancellationToken ct = default);
}
public enum OrderStatus { Draft, Paid, Shipped, Cancelled }
public sealed record Order(Guid Id, string CustomerName, decimal Total, OrderStatus Status);
これの何がツラいの?😇💦
たとえば「注文一覧を表示する画面」は、SearchAsync しか使わないのに…
- 更新/削除メソッドまで見えちゃう → 誤用の誘惑が増える🙈💥
- モック/フェイクが重くなる(テストで Delete まで実装させられる)😵💫
- 将来、更新系の仕様変更が入ると、参照側まで巻き添えコンパイルエラー🤯
ISPの気持ち: 👉 “使わないメソッドに依存させないで” ってことだよ✂️💕
使う人(利用者)を分けてみよ〜👥✨
同じ注文でも、利用者タイプで必要な操作が違うよね😊
- 注文一覧画面:検索・詳細表示だけ見たい👀📄
- 管理画面:ステータス更新したい🛠️✨
- バッチ:期限切れ注文をキャンセルしたい⏰🧹
- テスト:読み取りだけのフェイクが欲しい🧪💕
この“違い”がそのままインターフェース分割の根拠になるよ👍💡
解決:Read と Write を分ける📖✍️✂️

✅ After(分割版)
public interface IOrderReader
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<IReadOnlyList<Order>> SearchAsync(string keyword, CancellationToken ct = default);
}
public interface IOrderWriter
{
Task SaveAsync(Order order, CancellationToken ct = default);
Task UpdateStatusAsync(Guid id, OrderStatus newStatus, CancellationToken ct = default);
Task DeleteAsync(Guid id, CancellationToken ct = default);
}
これだけで世界がだいぶ平和になるよ🕊️✨
「読む側」は IOrderReader だけ持てばOK、「更新する側」は IOrderWriter だけ持てばOK😊
“利用者ごと”に依存を貼る(ここが実戦!)🔥
① 注文一覧(読むだけ)👀📄
public sealed class OrderListUseCase
{
private readonly IOrderReader _reader;
public OrderListUseCase(IOrderReader reader) => _reader = reader;
public Task<IReadOnlyList<Order>> SearchAsync(string keyword, CancellationToken ct = default)
=> _reader.SearchAsync(keyword, ct);
}
💡 ここがポイント: このクラスは 更新の存在すら知らない ✨(だから壊れにくい!)
② ステータス変更(書く側)🛠️🚚
public sealed class ChangeOrderStatusUseCase
{
private readonly IOrderWriter _writer;
public ChangeOrderStatusUseCase(IOrderWriter writer) => _writer = writer;
public Task ExecuteAsync(Guid orderId, OrderStatus status, CancellationToken ct = default)
=> _writer.UpdateStatusAsync(orderId, status, ct);
}
③ 「読む+書く」両方必要なケースもあるよね🙂🔁
そのときは 両方に依存してOK(無理に1本化しないのがISP脳!)
public sealed class CheckoutUseCase
{
private readonly IOrderReader _reader;
private readonly IOrderWriter _writer;
public CheckoutUseCase(IOrderReader reader, IOrderWriter writer)
=> (_reader, _writer) = (reader, writer);
public async Task PayAsync(Guid orderId, CancellationToken ct = default)
{
var order = await _reader.GetByIdAsync(orderId, ct);
if (order is null) throw new InvalidOperationException("Order not found.");
var paid = order with { Status = OrderStatus.Paid };
await _writer.SaveAsync(paid, ct);
}
}
実装クラスは1つでもOK(でも見せる顔を変える)🎭✨
「実装は1クラス」で、インターフェースを2つ実装すればOKだよ😊
using System.Collections.Concurrent;
public sealed class InMemoryOrderStore : IOrderReader, IOrderWriter
{
private readonly ConcurrentDictionary<Guid, Order> _db = new();
public Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default)
=> Task.FromResult(_db.TryGetValue(id, out var v) ? v : null);
public Task<IReadOnlyList<Order>> SearchAsync(string keyword, CancellationToken ct = default)
{
var list = _db.Values
.Where(o => o.CustomerName.Contains(keyword, StringComparison.OrdinalIgnoreCase))
.ToList()
.AsReadOnly();
return Task.FromResult((IReadOnlyList<Order>)list);
}
public Task SaveAsync(Order order, CancellationToken ct = default)
{
_db[order.Id] = order;
return Task.CompletedTask;
}
public Task UpdateStatusAsync(Guid id, OrderStatus newStatus, CancellationToken ct = default)
{
if (_db.TryGetValue(id, out var old))
_db[id] = old with { Status = newStatus };
return Task.CompletedTask;
}
public Task DeleteAsync(Guid id, CancellationToken ct = default)
{
_db.TryRemove(id, out _);
return Task.CompletedTask;
}
}
🌟 重要:
利用者は InMemoryOrderStore を直接知らなくていい。
「読む人には読む口(IOrderReader)だけ」「書く人には書く口(IOrderWriter)だけ」見せるのがISPだよ〜✂️💕
ありがち事故と対策🚑💦
❌ 事故1:分けたのに、結局 “全部入り” を注入しちゃう
- 利用者クラスのコンストラクタが
IOrderReaderじゃなくてInMemoryOrderStoreとかIOrderRepositoryになってる😇 ✅ 依存先を必ず細い方にする(利用者が見たい約束だけ)
❌ 事故2:Read側が更新メソッドを返し始める
- 例:
GetByIdAsyncが「追跡付きEntity」を返して、触っただけで更新される系😵💫 ✅ “読む”は読む。必要ならRead用DTOにする(軽くCQRSっぽい発想)📖✨
❌ 事故3:細かくしすぎて迷子
✅ 最初は Read / Write の2分割で十分! (もっと細くしたくなったら「誰が困ってる?」を根拠に✂️)
Copilot / AI の使いどころ🤖✨(超実戦)
Visual Studio 2026 はAI統合が強化されていて、Copilotも統合的に使える流れだよ🧠⚡ (Microsoft Learn) (CopilotはVSの対応バージョン要件があるので、そこだけ注意ね🔧) (GitHub Docs)
使えるプロンプト例💬✨
- 「この
IOrderRepositoryを “読み取り用” と “更新用” に分割して。利用者クラスごとに必要なメソッドも整理して」 - 「注文一覧画面が依存すべき最小インターフェースを提案して」
- 「分割後に発生するコンパイルエラーを直す手順を、変更箇所の順番付きで出して」
- 「Read側のフェイク実装(テスト用)を最小で作って」
演習(手を動かすやつ)🧩🔥
演習1:利用者タイプを書き出す📝
あなたのサンプルで、利用者を3つ挙げてね😊 例:画面・管理・バッチ・テスト…など!
演習2:太いIFを2つに分割✂️
IOrderRepository→IOrderReader/IOrderWriter- 利用者クラスの依存を「細い方」に付け替え
演習3:テストがラクになったのを味わう🧪✨
- 「注文一覧」用に Readだけのフェイクを作ってみてね
- Writeメソッドを実装しなくてよくなる快感…🥹💕
ミニまとめ🌈✨
- ISPは “利用者が必要な約束だけ持つ” ための原則✂️😊
- 実戦で一番効く型が Read/Write 分離(読む人に書く道具を渡さない)📖✍️
- テストも変更もラクになるよ〜🧪🧼💕
次の第21章では、外部APIみたいに「向こう都合で変な形」になってるやつを Adapterで内側を守るやつに進むよ🔌🛡️✨