1. 程式人生 > 其它 >基於ABP落地領域驅動設計-05.實體建立和更新最佳實踐

基於ABP落地領域驅動設計-05.實體建立和更新最佳實踐

目錄

系列文章

圍繞DDDABP Framework兩個核心技術,後面還會陸續釋出核心構件實現

綜合案例實現系列文章,敬請關注!
ABP Framework 研習社(QQ群:726299208)
ABP Framework 學習及實施DDD經驗分享;示例原始碼、電子書共享,歡迎加入!

資料傳輸物件

DTO 是簡單物件,用於在應用層和展示層傳遞狀態資料。所以,應用服務方法返回 DTO。

DTO原則和最佳實踐:

  • DTO應該可序列化,因為大多數時候,需要網路傳輸。
  • 應該有一個無參建構函式
  • 不能包含任何業務邏輯
  • 不能繼承或引用實體

輸入DTO輸出DTO在本質上不同:一個用於給應用服務方法傳遞引數,一個作為應用服務方法的返回值,根據業務需要區別對待。

輸入DTO最佳實踐

不要在輸入DTO中定義不使用的屬性

只定義需要用的屬性,否則,無用的屬性只會讓客戶端在使用應用服務方法時感到困惑。當然可以定義可選屬性,但是確保當客戶端在使用時,不應該影響到用例的工作方式。

這條規則看起來沒什麼必要,誰會為方法倉儲(輸入DTO)新增不使用的屬性呢?但是,它經常發生,尤其是當你想重用輸入DTO物件時,會將多個DTO屬性放在一個DTO物件中。

不要重用輸入DTO

為每個用例(應用服務方法)定義特定的輸入DTO,否則,在某些情況下不會新增一些不被使用的屬性,這就違反了上面定義的規則。

有時候,在兩個不同的用例中使用相同的DTO似乎很有吸引力,因為他們如此相似。甚至,當前是一模一樣,可能後面隨著業務變化才會有可能不同,此時也應該不要重用輸入DTO。因為和用例間的耦合相比,程式碼複製可能是更好的做法。

重用DTO的另一種方式是:DTO繼承,這同樣會產生上面描述的問題.。

示例:使用者應用服務

public interface IUserAppService:IApplicationService
{
  Task CreateAsync(UserDto input);
  Task UpdateAsync(UserDto input);
  Task ChangePasswordAsync(UserDto input);
}

IUserAppService 在所有方法(用例)使用 UserDto 作為輸入DTO,UserDto定義如下:

public class UserDto
{
  public Guid Id{get;set;}
  public string UserName{get;set;}
  public string Email{get;set;}
  public string Password{get;set;}
  public DateTime CreationTime{get;set;}
}
  • Id 在 Create 方法中不被使用,因為 Id 由伺服器生成。
  • Password 在 Update 方法中不使用,因為有修改密碼的單獨方法。
  • CreationTime 未被使用,且不應該由客戶端傳送給服務端,應該在服務端設定建立時間。

正確的實現,如下:

public interface IUserAppService:IApplicationService
{
  Task CreateAsync(UserCreationDto input);
  Task UpdateAsync(UserUpdateDto input);
  Task ChangePasswordAsync(UserChangePasswordDto input);
}

然後定義對應的DTO類:

public class UserCreationDto
{
  public string UserName {get;set;}
  public string Email{get;set;}
  public string Password{get;set;}
}

public class UserUpdateDto
{
  public Guid Id{get;set;}
  public string UserName{get;set;}
  public string Email{get;set;}
}

public class UserChangePasswordDto
{
  public Guid Id{get;set;}
  public string Password{get;set;}
}

儘管需要編寫更多的程式碼,但是這是一種更易維護的方法。

特殊情況:舉個例子,如果你有一個報表頁,頁面中有多個過濾條件,對應多個應用服務方法(顯示報表、匯出Excel、匯出CSV),此時應該使用相同的輸入DTO引數,返回不同的結果。因為當頁面過濾條件改變時,修改一個DTO而對整個頁面對應的應用服務方法引數生效。

輸入DTO中驗證邏輯

  • 僅在DTO內部執行簡單驗證,使用資料註解特性或實現 IValidatableObject 介面
  • 不要執行領域驗證,舉個例子,不要在DTO中檢測使用者名稱是否唯一的驗證。

示例:使用資料註解特性

using System.ComponentModel.DataAnnotations;

namespace IssueTracking.Users
{
  public class UserCreationDto
  {
    [Required]
    [StringLength(UserConsts.MaxUserNameLength)]
    public string UserName {get;set;}

    [Required]
    [EmailAddress]
    [StringLength(UserConsts.MaxEmailLength)]
    public string Email{get;set;}
    [Required]
    [StringLength(UserConsts.MaxEmailLength,MinimumLength=UserConsts.MinPasswordLength)]
    public string Password{get;set;}
  }
}

ABP框架自動驗證輸入DTO,驗證失敗則丟擲AbpValidationException異常,返回 400 HTTP 狀態碼。

某些開發者認為將驗證規則和DTO類分離可能會更好。我們認為宣告式(資料註解)是實用的,不會導致任何設計問題。當然,ABP支援 FluentValidation整合。

輸出DTO最佳實踐

  • 保持輸出DTO數量最小,儘可能重用,但是不能將輸入DTO作為輸出DTO使用。
  • 輸出DTO可以包含比用例需要的更多屬性
  • CreateUpdate 方法中返回DTO

以上建議的主要原因是:

  • 使客戶端程式碼易於開發和擴充套件
    • 在客戶端端處理不同但相似的DTO容易混淆
    • 輸入DTO中的更多屬性可能未來會在UI/客戶端中被使用,返回實體的所有屬性(已經考慮過安全性和特殊情況)使客戶端程式碼易於改進,而不需要修改後端程式碼。
    • 如果是通過API暴露給第三方客戶端,避免不同需求返回不同DTO
  • 使服務端程式碼易於開發和擴充套件
    • 更少的類,易於理解和維護
    • 可以重用實體到DTO(AutoMapper)的物件對映程式碼
    • 不同方法返回相同型別,使新增新方法變得簡單明瞭。

示例:從不同方法返回不同DTO

public interface IUserAppService:IApplicationService
{
  UserDto Get(Guid id);
  List<UserNameAndEmailDto> GetUserNameAndEmail(Guid id);
  List<string> GetRoles(Guid id);
  List<UserListDto> GetList();
  UserCreateResultDto Create(UserCreationDto input);
  UserUpdateResultDto Update(UserUpdateDto input);
}

示例中沒有使用非同步方法,在實際開發時應該是非同步方法。

上面的示例程式碼中,為每個方法返回不同DTO型別,這樣會導致我們需要處理非常多的資料查詢,對映實體到DTO的重複程式碼。

按照以下方式定義就簡單多了:

public interface IUserAppService:IApplicationService
{
  UserDto Get(Guid id);
  List<UserDto> GetList();
  UserDto Create(UserCreationDto input);
  UserDto Update(UserUpdateDto input);
}

使用一個輸出DTO:

public class UserDto
{
  public Guid Id{get;set;}
  public string UserName{get;set;}
  public string Email{get;set;}
  public DateTiem CreationTime{get;set;}
  public List<string> Roles{get;set;}
}
  • 移除 GetUserNameAndEmailGetRoles 方法,因為 Get 方法已經返回足夠需要的資訊。
  • GetList 返回物件與 Get 相同
  • CreateUpdate 同樣返回 UserDto

由此可見,返回相同DTO更加簡潔。

為什麼建立或更新之後要返回DTO? 想象一個用例場景,在頁面中顯示表格資料,當更新之後,獲取返回物件,並對錶格資料來源進行更新,這樣就不需要再次呼叫 GetList 方法,這是我們建議在 CreateUpdate 方法中返回 DTO 的原因。

討論

以上關於輸出DTO的建議,並不適用所有場景。

出於效能考慮,這些建議可以被忽略,特別是當存在大型資料集返回結果時,或者使用者介面需要發起很多併發請求時,此時應該建立特定的輸出DTO,只包含儘可能少的資訊。

可維護性和效能,需要開發者權衡,上面的建議適用於效能損失可忽略不計的應用。

物件對映

自動物件對映是一個非常有用的工具,兩個物件的屬性相同或相似,將一個物件的值複製給另一個物件。

DTO和實體類通常具有相同或相似的屬性,通常需要根據實體和業務需求來建立DTO物件。ABP框架物件對映基於 AutoMapper,相比手動賦值,效率更高。

  • 僅對實體到輸出DTO使用自動物件對映。
  • 輸入DTO到實體不適用自動物件對映。

不使用輸入DTO到實體自動對映的原因:

  1. 實體類通常有建構函式,接收引數並在建立時,進行引數驗證。自動物件對映操作通常需要無參建構函式建立物件。
  2. 實體屬性設定器大多是私有的,應該使用方法設定屬性值
  3. 通常需要仔細驗證和處理使用者/客戶端輸入,而不是盲目地對映到實體屬性。

雖然其中一些問題可以通過對映配置來解決(例如,AutoMapper允許定義自定義對映規則),但它使你的業務邏輯隱含/隱藏,並與基礎設施緊密耦合。我們認為業務程式碼應該是明確的、清晰的、容易理解的。

學習幫助

圍繞DDDABP Framework兩個核心技術,後面還會陸續釋出核心構件實現綜合案例實現系列文章,敬請關注!

ABP Framework 研習社(QQ群:726299208)
專注 ABP Framework 學習及DDD實施經驗分享;示例原始碼、電子書共享,歡迎加入!