solid_cs_study_021
第21章:ISP+外部連携:Adapterで変換して守る🔌🛡️✨
(テーマ:外部APIの“都合”を、アプリの内側に持ち込まない💪💖)
この章のゴールはこれだよ〜!😊🌸
- 外部API(決済・配送・在庫・通知など)がイケてなくても😇、自分のコードを綺麗に保つ
- ISPの考え方で、必要な約束(インターフェース)だけに絞る✂️
- Adapterで、外側→内側の変換壁(境界)を作る🧱✨
- テストが一気に楽になる(ここ超大事)🧪💕
ちなみに、.NET 10 は 2025/11/11 リリースの LTS で、C# 14 は .NET 10 でサポート、Visual Studio 2026 に .NET 10 SDK が入ってるよ📦✨ (Microsoft)
1) まず「外部連携あるある地獄」😵💫💥
外部SDKや外部APIって、だいたいこうなるの…👇
- メソッドがやたら多い(太い)📌
- 名前やデータ構造が微妙(クセ強)🌀
- 例外やエラーが独自で扱いづらい⚠️
- アプリ内の“言葉”と合ってない(ドメインとズレる)🧩
そして一番ありがちなミスがこれ👇 アプリの中心(業務ロジック)が、外部SDKに直接依存する😇 → すると、変更・差し替え・テストが全部きつくなる💦
ここで ISP が効くよ✂️✨ **「使わないメソッドまで実装させない」「必要な約束だけにする」**って発想ね😊💕
2) 今日の結論:こう分けると勝ち🏆✨
外部連携は、基本この形にしよう〜👇🥰
-
✅ 内側(業務側)
- 小さくて必要十分なインターフェース(例:
IPaymentGateway)だけを定義 - そのインターフェースだけに依存する(外部SDKを知らない)🙈
- 小さくて必要十分なインターフェース(例:
-
✅ 外側(インフラ側)
- 外部SDK・外部APIのクセを全部引き受ける😤
- Adapter が「翻訳係」になる🗣️➡️🗣️✨
- DTO変換、例外変換、リトライ方針、ログ…ぜんぶここ!
この構造、見た目は地味だけど… “将来の自分”が泣かない設計になるよ😭➡️😊💕
3) 例題:決済API(外部SDK)を守りながら使う💳🛡️
✅ やりたいこと(業務側の言葉)
- 注文IDに対して
- 指定金額を
- “チャージする”
…だけでいい!他はいらん!✋😆
3.1 内側(業務)に「細いインターフェース」を作る✂️✨
public readonly record struct OrderId(string Value);
public readonly record struct Money(decimal Amount, string Currency);
public enum PaymentStatus
{
Succeeded,
Failed
}
public sealed record PaymentResult(
PaymentStatus Status,
string? TransactionId,
string? ErrorCode,
string? ErrorMessage);
public interface IPaymentGateway
{
Task<PaymentResult> ChargeAsync(OrderId orderId, Money amount, CancellationToken ct);
}
ポイントはこれだよ😊🌸
- 外部SDKの型を 一切出さない 🙅♀️
- 業務に必要なメソッドだけ(超スリム)✂️✨
PaymentResultも自分たちの都合の良い形にする💕
3.2 外部SDK(例:クセ強クライアント)を想定😇
外部SDKってだいたい、こんなノリ👇(イメージ)
// 外部SDK側(例): 変更されうる&クセ強い
public sealed class MegaPayClient
{
public Task<MegaPayChargeResponse> ChargeAsync(MegaPayChargeRequest req, CancellationToken ct) => throw new NotImplementedException();
}
public sealed record MegaPayChargeRequest(string MerchantId, string OrderNo, decimal Amount, string Currency);
public sealed record MegaPayChargeResponse(string TxId, string ResultCode, string? Message);
3.3 Adapter:外部SDKをラップして「翻訳」する🔁🛡️

public sealed class MegaPayAdapter : IPaymentGateway
{
private readonly MegaPayClient _client;
private readonly string _merchantId;
public MegaPayAdapter(MegaPayClient client, string merchantId)
{
_client = client;
_merchantId = merchantId;
}
public async Task<PaymentResult> ChargeAsync(OrderId orderId, Money amount, CancellationToken ct)
{
try
{
var req = new MegaPayChargeRequest(
MerchantId: _merchantId,
OrderNo: orderId.Value,
Amount: amount.Amount,
Currency: amount.Currency
);
var res = await _client.ChargeAsync(req, ct);
// 外部の結果コードを、内側の言葉へ変換✨
return res.ResultCode switch
{
"OK" => new PaymentResult(PaymentStatus.Succeeded, res.TxId, null, null),
_ => new PaymentResult(PaymentStatus.Failed, null, res.ResultCode, res.Message)
};
}
catch (Exception ex)
{
// 外部例外は “境界” で止める(内側へ漏らさない)🧱
return new PaymentResult(PaymentStatus.Failed, null, "EXCEPTION", ex.Message);
}
}
}
ここ、めちゃ重要だよ〜!🥹✨
- 外部のResultCode を、内側の
PaymentStatusに変換する - 外部例外をそのまま投げない(内側が外部事情に振り回される)😇
- Adapterが「防波堤」🌊🧱✨
4) 業務サービス側は、外部の存在を忘れる😌💕
public sealed class OrderService
{
private readonly IPaymentGateway _paymentGateway;
public OrderService(IPaymentGateway paymentGateway)
{
_paymentGateway = paymentGateway;
}
public async Task<bool> PayAsync(OrderId orderId, Money amount, CancellationToken ct)
{
var result = await _paymentGateway.ChargeAsync(orderId, amount, ct);
if (result.Status == PaymentStatus.Succeeded) return true;
// 失敗時の処理(ログ・通知など)はここで自由に✨
return false;
}
}
OrderServiceは 外部SDKを一切知らない🙈✨ だから…
- 決済会社を変える(MegaPay→OtherPay)も、Adapter差し替えでOK🔁
- テストは Fake を差すだけでOK🧪💕
5) DI登録:差し替え可能にする🔧✨
ASP.NET Core / Worker / Console でも考え方同じだよ😊
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
// 外部クライアント登録(実際はHttpClientやSDKの作法に合わせて)
services.AddSingleton(new MegaPayClient());
// 内側に見せるのは IPaymentGateway だけ✨
services.AddScoped<IPaymentGateway>(sp =>
{
var client = sp.GetRequiredService<MegaPayClient>();
return new MegaPayAdapter(client, merchantId: "MERCHANT-001");
});
services.AddScoped<OrderService>();
var provider = services.BuildServiceProvider();
6) テストが爆速でラクになる🧪🚀✨(ここが最高)
外部SDKに依存してると、テストがこうなる👇
- 変な初期化が必要
- 使わないメソッドまでモック地獄
- 通信しないと確認できない(遅い)
でも ISP+Adapter だと、こう👇😍
- 業務テストは Fake で一瞬
- Adapterは別枠で「結合テスト」すればOK
6.1 Fakeで業務テスト(例:xUnit想定)🧪💕
public sealed class FakePaymentGateway : IPaymentGateway
{
public PaymentResult NextResult { get; set; }
= new(PaymentStatus.Succeeded, "TX-FAKE", null, null);
public Task<PaymentResult> ChargeAsync(OrderId orderId, Money amount, CancellationToken ct)
=> Task.FromResult(NextResult);
}
7) 外部通信するなら:HttpClientの作法も押さえる📡🧠
外部APIを叩く実装になるなら、.NET は “作法” があるよ〜😊
HttpClientの扱いを雑にすると、ソケット枯渇などの問題が起きうる- その対策として、
IHttpClientFactoryや “typed client” が推奨される流れ✨ (Microsoft Learn) - ただし Cookieが必要なケースでは注意点もある(共有・破棄など)🍪⚠️ (Microsoft Learn)
- 回復性(リトライ等)を組みやすくする説明もあるよ🛡️ (Microsoft Learn)
この章の“結論”としては: 外部通信の詳細も、Adapter側(外側)へ閉じ込める🔒✨ 業務ロジック側に「HttpClient」「JSON」「ResultCode」みたいなのを漏らさないのが勝ち🏆😊
8) Copilotに頼むと超はかどるプロンプト集🤖💕
- 「この外部SDKのレスポンスを、
PaymentResultに安全にマッピングして。失敗コードも考慮して」 - 「例外を内側に漏らさない方針で、Adapterの実装案を3パターン出して」
- 「
IPaymentGatewayのテスト用 Fake 実装を作って。成功・失敗を切り替えられるように」 - 「外部DTOと内部モデルの変換クラス(Mapper)を作って。責務が肥大化しないように分割して」
9) 演習(3段階)📚✨
レベル1(ウォームアップ)😊
IShippingGatewayを作って、CreateShipmentAsyncだけ定義✂️- Adapterで外部の
ShipmentStatusCodeを内側の enum に変換
✅ 合格ライン:業務側が外部SDKの型を一切参照してないこと🙈
レベル2(実戦)🔥
- 外部が返すエラー形式(コード+メッセージ)を、内側の
ErrorCode/ErrorMessageに統一 - タイムアウト・キャンセル(
CancellationToken)をちゃんと流す
✅ 合格ライン:OrderServiceのテストが Fake だけで完結🧪
レベル3(強い)💪✨
- Adapterを「DTO変換」「通信」「例外変換」に分割(SRPも同時に意識)🧹
- Adapterの結合テスト(スタブサーバー or テストダブル)方針を文章化📝
✅ 合格ライン:外部差し替えの影響範囲が“Adapter周辺だけ”になってる🔁✨
10) 理解チェッククイズ(サクッと)📝😊
- 外部SDKの型を業務側に漏らすと、何が困る?😇
- ISPの観点で「良いインターフェース」ってどんな形?✂️
- Adapterは何を“変換”する?(最低3つ言ってみて)🔁
- 業務テストで Fake を差すと何が嬉しい?🧪✨
必要なら、この章の続きとして👇も一緒に作れるよ😊💕
- 「決済だけじゃなく、配送APIでもう1セット(まるごと)実装」📦
- 「エラー設計(例外にする?Result型にする?)の選び方」⚖️
- 「Adapter肥大化を防ぐ分割テンプレ(Mapper/Client/Policy)」🧱✨