ℹ️
INFO
この記事は、.NETで大規模アプリケーションを開発する際に活用したデザインパターンについて、実践的な視点から解説するシリーズの第2回です。
第1回では、クリーンアーキテクチャ(レイヤー分離)、MVVMパターン(UIとビジネスロジックの分離)、Repositoryパターン(データアクセスの抽象化)を紹介しました。
今回は、次の3つのパターンを紹介します。Facadeパターン(チャート生成システム)、Template Methodパターン(チャート生成基底クラス)、Builderパターン(コンテキストメニュー構築)です。
対象読者
- .NETで大規模アプリケーションを開発している、または開発を検討しているエンジニア
- デザインパターンを実践的に学びたい方
- コードの保守性と拡張性を向上させたい方
- Facadeパターン、Template Methodパターン、Builderパターンに興味がある方
記事に書いてあること
- Facadeパターン(複雑なサブシステムを単純なインターフェースで隠蔽する設計手法)の実装方法と、チャート生成システムでの活用例
- Template Methodパターン(アルゴリズムの骨組みを定義し、具体的な処理をサブクラスに委譲する設計手法)の実装と、チャート生成基底クラスでの活用例
- Builderパターン(複雑なオブジェクトを段階的に構築する設計手法)の実装と、コンテキストメニュー構築での活用例
- 実際の開発で直面した課題と、パターンを使うことでどう解決したか
- パターンを組み合わせて使うことで、大規模アプリケーションをどう整理したか
なぜこれらのパターンが必要だったのか
大規模なアプリケーションを開発していると、複雑な処理を扱う場面が増えてきます。今(2025年12月未リリースのアプリケーション)で実際に使ったデザインパターンをご紹介します。
チャートを生成する処理では、データの取得、データの変換、チャートの描画、スタイルの適用など、多くの処理が必要です。これらをすべて1つのクラスに書くと、コードが複雑になり、どこを修正すればいいか分からなくなります。
また、コンテキストメニューを構築する処理では、メニュー項目の追加、区切り線の挿入、アイコンの設定、イベントハンドラーの登録など、多くの設定が必要です。これらをすべて1つのメソッドに書くと、コードが長くなり、読みづらくなります。
そこで、デザインパターンを使って、コードを整理することにしました。
Facadeパターン(チャート生成システム)
Facadeパターンは、複雑なサブシステム(複数のクラスやモジュールで構成される処理の集合)を、単純なインターフェース(外部との接続部分)で隠蔽するためのパターンです。
Facadeパターンの構成
チャート生成システムでは、以下のような複雑な処理が必要です。
-
1
データの取得: データベースやAPIからデータを取得
-
2
データの変換: 取得したデータをチャート用の形式に変換
-
3
チャートの描画: 変換したデータを基にチャートを描画
-
4
スタイルの適用: チャートにスタイル(色、フォント、レイアウトなど)を適用
これらをすべて1つのクラスに書くと、コードが複雑になります。そこで、Facadeパターンを使って、これらの処理を1つのシンプルなインターフェースで提供します。
実装の詳細
実際のコードでは、ChartFacadeクラスを作成し、複雑な処理を隠蔽しました。
// Services/ChartFacade.cs
namespace Services
{
public class ChartFacade
{
private readonly IDataService _dataService;
private readonly IDataTransformer _dataTransformer;
private readonly IChartRenderer _chartRenderer;
private readonly IStyleApplier _styleApplier;
public ChartFacade(
IDataService dataService,
IDataTransformer dataTransformer,
IChartRenderer chartRenderer,
IStyleApplier styleApplier)
{
_dataService = dataService;
_dataTransformer = dataTransformer;
_chartRenderer = chartRenderer;
_styleApplier = styleApplier;
}
public Chart GenerateChart(ChartRequest request)
{
// 1. データの取得
var rawData = _dataService.GetData(request.DataSource);
// 2. データの変換
var chartData = _dataTransformer.Transform(rawData, request.ChartType);
// 3. チャートの描画
var chart = _chartRenderer.Render(chartData);
// 4. スタイルの適用
_styleApplier.ApplyStyle(chart, request.Style);
return chart;
}
}
}
ChartFacadeクラスは、複雑な処理を1つのGenerateChartメソッドで提供します。呼び出し側は、ChartRequestを渡すだけで、チャートを生成できます。
// ViewModels/ChartViewModel.cs
namespace ViewModels
{
public class ChartViewModel
{
private readonly ChartFacade _chartFacade;
public ChartViewModel(ChartFacade chartFacade)
{
_chartFacade = chartFacade;
}
public void GenerateChart()
{
var request = new ChartRequest
{
DataSource = "SalesData",
ChartType = ChartType.Line,
Style = new ChartStyle { Colors = new[] { "#FF5733", "#33FF57" } }
};
var chart = _chartFacade.GenerateChart(request);
// チャートを表示
}
}
}
呼び出し側は、ChartFacadeのGenerateChartメソッドを呼び出すだけで、複雑な処理を実行できます。データの取得、変換、描画、スタイル適用の詳細を知る必要はありません。
試行錯誤の過程
複雑なサブシステムを実装する際、コードの設計はしばしば以下のような変遷をたどります。
- Fat ViewModel(何でも屋)
「チャート生成」という処理を全てViewModelの一箇所に記述してしまう段階。手っ取り早いですが、コード量が膨れ上がり、他の場所での再利用も困難になります。
- クラスの散逸(バラバラな部品)
「データ取得クラス」「描画クラス」「変換クラス」と機能を分割した段階。クラス単位では整理されましたが、利用側(ViewModel)がそれら全てのクラスの正しい呼び出し順序や依存関係を知っておく必要があり、使い勝手が悪化します。
- Facadeパターン(窓口の統一)
バラバラになった部品を束ねる「窓口(Facade)」を用意する最終形。
複雑な内部連携はこのFacadeの中に閉じ込め、外部にはGenerateChart()というたった一つのシンプルなボタンだけを提供します。これにより、利用側は裏側の複雑さを一切知ることなく機能を享受できるようになります。
Facadeパターンの効果
Facadeパターンを使うことで、以下のような効果が得られました。
- コードの複雑さを隠蔽できる: 呼び出し側は、複雑な処理の詳細を知る必要がありません
- コードの再利用性が向上する: チャート生成のロジックを、別の場所でも使えます
- コードの保守性が向上する: チャート生成の処理を変更しても、呼び出し側は変更する必要がありません
Template Methodパターン(チャート生成基底クラス)
Template Methodパターンは、アルゴリズムの骨組み(処理の流れ)を定義し、具体的な処理をサブクラス(継承したクラス)に委譲するためのパターンです。
Template Methodパターンの構成
チャート生成では、以下のような処理の流れがあります。
-
1
データの取得: データソースからデータを取得
-
2
データの変換: 取得したデータをチャート用の形式に変換
-
3
チャートの描画: 変換したデータを基にチャートを描画
-
4
スタイルの適用: チャートにスタイルを適用
この処理の流れは、すべてのチャートタイプ(折れ線グラフ、棒グラフ、円グラフなど)で同じです。でも、具体的な処理(データの変換方法、描画方法など)は、チャートタイプによって異なります。
そこで、Template Methodパターンを使って、処理の流れを基底クラスで定義し、具体的な処理をサブクラスで実装します。
実装の詳細
実際のコードでは、ChartGeneratorBaseクラスを作成し、処理の流れを定義しました。
// Services/ChartGeneratorBase.cs
namespace Services
{
public abstract class ChartGeneratorBase
{
// Template Method: 処理の流れを定義
public Chart Generate(ChartRequest request)
{
// 1. データの取得
var rawData = GetData(request.DataSource);
// 2. データの変換(サブクラスで実装)
var chartData = TransformData(rawData, request.ChartType);
// 3. チャートの描画(サブクラスで実装)
var chart = RenderChart(chartData);
// 4. スタイルの適用
ApplyStyle(chart, request.Style);
return chart;
}
// 共通処理: データの取得
protected virtual Data GetData(string dataSource)
{
// デフォルトの実装
return DataService.GetData(dataSource);
}
// 抽象メソッド: サブクラスで実装
protected abstract ChartData TransformData(Data rawData, ChartType chartType);
protected abstract Chart RenderChart(ChartData chartData);
// 共通処理: スタイルの適用
protected virtual void ApplyStyle(Chart chart, ChartStyle style)
{
// デフォルトの実装
StyleApplier.ApplyStyle(chart, style);
}
}
}
基底クラスは、処理の流れをGenerateメソッドで定義します。具体的な処理(TransformData、RenderChart)は、抽象メソッドとして定義し、サブクラスで実装します。
// Services/LineChartGenerator.cs
namespace Services
{
public class LineChartGenerator : ChartGeneratorBase
{
protected override ChartData TransformData(Data rawData, ChartType chartType)
{
// 折れ線グラフ用のデータ変換
return new ChartData
{
Points = rawData.Select(d => new Point(d.X, d.Y)).ToList(),
Type = ChartType.Line
};
}
protected override Chart RenderChart(ChartData chartData)
{
// 折れ線グラフの描画
var chart = new Chart();
foreach (var point in chartData.Points)
{
chart.AddPoint(point);
}
chart.DrawLine();
return chart;
}
}
}
サブクラスは、基底クラスの抽象メソッドを実装します。これにより、処理の流れは同じで、具体的な処理だけが異なる実装ができます。
試行錯誤の過程
アルゴリズムの共通化とバリエーション管理は、以下のような段階を経て最適化されます。
- コピペ量産(各クラスが独立)
LineChart、BarChartと個別にクラスを作り、似たような「データ取得」「前処理」コードをコピー&ペーストしてしまう段階。修正漏れが頻発します。
- 共通処理の切り出し(単なる継承/インターフェース)
共通部分を親クラスにまとめますが、手順ごとの呼び出し順序(レシピ)まで強制できていない状態。開発者によって「初期化の前に描画を呼んでしまう」といったミスが起こり得ます。
- Template Methodパターン(レシピの固定)
親クラスで「料理の手順(アルゴリズムの骨格)」を完全に定義し、固定してしまう最終形。
「下ごしらえ」や「盛り付け」といった具体的な作業内容だけを子クラスに任せることで、誰が実装しても必ず正しい手順で処理が実行されるようになります。
Template Methodパターンの効果
Template Methodパターンを使うことで、以下のような効果が得られました。
- コードの重複を削減できる: 共通処理を基底クラスに集約し、コードの重複を削減できます
- 処理の流れを強制できる: 基底クラスで処理の流れを定義することで、サブクラスが正しい順序で処理を実行できます
- コードの拡張性が向上する: 新しいチャートタイプを追加する時は、基底クラスを継承して、抽象メソッドを実装するだけで済みます
Builderパターン(コンテキストメニュー構築)
Builderパターンは、複雑なオブジェクトを段階的に構築するためのパターンです。
Builderパターンの構成
コンテキストメニューを構築する処理では、以下のような多くの設定が必要です。
-
1
メニュー項目の追加: 「コピー」「貼り付け」「削除」などのメニュー項目を追加
-
2
区切り線の挿入: メニュー項目の間に区切り線を挿入
-
3
アイコンの設定: メニュー項目にアイコンを設定
-
4
イベントハンドラーの登録: メニュー項目をクリックした時の処理を登録
-
5
ショートカットキーの設定: メニュー項目にショートカットキーを設定
これらをすべて1つのメソッドに書くと、コードが長くなり、読みづらくなります。また、メニューの構成が複雑になると、コードが複雑になります。
そこで、Builderパターンを使って、メニューを段階的に構築します。
実装の詳細
実際のコードでは、ContextMenuBuilderクラスを作成し、メニューを段階的に構築しました。
// Builders/ContextMenuBuilder.cs
namespace Builders
{
public class ContextMenuBuilder
{
private readonly ContextMenu _menu;
public ContextMenuBuilder()
{
_menu = new ContextMenu();
}
public ContextMenuBuilder AddItem(string text, Action action)
{
var item = new MenuItem
{
Text = text,
Command = new RelayCommand(action)
};
_menu.Items.Add(item);
return this;
}
public ContextMenuBuilder AddSeparator()
{
_menu.Items.Add(new Separator());
return this;
}
public ContextMenuBuilder WithIcon(string iconPath)
{
if (_menu.Items.Count > 0)
{
var lastItem = _menu.Items[_menu.Items.Count - 1] as MenuItem;
if (lastItem != null)
{
lastItem.Icon = new BitmapImage(new Uri(iconPath));
}
}
return this;
}
public ContextMenuBuilder WithShortcut(Key key, ModifierKeys modifiers)
{
if (_menu.Items.Count > 0)
{
var lastItem = _menu.Items[_menu.Items.Count - 1] as MenuItem;
if (lastItem != null)
{
lastItem.InputGestureText = $"{modifiers}+{key}";
}
}
return this;
}
public ContextMenu Build()
{
return _menu;
}
}
}
ContextMenuBuilderクラスは、メソッドチェーン(メソッドを連続して呼び出す書き方)で、メニューを段階的に構築します。
// ViewModels/MainViewModel.cs
namespace ViewModels
{
public class MainViewModel
{
public ContextMenu CreateContextMenu()
{
return new ContextMenuBuilder()
.AddItem("コピー", OnCopy)
.WithIcon("Icons/Copy.png")
.WithShortcut(Key.C, ModifierKeys.Control)
.AddItem("貼り付け", OnPaste)
.WithIcon("Icons/Paste.png")
.WithShortcut(Key.V, ModifierKeys.Control)
.AddSeparator()
.AddItem("削除", OnDelete)
.WithIcon("Icons/Delete.png")
.WithShortcut(Key.Delete, ModifierKeys.None)
.Build();
}
private void OnCopy() { /* コピー処理 */ }
private void OnPaste() { /* 貼り付け処理 */ }
private void OnDelete() { /* 削除処理 */ }
}
}
メソッドチェーンで、メニューを段階的に構築できます。コードが読みやすく、メニューの構成が一目で分かります。
試行錯誤の過程
複雑なオブジェクト構築(今回のコンテキストメニューのように設定項目が多いもの)は、可読性と管理のバランスが崩れやすい箇所です。
- スパゲッティコード(巨大な初期化メソッド)
new Menu(), item.Text = ..., item.Icon = ..., menu.Add(item)... といった手続き的なコードが延々と続く状態。設定漏れがあっても気づきにくく、後から読むのも苦痛です。
- ヘルパーメソッド(引数地獄)
AddMenuItem(menu, "Text", icon, action, shortcut...) のようにメソッド化を試みますが、引数が多すぎてどれが何の設定かわからなくなります(テレスコピックコンストラクタ問題)。
- Builderパターン(Fluent Interface)
「ビルダー」という専用の組み立て役を用意し、AddItem().WithIcon().WithShortcut()のように会話形式(メソッドチェーン)で記述する最終形。
コード自体が「何を作っているか」を語るようになるため、コメントなしでも構造が理解できるようになります。これがモダンなライブラリでBuilderが多用される理由です。
Builderパターンの効果
Builderパターンを使うことで、以下のような効果が得られました。
- コードが読みやすくなる: メソッドチェーンで、メニューの構成が一目で分かります
- コードの柔軟性が向上する: メニューの構成を、柔軟に変更できます
- コードの保守性が向上する: メニューの構築ロジックを、1つのクラスに集約できます
パターンを組み合わせて使う
実際の開発では、これらのパターンを組み合わせて使います。
Facadeパターンで複雑な処理を隠蔽し、Template Methodパターンで処理の流れを定義し、Builderパターンでオブジェクトを段階的に構築します。
これにより、大規模なアプリケーションでも、コードを整理し、保守しやすくすることができます。
実際の開発での活用例
実際の開発では、以下のような構成でパターンを組み合わせました。
-
1
Facadeパターン: チャート生成システムで、複雑な処理を1つのシンプルなインターフェースで提供
-
2
Template Methodパターン: チャート生成基底クラスで、処理の流れを定義し、具体的な処理をサブクラスで実装
-
3
Builderパターン: コンテキストメニュー構築で、メニューを段階的に構築
この構成により、コードの複雑さを隠蔽し、処理の流れを強制し、オブジェクトを柔軟に構築できるようになりました。
まとめ
主要なポイント
- Facadeパターン(複雑なサブシステムを単純なインターフェースで隠蔽する設計手法)で複雑な処理を隠蔽することで、呼び出し側は複雑な処理の詳細を知る必要がなくなる
- Template Methodパターン(アルゴリズムの骨組みを定義し、具体的な処理をサブクラスに委譲する設計手法)で処理の流れを定義することで、コードの重複を削減し、処理の流れを強制できる
- Builderパターン(複雑なオブジェクトを段階的に構築する設計手法)でオブジェクトを段階的に構築することで、コードが読みやすく、柔軟に変更できる
- これらのパターンを組み合わせて使うことで、大規模なアプリケーションでも、コードを整理し、保守しやすくすることができる
- パターンを使うことで、コードの再利用性、拡張性、保守性が向上する
さらに深く学ぶには
最後まで読んでくださりありがとうございます。🎉 大規模なアプリケーションを開発する際に、デザインパターンがどのように役立つかを、実践的な視点からまとめました。同じように大規模アプリケーションと向き合う方の助けになれば嬉しいです。