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
{
});
return result switch
{
RegisterProductResult.Success success => success.Product;
_ => BadRequest(ModelState);
};
}
}
ASP.NET Core MVC を使って、ちゃんとした Web API を実装するとき、最近はこのやり方に落ち着いた。戻り値用の型を定義するのだけはいつも面倒で、TypeScript の Union 型が C# にも欲しいと、しょっちゅう思う。