🌊

Clean Architectureソース解説編(後半) →記述中

2024/01/18に公開

はじめに

クリーンアーキテクチャをソースコードを読み解き説明します。
以下のソースを参考にさせていただきます。

https://github.com/matthewrenze/clean-architecture-demo

記事の構成

  • Clean Architecture超概要編(前半)
    章構成: 要約 = 手段 + 効果
  • Clean Architecture概要編(中半)
    章構成: 手段の説明
  • Clean Architectureソース解説編(後半)
    今ここ👆

アプリケーション構成

プロジェクト構成(フォルダ)は以下のようになっています。
image.png

ディレクトリ名 役割
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) のモデルを分離するパターン

https://zenn.dev/shmi593/articles/c1baeb2d453929

Group 42.png

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)

Queries.png

https://buildersbox.corp-sansan.com/entry/2019/07/10/110000

Discussion