ℹ️
INFO
この記事では、.NETで大規模アプリケーションを開発する際に活用したデザインパターンについて、実践的な視点から解説します。
JavascriptとElectronを使った大規模アプリケーション開発を通じて、デザインパターンやレイヤー構造の重要性を痛感しました。これらは言わば「先人の知恵」であり、非常に強力な武器です。
AIにコードを書かせることができる時代になっても、システム全体の「骨組み」は人間がしっかり設計しないと、プロジェクトは簡単に破綻してしまいます(これも私の苦い経験です……個人的な考えですが、最初に最終的な内部状態を明確に設計することが大事だと感じました。。。。。。これがかなり難しいい。。。。。。)。
そこで今回は、.NETフレームワークでの大規模開発(現在進行中の未リリースの個人開発アプリ)で実際に役立った、以下の3つの重要なデザインパターンについて解説します。
今回扱うデザインパターン
1. クリーンアーキテクチャ(レイヤー分離)
2. MVVMパターン(UIとロジックの分離)
3. Repositoryパターン(データアクセスの抽象化)
コードが複雑化し、「どこを直せばいいのか分からない!」という迷宮入りを防ぐための地図として、最後までお付き合いいただければ幸いです。
ℹ️
INFO
本稿で扱う3つの軸: クリーンアーキテクチャ(レイヤー分離)、MVVM(UIとロジック分離)、Repository(データアクセスの抽象化)。実務で得た導入ステップと効果を短く共有します。
対象読者
- .NETで大規模アプリケーションを開発している、または開発を検討しているエンジニア
- デザインパターンを実践的に学びたい方
- コードの保守性と拡張性を向上させたい方
- MVVMパターンやクリーンアーキテクチャに興味がある方
記事に書いてあること
この記事のゴール
- クリーンアーキテクチャ(レイヤー分離)の実装方法と、なぜレイヤーを分けるのか
- MVVMパターン(Model-View-ViewModel、データと画面を分離する設計手法)の実装と、UIとビジネスロジックを分離する理由
- Repositoryパターン(データアクセスの抽象化)の実装と、データソースを切り替えやすくする方法
- 実際の開発で直面した課題と、パターンを使うことでどう解決したか
- パターンを組み合わせて使うことで、大規模アプリケーションをどう整理したか
なぜデザインパターンが必要だったのか
📝
詰まった課題を解くために導入した順番と効果を整理します。
アプリケーションを開発していると、コードが複雑になり、どこを修正すればいいか分からなくなります。
気づけば、「画面の表示」「データの取得」「ビジネスロジック」が1つのファイルにぎゅっと詰め込まれていて、どこを触れば何が変わるのかパッと見て分かりにくい状態になっていました。
元々JavaScriptで自由に書くことに慣れていて、つい関数を積み重ねていくスタイルが身についていたのですが、.NETのプロジェクトではその手法では通用しない場面が増えてきました。ここで初めて、「デザインパターンで整理する必要がある」と身をもって感じたんです。
これだと、データベースを変更したい時に、画面のコードも一緒に修正する必要があります。画面のデザインを変更したい時に、ビジネスロジックのコードも一緒に修正する必要があります。
変更が1箇所に留まらず、あちこちに影響が広がってしまいます。そこで、デザインパターンを使って、コードを整理することにしました。
(LLMなど駆使して最適なパターンを模索して採用という流れです)
クリーンアーキテクチャ(レイヤー分離)
クリーンアーキテクチャは、アプリケーションを複数のレイヤー(層)に分けて、それぞれの役割を明確にする設計手法です。
レイヤーの構成
アプリケーションを4つのレイヤーに分けました。
4レイヤー構成
- Domain: ビジネスロジックの中核。外部に依存しない最内層。
- Application: ユースケースを定義し、Domain にのみ依存。
- Infrastructure: DB・外部サービス連携。Application に依存。
- Presentation: UI(画面)。Application に依存。
この構成により、内側のレイヤーは外側のレイヤーに依存しません。ビジネスロジックは、UIやデータベースのことを知らなくても動作します。
実装の詳細
実際のコードでは、各レイヤーを別々のプロジェクト(.NETのプロジェクト単位)に分けました。
// Domain/Entities/User.cs
namespace Domain.Entities
{
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
// ビジネスロジック: メールアドレスの検証
public bool IsValidEmail()
{
return !string.IsNullOrEmpty(Email) && Email.Contains("@");
}
}
}
Domainレイヤーは、他のレイヤーに一切依存しません。データベースのことも、UIのことも知りません。純粋なビジネスロジックだけを扱います。
// Application/Interfaces/IUserRepository.cs
namespace Application.Interfaces
{
public interface IUserRepository
{
Task<User> GetByIdAsync(int id);
Task<List<User>> GetAllAsync();
Task<User> CreateAsync(User user);
}
}
Applicationレイヤーは、インターフェース(契約)を定義します。実装は、Infrastructureレイヤーで行います。
// Infrastructure/Repositories/UserRepository.cs
namespace Infrastructure.Repositories
{
public class UserRepository : IUserRepository
{
private readonly DbContext _context;
public UserRepository(DbContext context)
{
_context = context;
}
public async Task<User> GetByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
// 他のメソッドも実装...
}
}
Infrastructureレイヤーは、データベースへのアクセスを実装します。Applicationレイヤーで定義したインターフェースを実装することで、データソースを切り替えやすくなります。
試行錯誤の過程
よくある問題は、変更の影響がドミノ倒しのように広がることです。「データベースを変えただけなのに、なぜか画面のコードまで修正が必要になる」といった状況です。
これを防ぐためのレイヤー設計は、一般的に以下のような思考プロセスをたどります。
分離
1. 初期段階(分離なし・2層)
ロジックの置き場が定まっておらず、UIやデータ層に散らばる。結果、どこを変えても全体に影響が出る。
2. 伝統的な3層構造(UI → Logic → Data)
ロジック層を分けたものの、ロジックがデータ層(詳細)に依存しているため、DB変更の影響をロジックが受けてしまう。
3. クリーンアーキテクチャ(依存性の逆転)
「詳細(UI/DB)がロジックに依存する」形に依存の矢印を反転させる。これにより、最も重要なビジネスロジックを外部の変更から守り切ることができる。
今回採用した4レイヤー(Domain, Application, Infrastructure, Presentation)も、この「ビジネスロジックを核として守る」という目的のために設計されています。
レイヤー分離の効果
レイヤー分離の効果
- 変更の影響範囲を限定: DBを変えてもUIは手を入れないで済む。
- テストが書きやすい: ビジネスロジックをUI/DBに依存せず検証可能。
- 再利用性の向上: 同じロジックを別UI・別データソースで流用できる。
MVVMパターン(UIとビジネスロジックの分離)
MVVMパターン(Model-View-ViewModel、データと画面を分離する設計手法)は、UIとビジネスロジックを分離するためのパターンです。
MVVMの構成要素
📝
MVVMは「View ⇔ ViewModel ⇔ Model」の一方向バインディングで疎結合にするのが肝です。
MVVMは、3つの要素で構成されます。
- Model(モデル): データとビジネスロジック。DomainレイヤーやApplicationレイヤーに相当
- View(ビュー): UI(ユーザーインターフェース、操作画面)。XAML(WPFの画面定義言語)で定義
- ViewModel(ビューモデル): ViewとModelを繋ぐ橋渡し役。Viewの状態を管理し、ModelのデータをViewに適した形に変換
ViewModelは、Viewのことを知りません。Viewは、ViewModelのことを知ります。この一方向の依存関係により、ViewとViewModelを独立して開発・テストできます。
実装の詳細
実際のコードでは、ViewModelでビジネスロジックを処理し、ViewはViewModelの状態を監視して自動的に更新されるようにしました。
// ViewModels/UserViewModel.cs
namespace ViewModels
{
public class UserViewModel : INotifyPropertyChanged
{
private readonly IUserService _userService;
private User _selectedUser;
private ObservableCollection<User> _users;
public User SelectedUser
{
get => _selectedUser;
set
{
_selectedUser = value;
OnPropertyChanged();
}
}
public ObservableCollection<User> Users
{
get => _users;
set
{
_users = value;
OnPropertyChanged();
}
}
public ICommand LoadUsersCommand { get; }
public UserViewModel(IUserService userService)
{
_userService = userService;
LoadUsersCommand = new RelayCommand(async () => await LoadUsersAsync());
}
private async Task LoadUsersAsync()
{
var users = await _userService.GetAllUsersAsync();
Users = new ObservableCollection<User>(users);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ViewModelは、ビジネスロジックを処理し、Viewに表示するデータを管理します。Viewは、ViewModelのプロパティにバインド(結びつける)することで、自動的に更新されます。
<!-- Views/UserView.xaml -->
<Window x:Class="Views.UserView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ユーザー一覧">
<Grid>
<ListBox ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="読み込み"
Command="{Binding LoadUsersCommand}" />
</Grid>
</Window>
Viewは、XAML(WPFの画面定義言語)で定義し、ViewModelのプロパティにバインドします。{Binding Users}と書くだけで、ViewModelのUsersプロパティと自動的に連動します。
データバインディングの仕組み
ℹ️
INFO
INotifyPropertyChanged で ViewModel 側が変化通知を出し、View はバインディングで追従します。XAML 変更なしにロジック側の変更が反映されるのが利点。
データバインディング(データと画面を自動的に連動させる仕組み)により、ViewModelのデータが変更されると、Viewも自動的に更新されます。
ViewModelは、INotifyPropertyChangedインターフェースを実装し、プロパティが変更された時にPropertyChangedイベントを発火します。Viewは、このイベントを監視し、自動的に更新されます。
これにより、ViewModelのコードを変更するだけで、Viewも自動的に更新されます。Viewのコードを変更する必要はありません。
試行錯誤の過程
UI開発における「関心事の分離」は、通常以下のような段階を経て洗練されていきます。
- コードビハインド(Viewにロジック直書き)
ボタンクリック時の処理などをUIファイルに直接書く手法。手軽ですが、「画面を表示しないとテストができず、自動化が困難」という致命的な欠点があります。
- 相互参照の罠(Logic ⇔ View)
ロジックを別クラスに切り出したものの、「ロジックから画面のテキストボックスを直接操作する」実装にしてしまうパターン。これでは結局、ロジックがUIの構造を知っている必要があり、完全な分離になりません。
- MVVMとデータバインディング(View ⇒ ViewModel)
「ロジックは単にデータを更新し、変更通知を出すだけ。画面がそれを勝手に監視して表示を変える」という形にします。
これにより、ViewModel(ロジック)は誰が自分を表示しているかを知る必要がなくなるため、純粋なC#クラスとして単体テストが可能になります。これがMVVMの最大の利点です。
MVVMパターンの効果
MVVMパターンを使うことで、以下のような効果が得られました。
- ViewとViewModelを独立して開発・テストできる: ViewModelは、Viewを表示せずにテストできます
- ビジネスロジックの再利用性が向上する: 同じViewModelを、別のViewでも使えます
- コードの保守性が向上する: ViewとViewModelの役割が明確になり、変更の影響範囲を限定できます
Repositoryパターン(データアクセスの抽象化)
Repositoryパターンは、データアクセスを抽象化し、データソースを切り替えやすくするためのパターンです。
Repositoryパターンの構成
Repositoryパターンは、データアクセスのロジックを、Repository(リポジトリ)というクラスに集約します。
Repositoryは、データの取得・保存・削除などの操作を、統一的なインターフェースで提供します。これにより、データソース(データベース、ファイル、APIなど)を切り替えても、アプリケーションの他の部分は変更する必要がありません。
実装の詳細
実際のコードでは、Repositoryのインターフェースを定義し、実装をInfrastructureレイヤーで行いました。
// Application/Interfaces/IUserRepository.cs
namespace Application.Interfaces
{
public interface IUserRepository
{
Task<User> GetByIdAsync(int id);
Task<List<User>> GetAllAsync();
Task<User> CreateAsync(User user);
Task<User> UpdateAsync(User user);
Task DeleteAsync(int id);
}
}
インターフェースを定義することで、データソースを切り替えても、アプリケーションの他の部分は変更する必要がありません。
// Infrastructure/Repositories/UserRepository.cs
namespace Infrastructure.Repositories
{
public class UserRepository : IUserRepository
{
private readonly DbContext _context;
public UserRepository(DbContext context)
{
_context = context;
}
public async Task<User> GetByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
public async Task<List<User>> GetAllAsync()
{
return await _context.Users.ToListAsync();
}
public async Task<User> CreateAsync(User user)
{
_context.Users.Add(user);
await _context.SaveChangesAsync();
return user;
}
public async Task<User> UpdateAsync(User user)
{
_context.Users.Update(user);
await _context.SaveChangesAsync();
return user;
}
public async Task DeleteAsync(int id)
{
var user = await _context.Users.FindAsync(id);
if (user != null)
{
_context.Users.Remove(user);
await _context.SaveChangesAsync();
}
}
}
}
実装は、Infrastructureレイヤーで行います。データベースへのアクセスを、Repositoryに集約することで、データソースを切り替えやすくなります。
データソースの切り替え
Repositoryパターンを使うことで、データソースを簡単に切り替えられます。
例えば、データベースからファイルに切り替えたい場合、新しいRepositoryを実装するだけで済みます。
// Infrastructure/Repositories/FileUserRepository.cs
namespace Infrastructure.Repositories
{
public class FileUserRepository : IUserRepository
{
private readonly string _filePath;
public FileUserRepository(string filePath)
{
_filePath = filePath;
}
public async Task<User> GetByIdAsync(int id)
{
var users = await LoadUsersFromFileAsync();
return users.FirstOrDefault(u => u.Id == id);
}
public async Task<List<User>> GetAllAsync()
{
return await LoadUsersFromFileAsync();
}
// 他のメソッドも実装...
private async Task<List<User>> LoadUsersFromFileAsync()
{
if (!File.Exists(_filePath))
{
return new List<User>();
}
var json = await File.ReadAllTextAsync(_filePath);
return JsonSerializer.Deserialize<List<User>>(json);
}
}
}
新しいRepositoryを実装するだけで、データソースを切り替えられます。アプリケーションの他の部分は、変更する必要がありません。
試行錯誤の過程
⚠️
WARNING
Repositoryを肥大化させがち/UIやサービスに直接DBアクセスを書きがちなのが失敗パターン。インターフェース分割と実装の隔離で回避します。
データアクセス層の設計も、多くのプロジェクトで似たような成長痛を経験します。
- 直書き(ViewModel/ServiceでSQL実行)
初期は手軽ですが、PostgreSQLからMySQLへ移行するような場面で「全コード修正」という悪夢を見ます。また、単純な「全件取得」のような処理があちこちにコピペされ、DRY原則も崩壊します。
- 神クラス化したRepository(単一クラス)
「とりあえずデータベース処理を1つにまとめよう」とすると、数百のメソッドを持つ巨大なクラスが誕生し、メンテナンス不能になります。
- 機能別Repositoryとインターフェース化(最終形)
UserRepository、ProductRepositoryのようにエンティティ単位で分割し、さらにインターフェース(IUserRepository)を介して利用します。
これにより、アプリ本体は「データの保存先がDBなのか、クラウドなのか、テスト用のメモリなのか」を気にする必要がなくなります。これがRepositoryパターンの完成形です。
Repositoryパターンの効果
Repositoryパターンを使うことで、以下のような効果が得られました。
- データソースを切り替えやすくなる: データベースからファイルに切り替えても、アプリケーションの他の部分は変更する必要がありません
- テストが書きやすくなる: Repositoryのモック(偽の実装)を作成することで、データベースに依存せずにテストできます
- コードの保守性が向上する: データアクセスのロジックがRepositoryに集約され、変更の影響範囲を限定できます
パターンを組み合わせて使う
実際の開発では、これらのパターンを組み合わせて使います。
クリーンアーキテクチャでレイヤーを分け、MVVMパターンでUIとビジネスロジックを分離し、Repositoryパターンでデータアクセスを抽象化します。
これにより、大規模なアプリケーションでも、コードを整理し、保守しやすくすることができます。
実際の開発での活用例
実際の開発では、以下のような構成でパターンを組み合わせました。
- Domainレイヤー: ビジネスロジックの中核。エンティティ(データの単位)と、ビジネスルールを定義
- Applicationレイヤー: ユースケース(アプリケーションの機能)を定義。Repositoryのインターフェースを定義
- Infrastructureレイヤー: データベースや外部サービスとの連携。Repositoryの実装
- Presentationレイヤー: UI(ユーザーインターフェース、操作画面)。ViewModelとViewを定義
この構成により、各レイヤーの役割が明確になり、変更の影響範囲を限定できるようになりました。
パターン組み合わせの効果
組み合わせの効果
- 影響範囲を最小限に: DB変更でもUIは手を入れず、UI変更でもロジックはそのまま。
- テストしやすい: Domainは純粋ロジック、ViewModelはUI非依存、Repositoryはモック差し替えで検証。
- 再利用しやすい: 別UI・別データソースでも同じユースケース/ドメインを流用可能。
- チーム開発が楽: 役割が分かれるので並行作業がしやすく衝突が減る。
使ってみて
-
1
-
2
-
3
-
4
-
5
まとめ
主要なポイント
- クリーンアーキテクチャでレイヤーを分けることで、ビジネスロジックをUIやデータベースから独立させ、変更の影響範囲を限定できる
- MVVMパターン(Model-View-ViewModel、データと画面を分離する設計手法)でUIとビジネスロジックを分離することで、ViewとViewModelを独立して開発・テストできる
- Repositoryパターンでデータアクセスを抽象化することで、データソースを切り替えても、アプリケーションの他の部分は変更する必要がなくなる
- これらのパターンを組み合わせて使うことで、大規模なアプリケーションでも、コードを整理し、保守しやすくすることができる
- パターンを使うことで、テストが書きやすくなり、コードの再利用性と保守性が向上する
- 各レイヤーの役割が明確になることで、チーム開発がしやすくなり、複数の開発者が同時に作業しても衝突が少なくなる
さらに深く学ぶには
最後まで読んでくださりありがとうございます。🎉 大規模なアプリケーションを開発する際に、デザインパターンがどのように役立つかを、実践的な視点からまとめました。同じように大規模アプリケーションと向き合う方の助けになれば嬉しいです。