介紹
我們將通過例⼦介紹和解釋⼀些顯式規則。在實現領域驅動設計時,應該遵循這些規則並將其應⽤到解決⽅案中。
領域劃分
首先我們先對比下Blog.Core和本次重構設計上的偏差,可以看到多了一個部落格管理和類別管理。
根據上面得到的業務腦圖我們可以看到包含Blog(部落格),Post(文章),Comment(評論),Tag(標籤),User(使用者),根據腦圖畫出領域圖來指明關係。
領域圖連線地址:https://www.processon.com/view/link/611365c00e3e7407d39727ee
聚合根最佳實踐
只通過ID引⽤其他聚合
⼀個聚合應該只通過其他聚合的ID引⽤聚合,這意味著你不能新增導航屬性到其他聚合。
-
這條規則使得實現可序列化原則得以實現。
-
可以防⽌不同聚合相互操作,以及將聚合的業務邏輯洩露給另⼀個聚合。
來看下面的2個聚合根 Blog 和 Post.
- Blog 沒有包含 Post集合,因為他們是不同聚合
- Post 使用 BlogId 關聯 Blog
當你有一個 Post 需要關聯 Blog的時候 你可以從資料庫通過 BlogId 進行獲取
public class Blog:FullAuditedAggregateRoot<Guid>
{
[NotNull]
public virtual string Name { get; set; }
[NotNull]
public virtual string ShortName { get; set; }
[CanBeNull]
public virtual string Description { get; set; }
}
public class Post : FullAuditedAggregateRoot<Guid>
{
public virtual Guid BlogId { get; protected set; }
[NotNull]
public virtual string Url { get; protected set; }
[NotNull]
public virtual string CoverImage { get; set; }
[NotNull]
public virtual string Title { get; protected set; }
[CanBeNull]
public virtual string Content { get; set; }
[CanBeNull]
public virtual string Description { get; set; }
public virtual int ReadCount { get; protected set; }
public virtual Collection<PostTag> Tags { get; protected set; }
}
聚合根/實體中的主鍵
⼀個聚合根通常有⼀個ID屬性作為其識別符號(主鍵,Primark Key: PK)。推薦使⽤ Guid 作為聚合,聚合中的實體(不是聚合根)可以使⽤複合主鍵(後面講),主鍵ABP已經幫我們做好了參閱文件:https://docs.abp.io/en/abp/latest/Entities。
public class Blog:FullAuditedAggregateRoot<Guid>
{
[NotNull]
public virtual string Name { get; set; }
[NotNull]
public virtual string ShortName { get; set; }
[CanBeNull]
public virtual string Description { get; set; }
}
聚合根/實體建構函式
建構函式是實體的⽣命週期開始的地⽅。⼀個設計良好的建構函式,擔負以下職責:
- 獲取所需的實體屬性引數,來建立⼀個有效的實體。應該強制只傳遞必要的引數,並可以將⾮必要 的屬性作為可選引數。
- 檢查引數的有效性。
- 初始化⼦集合。
public Blog(Guid id, [NotNull] string name, [NotNull] string shortName)
{
//屬性賦值
Id = id;
//有效性檢測
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
//有效性檢測
ShortName = Check.NotNullOrWhiteSpace(shortName, nameof(shortName));
}
- Blog 類通過建構函式引數、獲得屬性所需的值,以此建立一個正確有效的實體
- 在建構函式中驗證輸⼊引數的有效性,⽐如: Check.NotNullOrWhiteSpace(...) 當傳遞的值為空 時,丟擲異常 ArgumentException
- 建構函式將引數 id 傳遞給 base 類,不在建構函式中⽣成 Guid,可以將其委託給另⼀個 Guid⽣成 服務,作為引數傳遞進來
- ⽆參建構函式對於ORM是必要的。我們將其設定為私有,以防⽌在程式碼中意外地使⽤它
實體屬性訪問器和⽅法
上⾯的示例程式碼,看起來可能很奇怪。⽐如:在建構函式中,我們強制傳遞⼀個不為 null 的 Name 。 但是,我們可以將 Name 屬性設定為 null ,⽽對其沒有進⾏任何有效性控制。
如果我們⽤ public 設定器宣告所有的屬性,就像上⾯的 Blog 類中的屬性例⼦,我們就不能在實體的⽣命週期中強制保持其有效性和完整性。所以:
- 當需要在設定屬性時,執⾏任何邏輯,請將屬性設定為私有 private 。
- 定義公共⽅法來操作這些屬性。
public class Blog:FullAuditedAggregateRoot<Guid>
{
[NotNull]
public virtual string Name { get; protected set; }
[NotNull]
public virtual string ShortName { get; protected set; }
[CanBeNull]
public virtual string Description { get; set; }
protected Blog()
{
/*反序列化或ORM 需要*/
}
public Blog(Guid id, [NotNull] string name, [NotNull] string shortName)
{
//屬性賦值
Id = id;
//有效性檢測
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
//有效性檢測
ShortName = Check.NotNullOrWhiteSpace(shortName, nameof(shortName));
}
public virtual Blog SetName([NotNull] string name)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
return this;
}
public virtual Blog SetShortName(string shortName)
{
ShortName = Check.NotNullOrWhiteSpace(shortName, nameof(shortName));
return this;
}
}
業務邏輯和實體中的異常處理
當你在實體中進⾏驗證和實現業務邏輯,經常需要管理異常:
- 建立特定領域異常。
- 必要時在實體⽅法中丟擲這些異常
ABP框架 Exception Handing 系統處理了這些問題。
完成聚合的實體建立
根據 最佳實踐的講解完成,把其他實體建立出來,程式碼粘在這裡了。
public class Blog:FullAuditedAggregateRoot<Guid>
{
[NotNull]
public virtual string Name { get; protected set; }
[NotNull]
public virtual string ShortName { get; protected set; }
[CanBeNull]
public virtual string Description { get; set; }
protected Blog()
{
}
public Blog(Guid id, [NotNull] string name, [NotNull] string shortName)
{
//屬性賦值
Id = id;
//有效性檢測
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
//有效性檢測
ShortName = Check.NotNullOrWhiteSpace(shortName, nameof(shortName));
}
public virtual Blog SetName([NotNull] string name)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
return this;
}
public virtual Blog SetShortName(string shortName)
{
ShortName = Check.NotNullOrWhiteSpace(shortName, nameof(shortName));
return this;
}
}
public class Comment : FullAuditedAggregateRoot<Guid>
{
public virtual Guid PostId { get; protected set; }
public virtual Guid? RepliedCommentId { get; protected set; }
public virtual string Text { get; protected set; }
protected Comment()
{
}
public Comment(Guid id, Guid postId, Guid? repliedCommentId, [NotNull] string text)
{
Id = id;
PostId = postId;
RepliedCommentId = repliedCommentId;
Text = Check.NotNullOrWhiteSpace(text, nameof(text));
}
public void SetText(string text)
{
Text = Check.NotNullOrWhiteSpace(text, nameof(text));
}
}
public class Post : FullAuditedAggregateRoot<Guid>
{
public virtual Guid BlogId { get; protected set; }
[NotNull]
public virtual string Url { get; protected set; }
[NotNull]
public virtual string CoverImage { get; set; }
[NotNull]
public virtual string Title { get; protected set; }
[CanBeNull]
public virtual string Content { get; set; }
[CanBeNull]
public virtual string Description { get; set; }
public virtual int ReadCount { get; protected set; }
public virtual Collection<PostTag> Tags { get; protected set; }
protected Post()
{
}
public Post(Guid id, Guid blogId, [NotNull] string title, [NotNull] string coverImage, [NotNull] string url)
{
Id = id;
BlogId = blogId;
Title = Check.NotNullOrWhiteSpace(title, nameof(title));
Url = Check.NotNullOrWhiteSpace(url, nameof(url));
CoverImage = Check.NotNullOrWhiteSpace(coverImage, nameof(coverImage));
Tags = new Collection<PostTag>();
Comments = new Collection<Comment>();
}
public virtual Post IncreaseReadCount()
{
ReadCount++;
return this;
}
public virtual Post SetTitle([NotNull] string title)
{
Title = Check.NotNullOrWhiteSpace(title, nameof(title));
return this;
}
public virtual Post SetUrl([NotNull] string url)
{
Url = Check.NotNullOrWhiteSpace(url, nameof(url));
return this;
}
public virtual void AddTag(Guid tagId)
{
Tags.Add(new PostTag(Id, tagId));
}
public virtual void RemoveTag(Guid tagId)
{
Tags.RemoveAll(t => t.TagId == tagId);
}
}
public record PostTag
{
public virtual Guid TagId { get; init; } //主鍵
protected PostTag()
{
}
public PostTag( Guid tagId)
{
TagId = tagId;
}
}
public class Tag : FullAuditedAggregateRoot<Guid>
{
public virtual Guid BlogId { get; protected set; }
public virtual string Name { get; protected set; }
public virtual string Description { get; protected set; }
public virtual int UsageCount { get; protected internal set; }
protected Tag()
{
}
public Tag(Guid id, Guid blogId, [NotNull] string name, int usageCount = 0, string description = null)
{
Id = id;
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
BlogId = blogId;
Description = description;
UsageCount = usageCount;
}
public virtual void SetName(string name)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
}
public virtual void IncreaseUsageCount(int number = 1)
{
UsageCount += number;
}
public virtual void DecreaseUsageCount(int number = 1)
{
if (UsageCount <= 0)
{
return;
}
if (UsageCount - number <= 0)
{
UsageCount = 0;
return;
}
UsageCount -= number;
}
public virtual void SetDescription(string description)
{
Description = description;
}
}
結語
本節知識點:
- 1.根據腦圖劃分聚合
- 2.根據領域圖在遵守DDD的聚合根規範的情況下建立聚合
聯絡作者:加群:867095512 @MrChuJiu