デザインパターンとは?C#・.NETでよく使うものを実例で¶
最終確認: 2026-10-27
30秒で復習¶
- デザインパターン=よくある設計の悩みに対する、名前の付いた「定番の解き方」。 有名な23個(GoFのパターン)は、生成・構造・振る舞いの3つに分けられる。
- 覚える価値があるのは、名前で会話できるから。「ここは Strategy にしよう」で設計の意図が伝わる。
- .NET の中にはパターンがたくさん使われている:
StringBuilder(Builder)、IHttpClientFactory(Factory)、Streamの重ね掛け(Decorator)、ASP.NET Core のミドルウェア(Chain of Responsibility)、BackgroundService(Template Method)、event(Observer)、yield return(Iterator)。 - C# では、多くのパターンを インターフェース+DI で書く。特に Singleton は自分で作らず、DI の
AddSingletonに任せる。 - 使いすぎ注意:パターンは道具。困っていないのに当てはめると、クラスが増えて読みにくくなる。
- 覚え方:「作り方(生成)・組み合わせ方(構造)・やり取りの仕方(振る舞い)」
質問:デザインパターンとは?¶
答え: プログラムを作っていると、「作り方を切り替えたい」「古い部品を今の形に合わせたい」「処理の順番を差し込みたい」といった同じような悩みが何度も出てくる。その悩みと定番の解き方に名前を付けて整理したもの。1994年の書籍で紹介された23個(Gang of Four、GoF)が有名。
| 分類 | 何の悩み? | C#・.NETでよく使うもの |
|---|---|---|
| 生成 | オブジェクトの作り方を切り替えたい・隠したい | Factory Method、Builder、Singleton |
| 構造 | クラスを組み合わせて、形を合わせたり機能を足したりしたい | Adapter、Decorator、Facade、Proxy |
| 振る舞い | オブジェクト同士のやり取りや処理の流れを整理したい | Strategy、Observer、Template Method、Chain of Responsibility、Command、Iterator |
生成のパターン:作り方を整理する¶
Factory Method(ファクトリ):作り方を1か所に閉じ込める¶
悩み: new があちこちに散らばると、作るクラスを変えたいときに全部直すことになる。
public interface INotifier { Task SendAsync(string message); }
public class NotifierFactory
{
public INotifier Create(string channel) => channel switch
{
"mail" => new MailNotifier(),
"teams" => new TeamsNotifier(),
_ => throw new ArgumentException($"未対応の通知先: {channel}")
};
}
- .NETの実例:
IHttpClientFactory(HttpClientの作り方と使い回しを任せる)、ILoggerFactory。
Builder(ビルダー):少しずつ組み立てる¶
悩み: 設定項目が多いオブジェクトを、コンストラクターの長い引数で作ると読みにくい。
var sb = new StringBuilder();
sb.Append("こんにちは、").Append(name).AppendLine("さん");
var builder = WebApplication.CreateBuilder(args); // 設定を足していって…
builder.Services.AddControllers();
var app = builder.Build(); // 最後に組み立てる
- .NETの実例:
StringBuilder、WebApplication.CreateBuilder、UriBuilder。
Singleton(シングルトン):1つだけにする¶
悩み: 設定やキャッシュなど、アプリ全体で1つだけにしたいものがある。
- 昔は
staticなインスタンスを自分で作っていたが、テストで差し替えにくい。 - 今は DI の
AddSingletonに任せるのが定番。「1つだけ」の管理は DI コンテナがやってくれる。
builder.Services.AddSingleton<IClock, SystemClock>(); // アプリ全体で1つ
構造のパターン:組み合わせ方を整理する¶
Adapter(アダプター):形の違う部品を合わせる¶
悩み: 古いライブラリや外部のAPIが、今のコードが期待する形(インターフェース)と違う。
public interface IOrderReader { Order? Find(string orderNo); }
// 古いDBアクセス部品を、今のインターフェースに合わせる
public class LegacyOrderAdapter(LegacyDbClient legacy) : IOrderReader
{
public Order? Find(string orderNo)
{
var row = legacy.SelectOrder(orderNo.PadLeft(10, '0')); // 古い形式に変換
return row is null ? null : new Order(row.ORD_NO.Trim(), row.SHK_KBN == "3");
}
}
- オンプレのレガシーデータをMCPで扱うで、MCPサーバーが古いコードを翻訳するのも、考え方は同じ Adapter。
Decorator(デコレーター):包んで機能を足す¶
悩み: 元のクラスを書き換えずに、ログ・キャッシュ・リトライなどの機能を足したい。
// 圧縮しながらファイルに書く:FileStream を GZipStream で包む
using var file = File.Create("log.gz");
using var gzip = new GZipStream(file, CompressionLevel.Optimal);
// 同じインターフェースで包んで、ログを足す
public class LoggingNotifier(INotifier inner, ILogger<LoggingNotifier> log) : INotifier
{
public async Task SendAsync(string message)
{
log.LogInformation("送信します: {Message}", message);
await inner.SendAsync(message);
}
}
- .NETの実例:
Streamの重ね掛け(GZipStream・BufferedStream・CryptoStream)、HttpClientのDelegatingHandler。
Facade(ファサード):窓口を1つにまとめる¶
悩み: 「注文する」だけなのに、在庫・決済・配送・メールのクラスを順番に呼ぶ必要がある。
- 呼び出す側には
OrderService.PlaceOrder()だけを見せて、中で各クラスを呼ぶ。窓口係のイメージ。
Proxy(プロキシ):代理人を立てる¶
悩み: 重い処理を必要になるまで遅らせたい、呼び出しの前にチェックを挟みたい。
- .NETの実例:
Lazy<T>(最初に使うときに作る)、EF Core の遅延読み込み。
振る舞いのパターン:やり取りの仕方を整理する¶
Strategy(ストラテジー):やり方を差し替える¶
悩み: 「計算方法」「並べ方」「送料の決め方」などを、条件で切り替えたい。if / switch が増えていく。
public interface IShippingFee { decimal Calc(Order order); }
public class NormalFee : IShippingFee { public decimal Calc(Order o) => 800m; }
public class MemberFee : IShippingFee { public decimal Calc(Order o) => o.Total >= 5000m ? 0m : 500m; }
public class Checkout(IShippingFee fee) // どの計算方法かは外から渡す
{
public decimal Total(Order o) => o.Total + fee.Calc(o);
}
- .NETの実例:
IComparer<T>(並べ方を渡す)、List<T>.Sort(comparer)。 - DI と組み合わせると、設定や条件で計算方法を切り替えられる(.NET 8 からの キー付きサービスも便利)。
Observer(オブザーバー):起きたことを知らせる¶
悩み: 「注文が確定したら、メールも在庫も更新したい」。注文クラスが全部を知っていると、関係がこんがらがる。
public class OrderService
{
public event EventHandler<Order>? Placed; // 知らせたい相手は登録してもらう
public void Place(Order o) { /* … */ Placed?.Invoke(this, o); }
}
- .NETの実例: C# の
event、IObservable<T>/IObserver<T>。
Template Method(テンプレートメソッド):流れは親、中身は子¶
悩み: 処理の大きな流れは同じで、一部だけ違う。
public class NightlyJob : BackgroundService // 開始・停止の流れは親クラスが持つ
{
protected override async Task ExecuteAsync(CancellationToken stop) // 中身だけ書く
{
while (!stop.IsCancellationRequested)
{
await DoWorkAsync();
await Task.Delay(TimeSpan.FromHours(1), stop);
}
}
}
- .NETの実例:
BackgroundService(ExecuteAsyncだけを書く)、Streamを継承したクラス。
Chain of Responsibility(責任の連鎖):順番に回す¶
悩み: 「ログ → 認証 → 例外処理 → 本処理」のように、処理を順番に通したい。途中で止めることもある。
app.Use(async (context, next) =>
{
var start = Stopwatch.GetTimestamp();
await next(context); // 次の担当へ回す(回さなければ、ここで止まる)
var ms = Stopwatch.GetElapsedTime(start).TotalMilliseconds;
app.Logger.LogInformation("{Path} {Ms}ms", context.Request.Path, ms);
});
app.UseAuthentication();
app.UseAuthorization();
- .NETの実例: ASP.NET Core のミドルウェア、
HttpClientのDelegatingHandler。Kestrelが受けたリクエストは、このミドルウェアの列を順番に通る。
Command(コマンド):操作を「物」にする¶
悩み: 操作を、あとで実行したい・取り消したい・記録したい。
- .NETの実例: WPF の
ICommand(ボタンと処理をつなぐ)、キューに積むジョブ。
Iterator(イテレーター):中身を1つずつ取り出す¶
IEnumerable<string> ReadLines(string path)
{
foreach (var line in File.ReadLines(path))
if (!string.IsNullOrWhiteSpace(line)) yield return line; // 1行ずつ返す
}
- .NETの実例:
IEnumerable<T>・foreach・yield return、LINQ。
質問:.NETでよく出てくる、GoF以外のパターンは?¶
| パターン | ひとことで | .NETでの例 |
|---|---|---|
| 依存性の注入(DI) | 使う部品を外から渡す | builder.Services.AddScoped<…>()(→ DIとは) |
| Options | 設定を型付きのクラスで受け取る | IOptions<T>、appsettings.json |
| Repository | データの読み書きを1か所に集める | EF Core の DbContext の上に置くことが多い |
| Mediator | クラス同士が直接呼ばず、仲介役を通す | MediatR などのライブラリ(ライセンス条件は要確認) |
質問:使うときの注意は?¶
- 困ってから使う:「将来切り替えるかも」だけでインターフェースや Factory を増やさない。2つ目の実装が必要になったときに入れても遅くない。
- 名前を合わせる:
〜Factory・〜Adapter・〜Decoratorのように、パターン名をクラス名に入れると、読む人に意図が伝わる。 - C# の機能で済むなら、それを使う:Observer は
event、Iterator はyield return、Strategy は小さければFunc<T>(ラムダ)でも十分。 - Singleton は自作しない:テストで差し替えにくくなる。DI の
AddSingletonを使う。
関連メモ¶
確認したいこと¶
- 自分のコードで
if/switchが増え続けている場所はどこか(Strategy の候補) - 古いライブラリや外部APIを直接呼んでいる場所はどこか(Adapter の候補)
- ログ・リトライ・キャッシュを、いろいろなクラスに書き散らしていないか(Decorator・ミドルウェアの候補)