以領域為核心重新設計的線上房貸申請系統 use DDD (2)

以領域為核心重新設計的線上房貸申請系統 use DDD (2)

直播的 Log 圖

前言

在前一篇文章:以領域為中心的:線上房貸申請系統設計 use DDD 中,筆者簡單的介紹一下以房貸線上申請為例子的的領域模型如何繪製、以及用一點時間將領域模型撰寫成基本的程式碼骨架,現在,本篇文章將接續直播的內容持續的調整程式碼。

Visual Studio 2019 的 .NET 5 的整潔架構骨架

這裡也同步的調整一開始的設計,也就是 Repositories 的擺放位置,一開始我是放在 Domain Service 也就是領域層,有網友發現表示以依賴反向來說,應是由 Application Service/Use Case Layer 來存取 Repositories,雖然 Entity 可以透過 Factory 來產生實例,但是 Entity 必須透過 Aggregate 來無持資料的完整性,但調用 Entities/Value Objects 本來就是 Application Services/Layer 的責任,所以 ICustomerDetailRepository 放置在 Application Seervice 是比較合理的,因為本來就是 Application 來操作 Aggregate Root 並維持資料的完整性。

圖(一)、透過常見的依賴反轉來解決問題 依賴反向 

圖片取自:Clean Architecture 無暇的程式碼 - 整潔的軟體架構設計篇

因為 Entity 本來就在 Domain Layer,為了領域邏輯的獨立性,並不應該直接相依 Repository,外部調用 Domain 需要領域邏輯來操作得依靠 DIP 即可解決該問題。

另外,補充說明,就是其實以『依賴反向』來說,Clean Architecture 的 Aggregate Root 要封裝 Domain Layer 的多個的 Entities/Value Objects 的操作來講,將 ICustomerDetailRepository 放置在 Application Layer 這是正確的。

怎麼說呢?以 Clean Architecture 整潔架構一書中這張圖來說明,HL1 依賴 <I> 介面,早期我們大多用 OO 中的多型來達到這個效果,就是 HL1 要呼叫 +F() 函式,但是透過一些特殊技巧比如多型、或是現在大家耳熟能詳的 DI ,就能讓執行時期,這個 <I> 其實是不存在的,也就是說,執行時期,其實 HL1 呼叫到的是下面 +F() 方法,這就是所謂的依賴向,以及控制反轉 IoC 的核心概念。

調整領域模型 - 增加需求

圖(二)、調整領域模型 

增加檢核特殊身分領域邏輯

如上圖,因應需求的異動,假設,客戶需要再新增帳號的申請裡增加檢查(帳號/身分證號)是否為特殊身分時,這時我們就真的有一個 Domain 領域邏輯了,因為各位有注意到嗎?我們一開始建立的 Domain Service 其實只是空有一層,但是並未包含任何領域邏輯,Entity 內也未實作或包含與申請相關的領域邏輯,現在我們可以在 Domain 專案裡新增一個 CustomerService.cs 來撰寫抽離出來的領域邏輯。

在開始之前,我得先做幾件事情:

(一)、將介面 ICustomerDetailRepository 搬至 Application 

(二)、將 DetailData.cs 的 Value Object 搬至 Domain (這是第二個放置錯誤的物件,Value Object 應該置於 Domain 層) 

(三)、在 ICustomerDetailRepository 增加取得 Customer 這個 Entity 實例的方法

ICustomerDetailRepository 介面的程式碼:

// Application
public interface ICustomerDetailRepository
{
    IEnumerable<CustomerDomain.Customer> GetCustomers();
    int Save(CustomerDomain.Customer customer);
    /// <summary>
    /// 取得 Entity 的實例
    /// </summary>
    /// <returns></returns>
    CustomerDomain.Customer Get(string guid);
}

Infrastructure Service 的 CustomerDetailRepository 的實作:

// Infrastructure
public class CustomerDetailRepository : ICustomerDetailRepository
{
    private static List<global::Domain.Customer.Customer> _customers = new List<global::Domain.Customer.Customer>();
    public int Save(global::Domain.Customer.Customer customer)
    {
        _customers.Add(customer);
        return 1;
    }

    public global::Domain.Customer.Customer Get(string guid)
    {
        return _customers.Where(c => c.GetCustomerId().Value.ToString() == guid)
            .FirstOrDefault();
    }

    public IEnumerable<global::Domain.Customer.Customer> GetCustomers()
    {
        return _customers;
    }
}

(四)、增加 CustomerId 的 GUID 的 TypedIdValueBase 型態 與 TypedIdValueBase 類別

public class CustomerId : TypedIdValueBase
{
    public CustomerId(Guid value) : base(value)
    {
    }
}

// TypedIdValueBase 的 Guid 類型的 == operator 比較運算子類別
public abstract class TypedIdValueBase : IEquatable<TypedIdValueBase>
{
    public Guid Value { get; }

    protected TypedIdValueBase(Guid value)
    {
        Value = value;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        return obj is TypedIdValueBase other && Equals(other);
    }

    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }

    public bool Equals(TypedIdValueBase other)
    {
        return this.Value == other.Value;
    }

    public static bool operator ==(TypedIdValueBase obj1, TypedIdValueBase obj2)
    {
        if (object.Equals(obj1, null))
        {
            if (object.Equals(obj2, null))
            {
                return true;
            }
            return false;
        }
        return obj1.Equals(obj2);
    }
    public static bool operator !=(TypedIdValueBase x, TypedIdValueBase y)
    {
        return !(x == y);
    }
}

(五)、Customer 的 Entity 增加 Location 屬性 與 增加 檢查(帳號/身分證號)是否為特殊身分的領域邏輯方法 CheckSpecialUser()

因此,現在 Entity (Customer.cs) 的長相:

// Domain Layer
public class Customer: Entity, IAggegateRoot
{
    public Customer(string accountId, string chtName, string location)
    {
        _accountId = accountId;
        _chtName = chtName;
        _location = new DetailData(location);
        _customerid = new CustomerId(Guid.NewGuid());
    }
    
    private CustomerId _customerid;

    public CustomerId GetCustomerId()
    {
        return _customerid;
    }
    
    private string accountId;

    public string GetAccountId()
    {
        return accountId;
    }

    private string chtName;

    public string GetChtName()
    {
        return chtName;
    }

    private DetailData location;

    public DetailData GetLocation()
    {
        return location;
    }

    /// <summary>
    /// 檢查(帳號/身分證號)是否為特殊身分
    /// </summary>
    /// <param name="accountId"></param>
    /// <returns></returns>
    public bool CheckSpecialUser(string accountId)
    {
        return false;
    }

    public void ShowSpecialFlag()
    {
        // 針對如果是特殊身分進行相關的處裡
    }
}

目前只是先撰寫初骨架、先 return false,所以實際的邏輯其實還未撰寫實際的程式碼,不過這麼一來就容易的撰寫 Unit Test 來測試領域邏輯了。

(六)、增加 Domain Service 處裡特殊身分邏輯

這裡會需要呼叫行政機關的 Web API,所以目前並不在 Customer 實例化的時候處裡,直接抽離出一個領域邏輯,由 Application 需要時統籌並看時機呼叫使用。

public class CustomerService
{
    public CustomerService()
    {
    }

    /// <summary>
    /// 檢查是否為特出身分使用者
    /// 可能呼叫機關提供之服務以檢查是否為特殊身分國民
    /// </summary>
    /// <param name="customer">客戶物件實體</param>
    /// <param name="accountId">可能是身份證字號</param>
    /// <returns></returns>
    public void CheckIsSpecialAccount(Customer customer, string accountId)
    {
        bool isSpecial = customer.CheckSpecialUser(accountId);

        if (isSpecial)
        {
            customer.ShowSpecialFlag();
        }
    }
}

(七)、外部注入 Domain Service 並調整程式碼 現在,我們的 Application 的 CustomerBasicDetailRegisterHandler 可由外部注入 CustomerService,並將取得的 Entity 實例傳入進行特殊身分的判別與處裡了。

修改過後的 CustomerBasicDetailRegisterHandler.cs 程式碼如下:

// Application Layer
public class CustomerBasicDetailRegisterHandler
{
    public CustomerBasicDetailRegisterHandler(
        ICustomerDetailRepository customerDetailRepository, 
        CustomerService customerService)
    {
        _customerDetailRepository = customerDetailRepository;
        _customerService = customerService;
    }

    private ICustomerDetailRepository _customerDetailRepository;
    private CustomerService _customerService;

    public int AddCustomerDetailData(CustomerDetailDTO customerDeatil)
    {
        CustomerDetail.Customer customer 
            = new CustomerDetail.Customer(customerDeatil.UserId, customerDeatil.ChtName, customerDeatil.Location);

        // 檢查(帳號/身分證號)是否為特殊身分
        _customerService.CheckIsSpecialAccount(customer, customerDeatil.UserId);

        // Process and others..
        return _customerDetailRepository.Save(customer);
    }
}

最後,執行的結果當然與原先的相同,只是現在增加了領域邏輯。

詳細的範例原始檔案可參考 Github: 

https://github.com/wugelis/DDD-In-Depth-Net5Lab

若想了解 Guthub 上的 Visual Sutdio 2019 方案如何建立的可以參考下方影片。

直播網址 FB: 

https://www.facebook.com/will.fans/videos/1679528425584187

直播課程中的 Slide: 

https://www.slideshare.net/GelisWu/net-5/GelisWu/net-5

本篇文章會持續更新。

待續....... 

留言

這個網誌中的熱門文章

軟體工程師 - 成長的 10 個階段

常見的程式碼壞味道(Code Smell or Bad Smell)

什麼是 gRPC ?