十年河東,十年河西,莫欺少年窮
學無止境,精益求精
1、序言
領域驅動設計是一種解決業務複雜性的設計思想,不是一種標準規則的解決方法。
2、ddd 領域驅動模型介紹
參考:https://www.zhihu.com/question/481820861 和 https://zhuanlan.zhihu.com/p/91525839
3、ddd 領域模型VS事務腳本
事務腳本其實就是程序員依照業務邏輯進行自然的代碼構造
比如下訂單
public void 訂單() { 保存訂單(); 發送郵件(); 增減積分(); } public void 保存訂單() { } public void 發送郵件() { } public void 增減積分() { }
這種寫法,把大量的業務邏輯寫在方法內,一旦更改需求,就必須修改代碼,待業務足夠複雜時,代碼量都聚集在一個方法內,難以維護擴展。違背了設計模式的開閉原則
何爲領域模型呢?如果通過領域模型解決上述問題?可參考DDD的四種 Domain 模式,
- 失血模型
- 貧血模型
- 充血模型
- 脹血模型
詳見:https://zhuanlan.zhihu.com/p/91525839
4、ddd 實體與值對象
值對象:沒有標識符的對象,也有多個屬性,依附於某個實體存在。
以訂單爲例,一般情況下,我們設計訂單狀態時,一般將訂單狀態字段設計爲 Int 類型,例如:0:待支付 1:已支付 2:已取消
以code first爲例,新建一個數據庫實體,如下:
internal class OrderDto { public long uid { get; set; } public string? orderNo { get; set; } public int orderStatus { get; set; } }
上述實體中的orderStatus 不僅僅可以取值爲 0 、1 、2、還可以取值爲:100 、 200 、 888 等,這樣設計並不符合DDD的設計原則,那麼怎麼設計實體才符合DDD的設計原則呢?
internal class OrderDto { public long uid { get; set; } public string? orderNo { get; set; } public OrderStatusEnum orderStatus { get; set; } } public enum OrderStatusEnum { 待支付,已支付,已取消 }
上述定義的枚舉類型即爲實體的值對象
再或者,以商家爲例
用戶要想快速的找到商家,商家就必須擁有經緯度屬性,方便用戶導航
一般情況下,我們都是這樣定義商家
public class Shop { public long uid { get; set; } public string? shopName { get; set; } /// <summary> /// 緯度 /// </summary> public double lat { get; set; } /// <summary> /// 經度 /// </summary> public double lgt { get; set; } //.........其他字段 }
按照ddd的思想,我們可以將經緯度單獨抽出來,如下
public class Shop { public long uid { get; set; } public string? shopName { get; set; } public latlgt latlgt { get; set; } //.........其他字段 } public class latlgt { public bool CheckLatlgt() { if (lat < -90 || lat > 90) { return false; } if (lgt < -180 || lat > 180) { return false; } return true; } /// <summary> /// 緯度 /// </summary> public double lat { get; set; } /// <summary> /// 經度 /// </summary> public double lgt { get; set; } //.........其他字段 }
單獨抽出來的好處是重用、並且符合設計模式的單一職責模式,
5、聚合與聚合根
一個上下文內可能包含多個聚合,每個聚合都有一個根實體,叫做聚合根,一個聚合只有一個聚合根。
這裏面最重要的原則是:只有聚合根才能被外部訪問到,聚合根維護聚合的內部一致性。
以訂單和訂單詳情爲例
在code first 中,我們定義訂單和訂單詳情通常這樣定義
internal class OrderDto { public long uid { get; set; } public string? orderNo { get; set; } public OrderStatusEnum orderStatus { get; set; } public List<OrderDtlDto> OrderDtls { get; set; } } public enum OrderStatusEnum { 待支付,已支付,已取消 } public class OrderDtlDto { public long uid { get; set; } public long orderId { get; set; } //..其他字段 }
上述的訂單就是聚合根,訂單詳情屬於聚合根的從屬實體。
關於聚合和聚合根,可參考:https://zhuanlan.zhihu.com/p/146488464
6、領域服務、應用服務
以EfCore CodeFirst進行說明
領域服務是指:在同一個DbContext下,相同聚合根或不同聚合根之前的調用稱之爲領域服務,領域服務工作在同一個進程中,執行結果具有強一致性
應用服務是指:不同微服務之間的相同調用,他們之間的調用是基於網絡接口的形式,應用服務不在同一個進程內工作,執行結果不具有強一致性,屬於分佈式的範疇
上述表述是根據B站楊老師的視頻總結出來的,不完全準確,不需勿噴。
7、Net6 實現領域事件
在net6項目中引入Nuget包
MediatR.Extensions.Microsoft.DependencyInjec
註冊MediatR
builder.Services.AddMediatR(Assembly.GetAssembly(typeof(NotificationModel)));//當前程序集:Assembly.GetExecutingAssembly()
注意:註冊方法 AddMediatR 中的參數是命名空間,共MediatR掃描繼承INotification接口的類
發送方實體
在項目中新建發送方相關類,發送方相關類繼承自INotification接口
/// <summary> /// 發送方內容 註冊MediatR時,掃描該類所屬命名空間 /// </summary> public class NotificationModel : INotification { public string body { get; set; } }
發送方發送事件
在webApi中新建Action,進行事件發送
using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using swapModels.MediatrModels; namespace swap.Controllers { [AllowAnonymous] public class MediartController : BaseController { private readonly IMediator mediator; public MediartController(IMediator mediator) { this.mediator = mediator; } [HttpGet] public async Task<IActionResult> Test(CancellationToken cancellation=default) { await mediator.Publish<NotificationModel>(new NotificationModel() { body="hello"+DateTime.Now},cancellation); return Ok(); } } }
mediator 提供了兩個方法,一個是Publish,一個是Send,Publish 以廣播的形式進行事件發送,可以有多個接收方。send 只能有一個接收方,屬於點對點模式。
接收方代碼
/// <summary> /// 接收方1 /// </summary> public class NotificationHandler : INotificationHandler<NotificationModel> { public async Task Handle(NotificationModel notification, CancellationToken cancellationToken) { await Task.Run(() => { Console.WriteLine("接收方1"+notification.body); }); } } /// <summary> /// 接收方2 /// </summary> public class NotificationHandler2 : INotificationHandler<NotificationModel> { public async Task Handle(NotificationModel notification, CancellationToken cancellationToken) { await Task.Run(() => { Console.WriteLine("接收方2" + notification.body); }); } }
當運行項目,點擊swagger上Test方法時,將會有兩個接收方接收到發送事件發送的信息
8、DDD集成事件的發送
集成事件屬於跨微服務之間的事件,工作在不同的線程內【微服務工作在不同服務器上】,因此使用上述的MediatR 就不能滿足需求了,我們需要藉助第三方的MQ中間件。
比如,Redis/KafKa/RabbitMQ等