はじめに
この記事は、小規模な Web アプリケーション開発において、
私が実際に採用している設計の考え方を整理した連載の一部です。
大規模アーキテクチャや厳密な DDD を前提にせず、
「まずはシンプルに作り、必要になったら分ける」設計を重視しています。
この記事では、C# の拡張メソッドを使って
Entity / Model の責務を Command / Query / Utility に分ける考え方と実装例を紹介します。
1. 拡張メソッドの基本的な考え方
- EntityやModel自体に処理を直接書かず、拡張メソッドとして責務を分離
- namespaceやフォルダで整理することで、アクセス制御や依存関係を明確化
- 小規模案件でも保守性・理解性を向上
2. OrderEntity の拡張メソッド例
namespace Entity.Query;
public static class OrderQuery
{
// Query: データ取得
public static Order GetById(this DbContext db, int id)
{
return db.Orders.FirstOrDefault(o => o.Id == id);
}
}
namespace Entity.Command;
public static class OrderCommand
{
// Command: データ更新
public static void UpdateStatus(this DbContext db, Order request)
{
request.Validate(); // 失敗したら例外
// 実案件では要件に応じて追加検討
var target = db.GetById(request.Id);
target.Status = request.Status;
db.SaveChanges();
}
//note: 例では void ですが、成功/失敗の bool を返す場面も多いです
}
※ Commandでは、DTOや画面入力をそのまま渡すのではなく
「更新したい状態を持ったEntity」を引数にすることで
呼び出し側の責務を最小限にしています。
namespace Entity.Utility;
public static class OrderUtility
{
// DTO変換
public static OrderDto ToDto(this Order order)
{
return new OrderDto
{
Id = order.Id,
Status = order.Status,
Amount = order.Amount
};
}
// バリデーション(例外版)
public static void Validate(this Order order)
{
if (order == null)
throw new ArgumentNullException(nameof(order), "Order is null.");
if (order.Id <= 0)
throw new ArgumentException("Invalid Order Id.", nameof(order.Id));
if (string.IsNullOrWhiteSpace(order.Status))
throw new ArgumentException("Status is required.", nameof(order.Status));
if (order.Amount < 0)
throw new ArgumentException("Amount cannot be negative.", nameof(order.Amount));
}
}
※ 現状は例外を投げるスタイルですが、
戻り値やエラーリストでの検証することもあります
バリデーション(エラーリスト版)
// バリデーション(エラーリスト版)
public static bool Validate(this Order order, out List<string> errors)
{
errors = new List<string>();
if (order == null)
errors.Add("Order is null.");
if (order.Id <= 0)
errors.Add("Invalid Order Id.");
if (string.IsNullOrWhiteSpace(order.Status))
errors.Add("Status is required.");
if (order.Amount < 0)
errors.Add("Amount cannot be negative.");
return errors.Count == 0;
}
3. Command / Query / Utility の分け方
| 種類 | 主な役割 | 記述例 |
|---|---|---|
| Query | データ取得、参照専用 | GetById() |
| Command | データ更新・登録・削除 | UpdateStatus() |
| Utility | DTO変換や便利メソッド | ToDto(), Validate() |
ポイント
- 同じEntityでも、参照用処理と更新処理を明確に分ける
- Utilityは副作用なしで純粋にデータ変換や計算のみ
- namespaceやフォルダで分けると、間違ってCommandをQuery側で呼んでしまうことを防げる
- Validate は例外を投げる実装例を示していますが、戻り値で成否を返す方法や、エラーリストを返す方法も可能です
4. namespaceでの責務分離
[ Entity Project ]
├─ Entity
├─ Entity.Command
├─ Entity.Query
└─ Entity.Utility
- Command/Query/Utilityでnamespaceを分ける
- これにより、開発者が参照できる範囲を制限できる
- 新しい概念やドメインロジックを追加する場合も、namespace単位で整理可能
5. メリットと注意点
| 観点 | メリット | デメリット / 注意点 |
|---|---|---|
| Entityの責務 | Entity本体を薄く保てる | 本来Entityに含めるべきロジックが分散しすぎる可能性 |
| コードの見通し | Command / Query / Utility で整理され、役割が明確 | namespaceやフォルダ構成が複雑になると逆に迷いやすい |
| 小規模案件での効果 | シンプルに始められ、保守性が向上 | チームメンバーが慣れていないと理解コストが増える |
| 拡張メソッドの利点 | 既存クラスを変更せず機能追加できる | 過度に利用すると「どこに処理があるか」追いづらい |
| 移行性 | 将来的にService層やUseCase層へ切り出しやすい | 大規模化すると責務境界が曖昧になり、再設計が必要 |
| バリデーション | 例外版・エラーリスト版など柔軟に選べる | 実装スタイルが統一されないと混乱を招く |
6. まとめ
- 拡張メソッドを使った責務分離は、小規模C# MVC設計でも有効
- Command / Query / Utility に整理し、namespaceでアクセス制御
- 将来的な層追加(UseCase層やService層)にも柔軟に対応可能
💡 ポイント
- Query: 読み取り専用
- Command: データ変更
- Utility: 純粋関数的な処理
- namespaceで責務を分けることが保守性のカギ