1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2024-01-15

前の記事へ

はじめに

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

記事の構成

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

アプリケーション構成

販売情報を登録、表示するアプリケーションです。
販売情報には、顧客、従業員、商品が含まれまれています。
Salse
├─Customers
├─Employees
└Products

プロジェクト構成(フォルダ)は以下のようになっています。
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) のモデルを分離するパターン

Group 42.png

CreateSalesCommand : DBへの書き込み処理 Command(Write)

SalesController

namespace CleanArchitecture.Presentation.Sales
{
    [RoutePrefix("sales")]
    public class SalesController : Controller
    {
        private readonly IGetSalesListQuery _salesListQuery;
        private readonly IGetSaleDetailQuery _saleDetailQuery;
        private readonly ICreateSaleViewModelFactory _factory;
        private readonly ICreateSaleCommand _createCommand;

        public SalesController(
            IGetSalesListQuery salesListQuery,
            IGetSaleDetailQuery saleDetailQuery,
            ICreateSaleViewModelFactory factory,
            ICreateSaleCommand createCommand)
        {
            _salesListQuery = salesListQuery;
            _saleDetailQuery = saleDetailQuery;
            _factory = factory;
            _createCommand = createCommand;
        }

        /* Queryの記述は省略 */
        [Route("create")]
        public ViewResult Create()
        {
            var viewModel = _factory.Create();

            return View(viewModel);
        }

        [Route("create")]
        [HttpPost]
        public RedirectToRouteResult Create(CreateSaleViewModel viewModel)
        {
            var model = viewModel.Sale;            

            _createCommand.Execute(model);

            return RedirectToAction("index", "sales");
        }

}

CreateSalesCommand

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から依存されています。

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)

DBからデータを読み込みリストを取得する処理になります。

w:300

SalesController

namespace CleanArchitecture.Presentation.Sales
{
    [RoutePrefix("sales")]
    public class SalesController : Controller
    {
        private readonly IGetSalesListQuery _salesListQuery;
        private readonly IGetSaleDetailQuery _saleDetailQuery;
        private readonly ICreateSaleViewModelFactory _factory;
        private readonly ICreateSaleCommand _createCommand;

        public SalesController(
            IGetSalesListQuery salesListQuery,
            IGetSaleDetailQuery saleDetailQuery,
            ICreateSaleViewModelFactory factory,
            ICreateSaleCommand createCommand)
        {
            _salesListQuery = salesListQuery;
            _saleDetailQuery = saleDetailQuery;
            _factory = factory;
            _createCommand = createCommand;
        }

        [Route("")]
        public ViewResult Index()
        {
            var sales = _salesListQuery.Execute();

            return View(sales);
        }

        [Route("{id:int}")]
        public ViewResult Detail(int id)
        {
            var sale = _saleDetailQuery.Execute(id);

            return View(sale);
        }
        /* commandの記述は省略 */
}

GetSaleDetailQuery

namespace CleanArchitecture.Application.Sales.Queries.GetSaleDetail
{
    public interface IGetSaleDetailQuery
    {
        SaleDetailModel Execute(int id);
    }
namespace CleanArchitecture.Application.Sales.Queries.GetSaleDetail
{
    public class GetSaleDetailQuery
        : IGetSaleDetailQuery
    {
        private readonly IDatabaseService _database;

        public GetSaleDetailQuery(IDatabaseService database)
        {
            _database = database;
        }

        public SaleDetailModel Execute(int saleId)
        {
            var sale = _database.Sales
                .Where(p => p.Id == saleId)
                .Select(p => new SaleDetailModel()
                {
                    Id = p.Id, 
                    Date = p.Date,
                    CustomerName = p.Customer.Name,
                    EmployeeName = p.Employee.Name,
                    ProductName = p.Product.Name,
                    UnitPrice = p.UnitPrice,
                    Quantity = p.Quantity,
                    TotalPrice = p.TotalPrice
                })
                .Single();

            return sale;
        }
    }
}


GetSalesListQuery


namespace CleanArchitecture.Application.Sales.Queries.GetSalesList
{
    public interface IGetSalesListQuery
    {
        List<SalesListItemModel> Execute();
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using CleanArchitecture.Application.Interfaces;

namespace CleanArchitecture.Application.Sales.Queries.GetSalesList
{
    public class GetSalesListQuery 
        : IGetSalesListQuery
    {
        private readonly IDatabaseService _database;

        public GetSalesListQuery(IDatabaseService database)
        {
            _database = database;
        }

        public List<SalesListItemModel> Execute()
        {
            var sales = _database.Sales
                .Select(p => new SalesListItemModel()
                {
                    Id = p.Id, 
                    Date = p.Date,
                    CustomerName = p.Customer.Name,
                    EmployeeName = p.Employee.Name,
                    ProductName = p.Product.Name,
                    UnitPrice = p.UnitPrice,
                    Quantity = p.Quantity,
                    TotalPrice = p.TotalPrice
                });

            return sales.ToList();
        }
    }
}


DatabaseService

IDatabaseはApplicationプロジェクトに存在していて、PresistanceプロジェクトのDatabaseServiceから依存されています。

namespace CleanArchitecture.Persistence
{
    public class DatabaseService : DbContext, IDatabaseService
    {
        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());
        }
    }
}


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();
    }
}

前の記事へ

参考

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?