以領域為核心重新設計的線上房貸申請系統 use DDD (2)
以領域為核心重新設計的線上房貸申請系統 use DDD (2)
前言
在前一篇文章:以領域為中心的:線上房貸申請系統設計 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
本篇文章會持續更新。
待續.......
留言
張貼留言