上一篇:《 "DDD 領域驅動設計-領域模型中的用戶設計?" 》 開源地址: "https://github.com/yuezhongxin/CNBlogs.Apply.Sample" (代碼已更新) 在之前的項目開發中,只有一個 JsPermissionApply 實體(JS 許可權申請),所以,C ...
上一篇:《DDD 領域驅動設計-領域模型中的用戶設計?》
開源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代碼已更新)
在之前的項目開發中,只有一個 JsPermissionApply 實體(JS 許可權申請),所以,CNBlogs.Apply.Domain 設計的有些不全面,或者稱之為不完善,因為在一些簡單的項目開發中,一般只會存在一個實體,單個實體的設計,我們可能會忽略很多的東西,從而以後會導致一些問題的產生,那如果再增加一個實體,CNBlogs.Apply.Domain 該如何設計呢?
按照實際項目開發需要,CNBlogs.Apply.Domain 需要增加一個 BlogChangeApply 實體(博客地址更改申請)。
在 BlogChangeApply 實體設計之前,我們按照之前 JsPermissionApply 實體設計過程,先大致畫一下流程圖:
流程圖很簡單,並且和之前的 JS 許可權申請和審核很相似,我們再看一下之前的 JsPermissionApply 實體設計代碼:
namespace CNBlogs.Apply.Domain
{
public class JsPermissionApply : IAggregateRoot
{
private IEventBus eventBus;
public JsPermissionApply()
{ }
public JsPermissionApply(string reason, User user, string ip)
{
if (string.IsNullOrEmpty(reason))
{
throw new ArgumentException("申請內容不能為空");
}
if (reason.Length > 3000)
{
throw new ArgumentException("申請內容超出最大長度");
}
if (user == null)
{
throw new ArgumentException("用戶為null");
}
if (user.Id == 0)
{
throw new ArgumentException("用戶Id為0");
}
this.Reason = HttpUtility.HtmlEncode(reason);
this.User = user;
this.Ip = ip;
this.Status = Status.Wait;
}
public int Id { get; private set; }
public string Reason { get; private set; }
public virtual User User { get; private set; }
public Status Status { get; private set; } = Status.Wait;
public string Ip { get; private set; }
public DateTime ApplyTime { get; private set; } = DateTime.Now;
public string ReplyContent { get; private set; }
public DateTime? ApprovedTime { get; private set; }
public bool IsActive { get; private set; } = true;
public async Task<Status> GetStatus(string userAlias)
{
if (await BlogService.HaveJsPermission(userAlias))
{
return Status.Pass;
}
else
{
if (this.Status == Status.Deny && DateTime.Now > this.ApplyTime.AddDays(3))
{
return Status.None;
}
if (this.Status == Status.Pass)
{
return Status.None;
}
return this.Status;
}
}
public async Task<bool> Pass()
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Pass;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = "恭喜您!您的JS許可權申請已通過審批。";
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new JsPermissionOpenedEvent() { UserAlias = this.User.Alias });
return true;
}
public bool Deny(string replyContent)
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Deny;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = replyContent;
return true;
}
public bool Lock()
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Lock;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = "抱歉!您的JS許可權申請沒有被批准,並且申請已被鎖定,具體請聯繫[email protected]。";
return true;
}
public async Task Passed()
{
if (this.Status != Status.Pass)
{
return;
}
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS許可權申請已批准", Content = this.ReplyContent, RecipientId = this.User.Id });
}
public async Task Denied()
{
if (this.Status != Status.Deny)
{
return;
}
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS許可權申請未通過審批", Content = this.ReplyContent, RecipientId = this.User.Id });
}
public async Task Locked()
{
if (this.Status != Status.Lock)
{
return;
}
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS許可權申請未通過審批", Content = this.ReplyContent, RecipientId = this.User.Id });
}
}
}
根據博客地址更改申請和審核的流程圖,然後再結合上面 JsPermissionApply 實體代碼,我們就可以幻想出 BlogChangeApply 的實體代碼,具體是怎樣的了,如果你實現一下,會發現和上面的代碼簡直一摸一樣,區別就在於多了一個 TargetBlogApp(目標博客地址),然後後面的 Repository 和 Application.Services 複製粘貼就行了,沒有任何的難度,這樣設計實現也沒什麼問題,但是項目中的重覆代碼簡直太多了,領域驅動設計慢慢就變成了一個腳手架,沒有任何的一點用處。
該如何解決上面的問題呢?我們需要思考下 CNBlogs.Apply.Domain 所包含的含義,CNBlogs.Apply.Domain 顧名思議是申請領域,並不是 CNBlogs.JsPermissionApply.Domain,也不是 CNBlogs.BlogChangeApply.Domain,實體的產生是根據聚合根的設計,那 CNBlogs.Apply.Domain 的聚合根是什麼呢?在之前的設計中只有 IAggregateRoot 和 IEntity,具體代碼:
namespace CNBlogs.Apply.Domain
{
public interface IAggregateRoot : IEntity { }
}
namespace CNBlogs.Apply.Domain
{
public interface IEntity
{
int Id { get; }
}
}
現在再來看上面這種設計,完全是錯誤的,聚合根介面怎麼能繼承實體介面呢,還有一個問題,就是如果有多個實體設計,是繼承 IAggregateRoot?還是 IEntity?IEntity 在這樣的設計中,沒有任何的作用,並且閑的很多餘,IAggregateRoot 到最後也只是一個抽象的介面,CNBlogs.Apply.Domain 中並沒有具體的實現。
解決上面混亂的問題,就是抽離出 ApplyAggregateRoot(申請聚合根),然後 JsPermissionApply 和 BlogChangeApply 實體都是由它進行產生,在這之前,我們先定義一下 IAggregateRoot:
namespace CNBlogs.Apply.Domain
{
public interface IAggregateRoot
{
int Id { get; }
}
}
然後根據 JS 許可權申請/審核和博客地址更改申請/審核的流程圖,抽離出 ApplyAggregateRoot,並且繼承自 IAggregateRoot,具體實現代碼:
namespace CNBlogs.Apply.Domain
{
public class ApplyAggregateRoot : IAggregateRoot
{
private IEventBus eventBus;
public ApplyAggregateRoot()
{ }
public ApplyAggregateRoot(string reason, User user, string ip)
{
if (string.IsNullOrEmpty(reason))
{
throw new ArgumentException("申請內容不能為空");
}
if (reason.Length > 3000)
{
throw new ArgumentException("申請內容超出最大長度");
}
if (user == null)
{
throw new ArgumentException("用戶為null");
}
if (user.Id == 0)
{
throw new ArgumentException("用戶Id為0");
}
this.Reason = HttpUtility.HtmlEncode(reason);
this.User = user;
this.Ip = ip;
this.Status = Status.Wait;
}
public int Id { get; protected set; }
public string Reason { get; protected set; }
public virtual User User { get; protected set; }
public Status Status { get; protected set; } = Status.Wait;
public string Ip { get; protected set; }
public DateTime ApplyTime { get; protected set; } = DateTime.Now;
public string ReplyContent { get; protected set; }
public DateTime? ApprovedTime { get; protected set; }
public bool IsActive { get; protected set; } = true;
protected async Task<bool> Pass<TEvent>(string replyContent, TEvent @event)
where TEvent : IEvent
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Pass;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = replyContent;
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(@event);
return true;
}
public bool Deny(string replyContent)
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Deny;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = replyContent;
return true;
}
protected bool Lock(string replyContent)
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Lock;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = replyContent;
return true;
}
protected async Task Passed(string title)
{
if (this.Status != Status.Pass)
{
return;
}
await SendMessage(title);
}
protected async Task Denied(string title)
{
if (this.Status != Status.Deny)
{
return;
}
await SendMessage(title);
}
protected async Task Locked(string title)
{
if (this.Status != Status.Lock)
{
return;
}
await SendMessage(title);
}
private async Task SendMessage(string title)
{
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = title, Content = this.ReplyContent, RecipientId = this.User.Id });
}
}
}
ApplyAggregateRoot 的實現,基本上是抽離出 JsPermissionApply 和 BlogChangeApply 實體產生的重覆代碼,比如不管什麼類型的申請,都包含申請理由、申請人信息、通過或拒絕等操作,這些也就是 ApplyAggregateRoot 所體現的領域含義,我們再來看下 BlogChangeApply 實體的實現代碼:
namespace CNBlogs.Apply.Domain
{
public class BlogChangeApply : ApplyAggregateRoot
{
public BlogChangeApply()
{ }
public BlogChangeApply(string targetBlogApp, string reason, User user, string ip)
: base(reason, user, ip)
{
if (string.IsNullOrEmpty(targetBlogApp))
{
throw new ArgumentException("博客地址不能為空");
}
targetBlogApp = targetBlogApp.Trim();
if (targetBlogApp.Length < 4)
{
throw new ArgumentException("博客地址至少4個字元!");
}
if (!Regex.IsMatch(targetBlogApp, @"^([0-9a-zA-Z_-])+$"))
{
throw new ArgumentException("博客地址只能使用英文、數字、-連字元、_下劃線!");
}
this.TargetBlogApp = targetBlogApp;
}
public string TargetBlogApp { get; private set; }
public Status GetStatus()
{
if (this.Status == Status.Deny && DateTime.Now > this.ApplyTime.AddDays(3))
{
return Status.None;
}
return this.Status;
}
public async Task<bool> Pass()
{
var replyContent = $"恭喜您!您的博客地址更改申請已通過,新的博客地址:<a href='{this.TargetBlogApp}' target='_blank'>{this.TargetBlogApp}</a>";
return await base.Pass(replyContent, new BlogChangedEvent() { UserAlias = this.User.Alias, TargetUserAlias = this.TargetBlogApp });
}
public bool Lock()
{
var replyContent = "抱歉!您的博客地址更改申請沒有被批准,並且申請已被鎖定,具體請聯繫[email protected]。";
return base.Lock(replyContent);
}
public async Task Passed()
{
await base.Passed("您的博客地址更改申請已批准");
}
public async Task Denied()
{
await base.Passed("您的博客地址更改申請未通過審批");
}
public async Task Locked()
{
await Denied();
}
}
}
BlogChangeApply 繼承自 ApplyAggregateRoot,並且單獨的 TargetBlogApp 操作,其他一些實現都是基本的參數傳遞操作,沒有具體實現,JsPermissionApply 的實體代碼就不貼了,和 BlogChangeApply 比較類似,只不過有一些不同的業務實現。
CNBlogs.Apply.Domain 改造之後,還要對應改造下 Repository,之前的代碼大家可以看下 Github,這邊我簡單說下改造的過程,首先 IRepository 的設計不變:
namespace CNBlogs.Apply.Repository.Interfaces
{
public interface IRepository<TAggregateRoot>
where TAggregateRoot : class, IAggregateRoot
{
IQueryable<TAggregateRoot> Get(int id);
IQueryable<TAggregateRoot> GetAll();
}
}
IRepository 對應 BaseRepository 實現,它的作用就是抽離出所有聚合根的 Repository 操作,並不單獨包含 ApplyAggregateRoot,所以,我們還需要一個對 ApplyAggregateRoot 操作的 Repository 實現,定義如下:
namespace CNBlogs.Apply.Repository.Interfaces
{
public interface IApplyRepository<TApplyAggregateRoot> : IRepository<TApplyAggregateRoot>
where TApplyAggregateRoot : ApplyAggregateRoot
{
IQueryable<TApplyAggregateRoot> GetByUserId(int userId);
IQueryable<TApplyAggregateRoot> GetWaiting(int userId);
IQueryable<TApplyAggregateRoot> GetWaiting();
}
}
大家如果熟悉之前代碼的話,會發現 IApplyRepository 的定義和 IJsPermissionApplyRepository 的定義是一摸一樣的,設計 IApplyRepository 的好處就是,對於申請實體的相同操作,我們就不需要再寫重覆代碼了,比如 IJsPermissionApplyRepository 和 IBlogChangeApplyRepository 的定義:
namespace CNBlogs.Apply.Repository.Interfaces
{
public interface IJsPermissionApplyRepository : IApplyRepository<JsPermissionApply>
{ }
}
namespace CNBlogs.Apply.Repository.Interfaces
{
public interface IBlogChangeApplyRepository : IApplyRepository<BlogChangeApply>
{
IQueryable<BlogChangeApply> GetByTargetAliasWithWait(string targetBlogApp);
}
}
當然,除了上面的代碼改造,還有一些其他功能的添加,比如 ApplyAuthenticationService 領域服務增加了 VerfiyForBlogChange 等等,具體的一些改變,大家可以查看提交。
CNBlogs.Apply.Sample 開發進行到這,對於現階段的我來說,應用領域驅動設計我是比較滿意的,雖然還有一些不完善的地方,但至少除了 CNBlogs.Apply.Domain,在其他項目中是看不到業務實現代碼的,如果業務需求發生變化,首先更改的是 CNBlogs.Apply.Domain,而不是不是其它項目,這是一個基本點。
先設計 CNBlogs.Apply.Domain 和 CNBlogs.Apply.Domain.Tests,就能完成整個的業務系統設計,其它都是一些技術實現或工作流程實現,這個路子我覺得是正確的,以後邊做邊完善並學習。