ℹ️
INFO
この記事では、.NET 10で大規模アプリケーションを開発する際に活用したデザインパターン第3回目を解説します。Registryパターン、Strategyパターン、Observerパターンの実践的な実装例を、実際の開発経験に基づいてまとめました。
大規模アプリケーションを制作していると、コンポーネントの管理、アルゴリズムの切り替え、イベント通知の仕組みが複雑になっていく。最初は直接参照で済ませていたが、規模が大きくなるにつれて、どこで何が使われているか分からなくなった。そんな時、デザインパターンを導入することで、保守性と拡張性が大幅に向上します。
そこで、デザインパターンを導入した。第1回目ではクリーンアーキテクチャ、MVVM、Repositoryパターンを、第2回目ではFacade、Template Method、Builderパターンを解説した。今回は第3回目として、Registryパターン、Strategyパターン、Observerパターンの実装例をまとめる。
⚠️
WARNING
最初は直接参照で済ませていたが、コンポーネントが増えると依存関係が複雑になり、テストが困難になった。パターンを導入することで、保守性と拡張性が大幅に向上した。という経験はないでしょうか?
対象読者
- .NET 10で大規模アプリケーションを開発しているエンジニア
- デザインパターンを実践的に学びたい開発者
- コンポーネント管理、アルゴリズム切り替え、イベント通知の仕組みを改善したい人
- 第1回目・第2回目の記事を読んで、さらにパターンを学びたい方
記事に書いてあること
- Registryパターンによるコンポーネント管理の実装方法と、依存関係の解決
- Strategyパターンによる統計計算アルゴリズムの切り替えと、実行時選択の仕組み
- Observerパターンによるイベント通知システムの構築と、疎結合な設計
- 各パターンの実装例と、実際の開発での試行錯誤の過程
- パターンを組み合わせることで、保守性と拡張性を向上させる方法
前提知識
- .NET 10の基本操作とC#の文法
- デザインパターンの基礎知識(第1回目・第2回目の記事を読んでいることが望ましい)
- 依存性注入(DI)の基本概念
- インターフェースと抽象クラスの使い分け
今回整えたもの
大規模アプリケーションで使用した3つのデザインパターンの実装例。Registryパターンでコンポーネントを一元管理し、Strategyパターンで統計計算アルゴリズムを切り替え、Observerパターンでイベント通知を実現した。
Registryパターン:コンポーネント管理の一元化
大規模アプリケーションでは、多数のコンポーネントが散在し、どこで何が使われているか分からなくなる。直接参照で済ませていたが、コンポーネントが増えると依存関係が複雑になり、テストが困難になった。
そこで、Registryパターンを導入した。コンポーネントを一元管理し、名前で取得できるようにした。これにより、依存関係が明確になり、テストが容易になった。
実装の試行錯誤
最初は単純なDictionaryで実装した。しかし、スレッドセーフでなく、同時アクセスで例外が発生した。次に、ConcurrentDictionaryを使用したが、パフォーマンスが低下した。最終的に、読み取り専用のDictionaryと、書き込み時のロックを組み合わせた実装に落ち着いた。
試行錯誤の過程:
-
1
Dictionary実装:シンプルだが、スレッドセーフでない(例外発生)
-
2
ConcurrentDictionary実装:スレッドセーフだが、パフォーマンスが低下(読み取り時に10%遅延)
-
3
読み取り専用Dictionary + ロック実装:スレッドセーフで、パフォーマンスも良好(読み取り時にオーバーヘッドなし)
実装コード
Registryパターンの実装は、コンポーネントを登録・取得する仕組みを提供する。
// Components/Registry.cs
public class ComponentRegistry
{
private readonly Dictionary<string, object> _components = new();
private readonly object _lock = new();
public void Register<T>(string name, T component) where T : class
{
lock (_lock)
{
_components[name] = component;
}
}
public T Get<T>(string name) where T : class
{
// 読み取り時はロック不要(Dictionaryは読み取り時にスレッドセーフ)
if (_components.TryGetValue(name, out var component) && component is T typed)
{
return typed;
}
throw new KeyNotFoundException($"Component '{name}' not found.");
}
public bool TryGet<T>(string name, out T? component) where T : class
{
if (_components.TryGetValue(name, out var obj) && obj is T typed)
{
component = typed;
return true;
}
component = null;
return false;
}
}
コンポーネントの登録と取得の流れを視覚化したデモを作成した。登録時に名前を指定し、取得時に型を指定することで、型安全なコンポーネント管理を実現している。
依存関係の解決
Registryパターンを使用することで、コンポーネント間の依存関係が明確になる。直接参照ではなく、名前で取得するため、依存関係が疎結合になる。
Registryパターンの利点
- 一元管理:すべてのコンポーネントを一箇所で管理できる
- 型安全:ジェネリクスにより、型安全な取得が可能
- 疎結合:直接参照ではなく、名前で取得するため、依存関係が疎結合になる
- テスト容易性:モックコンポーネントを登録することで、テストが容易になる
Strategyパターン:統計計算アルゴリズムの切り替え
統計計算では、平均値、中央値、最頻値など、複数のアルゴリズムを切り替える必要がある。最初はif文で分岐していたが、アルゴリズムが増えるとコードが複雑になった。
そこで、Strategyパターンを導入した。各アルゴリズムを独立したクラスとして実装し、実行時に切り替えられるようにした。これにより、新しいアルゴリズムを追加する際も、既存のコードを変更する必要がなくなった。
実装の試行錯誤
最初は抽象クラスで実装した。しかし、インターフェースの方が柔軟性が高いと判断し、インターフェースに変更した。また、アルゴリズムの選択を設定ファイルで行えるようにしたが、実行時に切り替えられるようにした方が実用的だと判断した。
試行錯誤の過程:
-
1
抽象クラス実装:基本実装を提供できるが、多重継承ができない
-
2
インターフェース実装:柔軟性が高いが、共通実装を提供できない
-
3
インターフェース + デフォルト実装:柔軟性と実装の両立(.NET 8以降)
実装コード
Strategyパターンの実装は、統計計算アルゴリズムをインターフェースで定義し、各アルゴリズムを独立したクラスとして実装する。
// Statistics/IStatisticsStrategy.cs
public interface IStatisticsStrategy
{
double Calculate(IEnumerable<double> values);
string Name { get; }
}
// Statistics/MeanStrategy.cs
public class MeanStrategy : IStatisticsStrategy
{
public string Name => "平均値";
public double Calculate(IEnumerable<double> values)
{
var list = values.ToList();
return list.Count > 0 ? list.Average() : 0;
}
}
// Statistics/MedianStrategy.cs
public class MedianStrategy : IStatisticsStrategy
{
public string Name => "中央値";
public double Calculate(IEnumerable<double> values)
{
var sorted = values.OrderBy(x => x).ToList();
var count = sorted.Count;
if (count == 0) return 0;
if (count % 2 == 0)
{
return (sorted[count / 2 - 1] + sorted[count / 2]) / 2.0;
}
return sorted[count / 2];
}
}
// Statistics/StatisticsCalculator.cs
public class StatisticsCalculator
{
private IStatisticsStrategy _strategy;
public StatisticsCalculator(IStatisticsStrategy strategy)
{
_strategy = strategy;
}
public void SetStrategy(IStatisticsStrategy strategy)
{
_strategy = strategy;
}
public double Calculate(IEnumerable<double> values)
{
return _strategy.Calculate(values);
}
}
アルゴリズムの切り替えと、計算結果の比較を視覚化したデモを作成した。実行時にアルゴリズムを選択し、同じデータに対して異なる結果を取得できる。
アルゴリズムの拡張
Strategyパターンを使用することで、新しいアルゴリズムを追加する際も、既存のコードを変更する必要がない。インターフェースを実装する新しいクラスを追加するだけで、アルゴリズムを拡張できる。
Strategyパターンの利点
新しいアルゴリズムを追加する際、既存のコードを変更する必要がない。インターフェースを実装する新しいクラスを追加するだけで、アルゴリズムを拡張できる。これにより、オープン・クローズド原則(OCP)を満たす設計が実現できる。
Observerパターン:イベント通知の仕組み
大規模アプリケーションでは、複数のコンポーネントがイベントを監視し、通知を受け取る必要がある。最初は直接呼び出しで済ませていたが、コンポーネントが増えると依存関係が複雑になり、保守が困難になった。
そこで、Observerパターンを導入した。イベントを発行するSubjectと、イベントを監視するObserverを分離し、疎結合な設計を実現した。これにより、新しいObserverを追加する際も、既存のコードを変更する必要がなくなった。
実装の試行錯誤
最初は.NET標準のイベントを使用した。しかし、カスタムイベントが必要になり、独自の実装に変更した。また、非同期通知が必要になり、Taskベースの実装に変更した。
試行錯誤の過程:
-
1
.NET標準イベント:シンプルだが、カスタマイズが困難
-
2
独自実装(同期的):カスタマイズ可能だが、ブロッキングが発生
-
3
独自実装(非同期):カスタマイズ可能で、非同期通知が可能(最終実装)
実装コード
Observerパターンの実装は、イベントを発行するSubjectと、イベントを監視するObserverを分離する。
// Events/IEventObserver.cs
public interface IEventObserver<T>
{
Task OnEventAsync(T eventData);
}
// Events/EventSubject.cs
public class EventSubject<T>
{
private readonly List<IEventObserver<T>> _observers = new();
private readonly object _lock = new();
public void Subscribe(IEventObserver<T> observer)
{
lock (_lock)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
}
}
}
public void Unsubscribe(IEventObserver<T> observer)
{
lock (_lock)
{
_observers.Remove(observer);
}
}
public async Task NotifyAsync(T eventData)
{
List<IEventObserver<T>> observers;
lock (_lock)
{
observers = new List<IEventObserver<T>>(_observers);
}
var tasks = observers.Select(obs => obs.OnEventAsync(eventData));
await Task.WhenAll(tasks);
}
}
// Events/DataChangedEvent.cs
public class DataChangedEvent
{
public string Source { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
public object? Data { get; set; }
}
// Events/LoggingObserver.cs
public class LoggingObserver : IEventObserver<DataChangedEvent>
{
public async Task OnEventAsync(DataChangedEvent eventData)
{
await Task.Run(() =>
{
Console.WriteLine($"[{eventData.Timestamp:yyyy-MM-dd HH:mm:ss}] {eventData.Source}: Data changed");
});
}
}
イベント通知の流れと、複数のObserverが通知を受け取る様子を視覚化したデモを作成した。Subjectがイベントを発行すると、すべてのObserverに非同期で通知される。
イベント通知の拡張
Observerパターンを使用することで、新しいObserverを追加する際も、既存のコードを変更する必要がない。インターフェースを実装する新しいクラスを追加し、Subjectに登録するだけで、イベント通知を受け取れる。
Observerパターンの利点
- 疎結合:SubjectとObserverが直接依存しないため、疎結合な設計が実現できる
- 拡張性:新しいObserverを追加する際も、既存のコードを変更する必要がない
- 非同期通知:Taskベースの実装により、非同期でイベント通知が可能
- 複数監視:1つのSubjectに対して、複数のObserverを登録できる
パターンの組み合わせ
実際の開発では、複数のパターンを組み合わせて使用することが多い。Registryパターンでコンポーネントを管理し、Strategyパターンでアルゴリズムを切り替え、Observerパターンでイベント通知を実現する。
実装例
RegistryパターンでStrategyを管理し、Observerパターンで計算結果を通知する実装例。
// Services/StatisticsService.cs
public class StatisticsService
{
private readonly ComponentRegistry _registry;
private readonly EventSubject<DataChangedEvent> _eventSubject;
public StatisticsService(ComponentRegistry registry, EventSubject<DataChangedEvent> eventSubject)
{
_registry = registry;
_eventSubject = eventSubject;
}
public async Task<double> CalculateAsync(string strategyName, IEnumerable<double> values)
{
if (_registry.TryGet<IStatisticsStrategy>(strategyName, out var strategy))
{
var result = strategy.Calculate(values);
await _eventSubject.NotifyAsync(new DataChangedEvent
{
Source = "StatisticsService",
Timestamp = DateTime.Now,
Data = new { Strategy = strategyName, Result = result }
});
return result;
}
throw new KeyNotFoundException($"Strategy '{strategyName}' not found.");
}
}
パターンを組み合わせることで、保守性と拡張性が大幅に向上する。各パターンが独立しているため、個別にテストや拡張が可能になる。
まとめ
今回は、.NET 10で大規模アプリケーションを開発する際に活用したデザインパターン第3回目として、Registryパターン、Strategyパターン、Observerパターンの実装例を解説しました。
ポイントは以下の3つ:
- Registryパターン:コンポーネントを一元管理し、依存関係を明確にする。読み取り専用Dictionaryとロックを組み合わせることで、スレッドセーフでパフォーマンスの良い実装を実現。
- Strategyパターン:統計計算アルゴリズムを切り替え、実行時に選択できるようにする。インターフェースとデフォルト実装を組み合わせることで、柔軟性と実装の両立を実現。
- Observerパターン:イベント通知を疎結合に実現し、複数のObserverが通知を受け取れるようにする。Taskベースの非同期実装により、パフォーマンスと拡張性を両立。
実際の開発では、複数のパターンを組み合わせて使用することが多い。各パターンが独立しているため、個別にテストや拡張が可能になり、保守性と拡張性が大幅に向上する。
第1回目・第2回目の記事と合わせて、大規模アプリケーション開発で役立つデザインパターンの実践的な知識を提供できたと思います。同じような大規模アプリケーション開発を考えている方の参考になれば嬉しいです。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
最後まで読んでくださり、ありがとうございました。
ℹ️
INFO
今回、3回に分けて解説した内容はすべて、現在開発中の大規模アプリケーションで実際に使用している「実践的な知恵」です。
このアプリケーションも完成間近となりました。リリース時には本サイトにて告知いたしますので、もしご興味があればぜひご覧ください(有料販売を予定しております)。