🌊
Clean Architectureソース解説編(後半) →記述中
はじめに
クリーンアーキテクチャをソースコードを読み解き説明します。
以下のソースを参考にさせていただきます。
記事の構成
- Clean Architecture超概要編(前半)
章構成: 要約 = 手段 + 効果 - Clean Architecture概要編(中半)
章構成: 手段の説明 - Clean Architectureソース解説編(後半)
今ここ👆
アプリケーション構成
プロジェクト構成(フォルダ)は以下のようになっています。
ディレクトリ名 | 役割 |
---|---|
Application | ユースケースとコントローラが混在。ユースケースはビジネスロジックを実装し、コントローラはユーザーの入力をユースケースにルーティング |
Common | 共通のユーティリティやヘルパークラスを格納 |
Diagrams | プロジェクトの構造や設計を説明する図表を格納 |
Domain | ビジネスロジックとエンティティを格納。各ドメインモデルを定義 |
Infrastructure | データベース、ファイルシステム、ウェブサービスなどとのインタラクションを管理 |
Persistence | データの永続性とデータストアとのインタラクションを管理 |
Presentation | ユーザーインターフェースとユーザー体験を管理。ビューとそのロジックを格納 |
Service | 外部サービスとのインタラクションを管理 |
Specification | アプリケーションのテストを格納。単体テスト、統合テスト、エンドツーエンドテストを格納 |
細かいフォルダの責務の説明はいかに記述しているため、都度参考にしてください。
clean-architecture-demo
├─Application
│ ├─Customers
│ │ └─Queries : 顧客情報の取得クエリ
│ ├─Employees
│ │ └─Queries : 従業員情報の取得クエリ
│ ├─Interfaces : アプリケーション層のサービスインターフェース
│ ├─Products
│ │ └─Queries : 製品情報の取得クエリ
│ ├─Properties : アプリケーション層のプロジェクト設定
│ └─Sales
│ ├─Commands : 販売情報の作成コマンド
│ └─Queries : 販売情報の取得クエリ
├─Common
│ ├─Dates : 日付関連共通処理
│ ├─Mocks : テスト用モックオブジェクト
│ └─Properties : 共通部分のプロジェクト設定
├─Diagrams : プロジェクト設計図
├─Domain
│ ├─Common : ドメイン共有エンティティや値オブジェクト
│ ├─Customers : 顧客関連ビジネスルール
│ ├─Employees : 従業員関連ビジネスルール
│ ├─Products : 製品関連ビジネスルール
│ ├─Properties : ドメイン層のプロジェクト設定
│ └─Sales : 販売関連ビジネスルール
├─Infrastructure
│ ├─Inventory : 在庫関連インフラストラクチャ処理
│ ├─Network : ネットワーク関連インフラストラクチャ処理
│ └─Properties : インフラストラクチャ層のプロジェクト設定
├─Persistence
│ ├─Customers : 顧客情報の永続化処理
│ ├─Employees : 従業員情報の永続化処理
│ ├─Properties : 永続化層のプロジェクト設定
│ └─Sales : 販売情報の永続化処理
├─Presentation
│ ├─App_Start : アプリケーション起動設定
│ ├─Content : 静的ファイル(CSS等)
│ ├─Customers
│ │ └─Views : 顧客情報ビューとロジック
│ ├─DependencyResolution : 依存性解決設定
│ ├─Employees
│ │ └─Views : 従業員情報ビューとロジック
│ ├─Home
│ │ └─Views : ホームページビューとロジック
│ ├─Products
│ │ └─Views : 製品情報ビューとロジック
│ ├─Properties : プレゼンテーション層のプロジェクト設定
│ ├─Sales
│ │ ├─Models : 販売情報モデル
│ │ ├─Services : 販売情報サービス
│ │ └─Views : 販売情報ビューとロジック
│ └─Shared
│ └─Views : 複数ビュー共有部品
├─Service
│ ├─App_Start : サービス起動設定
│ ├─Customers : 顧客関連サービス
│ ├─Employees : 従業員関連サービス
│ ├─Products : 製品関連サービス
│ ├─Properties : サービス層のプロジェクト設定
│ └─Sales : 販売関連サービス
└─Specification
├─Common : テスト共通設定やユーティリティ
├─Customers
│ └─GetCustomersList : 顧客情報取得テスト
├─Employees
│ └─GetEmployeesList : 従業員情報取得テスト
├─Products : 製品関連テスト
├─Properties : テスト層のプロジェクト設定
└─Sales
├─CreateASale : 販売作成テスト
├─GetSaleDetails : 販売詳細取得テスト
└─GetSalesList : 販売情報取得テスト
ソース解説
データをDBに保存するフローを例に説明します。
以下の画像のようなフローになっています。
またSQRSパターンで実装されています。
SQRSパターンとは
DBの書き込みをCommand(Write) と Query(Read) のモデルを分離するパターン
CreateSalesCommand : DBへの書き込み処理 Command(Write)
namespace CleanArchitecture.Application.Sales.Commands.CreateSale
{
public interface ICreateSaleCommand
{
void Execute(CreateSaleModel model);
}
}
namespace CleanArchitecture.Application.Sales.Commands.CreateSale
{
public class CreateSaleCommand
: ICreateSaleCommand
{
private readonly IDateService _dateService;
private readonly IDatabaseService _database;
private readonly ISaleFactory _factory;
private readonly IInventoryService _inventory;
public CreateSaleCommand(
IDateService dateService,
IDatabaseService database,
ISaleFactory factory,
IInventoryService inventory)
{
_dateService = dateService;
_database = database;
_factory = factory;
_inventory = inventory;
}
public void Execute(CreateSaleModel model)
{
// 1. Service: 日付を取得
var date = _dateService.GetDate();
// 2. Entity: データをModelに格納ロジック
var customer = _database.Customers
.Single(p => p.Id == model.CustomerId);
var employee = _database.Employees
.Single(p => p.Id == model.EmployeeId);
var product = _database.Products
.Single(p => p.Id == model.ProductId);
var quantity = model.Quantity;
var sale = _factory.Create(
date,
customer,
employee,
product,
quantity);
_database.Sales.Add(sale);
// 3. Gateway: DBへ保存
_database.Save();
// 4. Gateway: Webへ保存
_inventory.NotifySaleOccurred(product.Id, quantity);
}
}
}
CreateSalesCommandで使用したクラスを解説
クラス | レイヤー | 説明 |
---|---|---|
DateService | Utility | 現在時刻を取得 |
DatabaseService | DB | データベースへデータを保存 |
SaleFactory | UseCase | Entityを生成し、データを格納 |
InventoryService | Web | Jsonを生成してWebApiへ渡している |
DateService
IDateServiceを継承していて、現在時刻を取得しています。
Utility的なレイヤーだと解釈しています。
namespace CleanArchitecture.Common.Dates
{
public interface IDateService
{
DateTime GetDate();
}
}
namespace CleanArchitecture.Common.Dates
{
public class DateService : IDateService
{
public DateTime GetDate()
{
return DateTime.Now.Date;
}
}
}
DatabaseService
IDatabaseはApplicationプロジェクトに存在していて、PresistanceプロジェクトのDatabaseServiceから依存されています。
using System.Data.Entity;
using CleanArchitecture.Domain.Customers;
using CleanArchitecture.Domain.Employees;
using CleanArchitecture.Domain.Products;
using CleanArchitecture.Domain.Sales;
namespace CleanArchitecture.Application.Interfaces
{
public interface IDatabaseService
{
// Entityをモデルに使用
IDbSet<Customer> Customers { get; set; }
IDbSet<Employee> Employees { get; set; }
IDbSet<Product> Products { get; set; }
IDbSet<Sale> Sales { get; set; }
void Save();
}
}
namespace CleanArchitecture.Persistence
{
public class DatabaseService : DbContext, IDatabaseService
{
// Entityをモデルに使用
public IDbSet<Customer> Customers { get; set; }
public IDbSet<Employee> Employees { get; set; }
public IDbSet<Product> Products { get; set; }
public IDbSet<Sale> Sales { get; set; }
public DatabaseService() : base("CleanArchitecture")
{
Database.SetInitializer(new DatabaseInitializer());
}
public void Save()
{
this.SaveChanges();
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.Add(new CustomerConfiguration());
modelBuilder.Configurations.Add(new EmployeeConfiguration());
modelBuilder.Configurations.Add(new ProductConfiguration());
modelBuilder.Configurations.Add(new SaleConfiguration());
}
}
}
InventoryService
InventoryServiceも同様にApplicationプロジェクトに存在していて、InfrastructureプロジェクトのInventoryServiceから依存されています。
namespace CleanArchitecture.Application.Interfaces
{
public interface IInventoryService
{
void NotifySaleOccurred(int productId, int quantity);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Infrastructure.Network;
namespace CleanArchitecture.Infrastructure.Inventory
{
public class InventoryService
: IInventoryService
{
// Note: these are hard coded to keep the demo simple
private const string AddressTemplate = "http://abc123.com/inventory/products/{0}/notifysaleoccured/";
private const string JsonTemplate = "{{\"quantity\": {0}}}";
private readonly IWebClientWrapper _client;
public InventoryService(IWebClientWrapper client)
{
_client = client;
}
public void NotifySaleOccurred(int productId, int quantity)
{
var address = string.Format(AddressTemplate, productId);
var json = string.Format(JsonTemplate, quantity);
_client.Post(address, json);
}
}
}
SaleFactory
Entityオブジェクトを生成しています。
namespace CleanArchitecture.Application.Sales.Commands.CreateSale.Factory
{
public interface ISaleFactory
{
Sale Create(DateTime date, Customer customer, Employee employee, Product product, int quantity);
}
}
namespace CleanArchitecture.Application.Sales.Commands.CreateSale.Factory
{
public class SaleFactory : ISaleFactory
{
public Sale Create(DateTime date, Customer customer, Employee employee, Product product, int quantity)
{
var sale = new Sale();
sale.Date = date;
sale.Customer = customer;
sale.Employee = employee;
sale.Product = product;
sale.UnitPrice = sale.Product.Price;
sale.Quantity = quantity;
// Note: Total price is calculated in domain logic
return sale;
}
}
}
SalesController : DBから読み込み Query(Read)
Discussion