第52章:Proxy ①:本人の代わりに制御する🪞🚧
ねらい 🎯
- 本体(本物のオブジェクト)に触る前後で、ちょい足しの制御を入れられるようになる(遅延・キャッシュ・制限・ログなど)🧊🧾🔐
- 「機能追加はDecorator?入口簡略化はFacade?」みたいに、“似てるけど違う”を見分ける軸を作る👀✨
- C#で「Proxyっぽいこと」をやるときに、まず思い出す定番(
Lazy<T>など)をつかむ⏳🧠
到達目標 ✅
- Proxyが解く困りごとを 1文で言える(例:“本体アクセスの前後に制御を挟みたい”)🗣️
- Proxyを使うべき状況を 具体例3つで説明できる(遅延/キャッシュ/アクセス制限 など)🧠🧩
- **手書きProxy(小さく)**で「キャッシュProxy」か「遅延Proxy」を作って、テストで効果を確認できる🧪✨
手順 🧭✨
1) Proxyってなに?(超ざっくり)🪞

Proxyは「本人(Real Subject)に見せかけた“代理人”」だよ〜!😺 呼び出し側からは 本体と同じインターフェースに見えるけど、裏でこういうことをする👇
- 本体を必要になるまで作らない(遅延 / Virtual Proxy)⏳
- 結果を覚えておいて次回を高速化(キャッシュ / Caching Proxy)🧊
- 条件が満たされないと通さない(保護 / Protection Proxy)🔐
- 呼ぶ前後でログ・計測・リトライなどを入れる(制御 / Smart Proxy)🧾📏🔁
2) ProxyとDecoratorの違い(ここ大事)🧠✨
どっちも「包む」けど目的が違うよ〜!🎁
- Proxy:アクセスを“制御”する(遅延・制限・キャッシュ・ログ)🚧
- Decorator:機能を“盛る”(組み合わせて機能追加)🎀
見分けの一言👇
- 「通していい?今作る?覚えてる?」→ Proxyっぽい🪞🚧
- 「機能を付け足していきたい」→ Decoratorっぽい🎁✨
3) C#でProxyをやる“定番3ルート”🛣️
この章はまず「小さく・安全に」いくよ〜🙂
- 手書きProxy(最推し):読める、デバッグしやすい、やりすぎない👌
Lazy<T>:遅延(Virtual Proxy)の最短ルート⏳(Lazy<T>は複数のコンストラクタやスレッド安全モードを持つよ)(Microsoft Learn)DispatchProxy:動的Proxy(次章で詳しく)。ただし 動的コードが必要な場面があり得るので注意(AOT/トリミング等)(Microsoft Learn)
ちなみに2026年時点のLTSは .NET 10 が目安(開始日などのライフサイクル情報は公式に載ってる)📅✨(Microsoft Learn) (こういう“今のLTSはどれ?”は年で変わるから、公式のライフサイクルを見るクセが強い💪)
4) ミニ実装:キャッシュProxy(+遅延も混ぜる)🧊⏳
題材は「大量に参照されるアイコン情報」🏷️ 本体は重い(外部から取る・ファイル読む・DB叩く等)想定で、Proxyが キャッシュして高速化するよ〜🚀
ポイント:
- Proxyは 本体と同じインターフェースを実装する
- Proxyは 本体を抱える(委譲する)
- Proxyが **制御(今回はキャッシュ)**を挟む
using System.Collections.Concurrent;
public sealed record IconInfo(string Key, string Svg);
// 本体もProxyも「同じ見た目」にする
public interface IIconStore
{
Task<IconInfo> GetAsync(string key, CancellationToken ct = default);
}
// 本体(重い処理がある想定)
public sealed class RealIconStore : IIconStore
{
public async Task<IconInfo> GetAsync(string key, CancellationToken ct = default)
{
// 例:外部取得っぽい遅延(ここでは疑似)
await Task.Delay(50, ct);
return new IconInfo(key, $"<svg><!-- {key} --></svg>");
}
}
// Proxy(キャッシュで制御する)
public sealed class CachingIconStoreProxy : IIconStore
{
private readonly IIconStore _inner;
// keyごとに「最初の1回だけ本体を呼ぶ」仕掛け
private readonly ConcurrentDictionary<string, Lazy<Task<IconInfo>>> _cache = new();
public CachingIconStoreProxy(IIconStore inner)
=> _inner = inner;
public Task<IconInfo> GetAsync(string key, CancellationToken ct = default)
{
// NOTE: ct はキャッシュと相性が悪い(呼び出しごとに違う)ので、
// まずは「キャッシュするAPIは ct なし」に分ける設計もよくあるよ🙂
var lazy = _cache.GetOrAdd(
key,
k => new Lazy<Task<IconInfo>>(() => _inner.GetAsync(k, CancellationToken.None))
);
return lazy.Value;
}
}
💡ここがProxyの気持ちいいところ:
- 呼び出し側は
IIconStoreしか見ない(本体かProxyか知らない)😌 - “制御(キャッシュ)”を呼び出し側から追い出せる🧹✨
5) テストで「本体の呼び出し回数」を固定する🧪🔒
Proxyは便利だけど、挙動がズレると地獄😵 だからテストで「本体が何回呼ばれたか」を押さえるよ〜!
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class ProxyTests
{
private sealed class CountingIconStore : IIconStore
{
public int CallCount { get; private set; }
public Task<IconInfo> GetAsync(string key, CancellationToken ct = default)
{
CallCount++;
return Task.FromResult(new IconInfo(key, "<svg/>"));
}
}
[TestMethod]
public async Task CachingProxy_CallsInnerOnlyOnce_PerKey()
{
var inner = new CountingIconStore();
IIconStore proxy = new CachingIconStoreProxy(inner);
var a1 = await proxy.GetAsync("cart");
var a2 = await proxy.GetAsync("cart");
var b1 = await proxy.GetAsync("mail");
Assert.AreEqual(2, inner.CallCount, "cart(1回) + mail(1回) の想定");
Assert.AreEqual("cart", a1.Key);
Assert.AreEqual("cart", a2.Key);
Assert.AreEqual("mail", b1.Key);
}
}
6) “本体アクセス前後の制御”のネタ帳(Proxyの使いどころ)🗂️✨
Proxyの制御は、だいたいこのへんに落ち着くよ〜🙂
-
遅延(Lazy):重い依存を必要になるまで作らない⏳
Lazy<T>の生成・モード(LazyThreadSafetyMode)で挙動を選べるよ(Microsoft Learn)
-
キャッシュ:同じ結果を何度も取りに行かない🧊
-
アクセス制限:権限・状態・回数制限🔐🚦
-
ログ/計測:前後でログ、時間測定、監査🧾⏱️
-
リモート呼び出しの“手前”:通信を隠す🌐(Remote Proxy)
-
HTTPまわりの“代理”:
HttpClientの内部はHttpMessageHandlerを差し替えてパイプラインを作れる(近縁の発想)📨🧩(Microsoft Learn)
7) AI補助(Copilot / Codex)で雛形を作るときのコツ🤖✍️
ProxyはAIが“盛りがち”なので、最初から縛るのが勝ち!😺✨
プロンプト例👇
- 「
IIconStoreは変更しないで、CachingIconStoreProxyを手書きで作って」 - 「キャッシュは
ConcurrentDictionaryのみ。独自フレームワーク禁止」 - 「テストは“本体呼び出し回数”を検証して」
レビュー観点はこれだけでOK🙆♀️
- Proxyが余計な責務を持ってない?(業務ロジック混ぜてない?)🧼
- 例外・戻り値が本体とズレてない?⚠️
- キャッシュのキーや寿命は妥当?(永遠キャッシュになってない?)🧊🕰️
よくある落とし穴 ⚠️😵
- Proxyが“何でも屋”になる(ログも認可もリトライも全部入れて太る)🐘 → まずは1種類だけ(例:キャッシュだけ)にする🙂
- 本体とProxyで例外や戻り値の契約がズレる💥 → テストで「ズレちゃダメなところ」を固定する🧪🔒
- キャッシュにCancellationTokenを混ぜてカオス🌀 → “キャッシュするAPI”は token を分ける/tokenは外側で扱う、が定番だよ🙂
- 共有(キャッシュ)するのに中身が可変で破綻🧨 → 共有するなら「不変」寄り(record等)にするのが安心🧾✨
演習(30〜60分)🛠️🎮
- 遅延Proxyを作る⏳
Lazy<IIconStore>を使って「最初の呼び出しまで本体を作らない」Proxyを作る- テスト:
IsValueCreated(またはカウント)で 本体が遅れて作られることを確認
- 保護Proxyを作る🔐
Func<bool> canAccessを受け取って、falseなら例外にするProxyを作る- テスト:許可あり/なしの2パターン
- “Proxyに入れるべきでない処理”を見抜く👀
- うっかりProxyに業務ルール(例:割引計算)を入れてしまった版を作る
- それを外に戻して、Proxyの責務を「制御」に戻す(コミット分けると最高)✨
チェック ✅📌
- Proxy導入理由を「遅延 / キャッシュ / 制限 / ログ」みたいに制御の言葉で言えてる?🪞🚧
- 呼び出し側が 具体クラス(RealIconStore)を知らずに済んでる?🙆♀️
- テストで「本体が何回呼ばれたか」「Proxyが何を守っているか」を固定できてる?🧪🔒
- Proxyが太ってない?(“ついでに便利機能”を詰め込みすぎてない?)🐘💦