1人でフロントエンドとバックエンドの両方を開発していたら、DDD と軽量 DDD の区別が付かないような状況になってきた。個人的には軽量 DDD 上等。ただ、アーキテクチャに関して言えば、クリーンアーキテクチャはやり過ぎ感あるので、シンプルなレイヤーアーキテクチャを好んで採用している。
実践 DDD で紹介されているようなレイヤーアーキテクチャだと、アプリケーション層にあたるのがアプリケーションサービスで、プレゼンテーション層の Controller から呼び出すことになる。このアプリケーションサービス、責任多くなりがち。仮に1メソッドが1ユースケースだとしても、アプリケーションサービスにユースケースを詰め込む形になって、自分の中でしっくりこなかった。
そこで試行錯誤してたどり着いたのが、ユースケースをクラスとして切り出すやり方。1 ユースケース 1 クラス 1 メソッド 。これも、単一責任の原則と言えなくもない、かも。
public class RegisterProductUseCase { public async ValueTask<RegisterProductResult> ExecuteAsync( RegisterProductCommand command) { // いろいろやるけど紙面の都合で省略 return new RegisterProductResult.Success { Product = product, // いろいろやって登録できた製品をセット }; } } public abstract class RegisterProductResult { private RegisterProductResult() { } public sealed class Success : RegisterProductResult { public Product Product { get; init; } } public sealed class Error : RegisterProductResult { // 紙面の都合で省略 } }
ASP.NET Core MVC なら、DI コンテナに登録したユースケースを、FromServices で引数として受け取ることができる。コンストラクタインジェクションだと、コンストラクタの引数が増えがちだったけど、必要としているメソッドの引数で受け取ればスッキリ。
[Route("/products")] [ApiController] [Authorize] public class ProductsController : ControllerBase { [HttpPost] public async Task<ActionResult<Product>> RegisterProduct( [FromBody] ProductInputModel model, [FromServices] RegisterProductUseCase useCase) { var result = await useCase.ExecuteAsync(new RegisterProductCommand { // init でいろいろ詰める }); return result switch { RegisterProductResult.Success success => success.Product; _ => BadRequest(ModelState); // 紙面の都合で手抜き }; } }
ASP.NET Core MVC を使って、ちゃんとした Web API を実装するとき、最近はこのやり方に落ち着いた。戻り値用の型を定義するのだけはいつも面倒で、TypeScript の Union 型が C# にも欲しいと、しょっちゅう思う。