系列文章
- 基於ABP落地領域驅動設計-00.目錄和前言
- 基於ABP落地領域驅動設計-01.全景圖
- 基於ABP落地領域驅動設計-02.聚合和聚合根的最佳實踐和原則
- 基於ABP落地領域驅動設計-03.倉儲和規約最佳實踐和原則
- 基於ABP落地領域驅動設計-04.領域服務和應用服務的最佳實踐和原則
- 基於ABP落地領域驅動設計-05.實體創建和更新最佳實踐
- 基於ABP落地領域驅動設計-06.正確區分領域邏輯和應用邏輯
圍繞DDD和ABP 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可以包含比用例需要的更多屬性
Create
和Update
方法中返回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;}
}
- 移除
GetUserNameAndEmail
和GetRoles
方法,因為 Get 方法已經返回足夠需要的信息。 GetList
返回對象與Get
相同Create
和Update
同樣返回UserDto
由此可見,返回相同DTO更加簡潔。
為什么創建或更新之后要返回DTO? 想象一個用例場景,在頁面中顯示表格數據,當更新之后,獲取返回對象,並對表格數據源進行更新,這樣就不需要再次調用 GetList
方法,這是我們建議在 Create
和 Update
方法中返回 DTO 的原因。
討論
以上關於輸出DTO的建議,並不適用所有場景。
出於性能考慮,這些建議可以被忽略,特別是當存在大型數據集返回結果時,或者用戶界面需要發起很多並發請求時,此時應該創建特定的輸出DTO,只包含盡可能少的信息。
可維護性和性能,需要開發者權衡,上面的建議適用於性能損失可忽略不計的應用。
對象映射
自動對象映射是一個非常有用的工具,兩個對象的屬性相同或相似,將一個對象的值復制給另一個對象。
DTO和實體類通常具有相同或相似的屬性,通常需要根據實體和業務需求來創建DTO對象。ABP框架對象映射基於 AutoMapper,相比手動賦值,效率更高。
- 僅對實體到輸出DTO使用自動對象映射。
- 輸入DTO到實體,不適用自動對象映射。
不使用輸入DTO到實體自動映射的原因:
- 實體類通常有構造函數,接收參數並在創建時,進行參數驗證。自動對象映射操作通常需要無參構造函數創建對象。
- 實體屬性設置器大多是私有的,應該使用方法設置屬性值。
- 通常需要仔細驗證和處理用戶/客戶端輸入,而不是盲目地映射到實體屬性。
雖然其中一些問題可以通過映射配置來解決(例如,AutoMapper允許定義自定義映射規則),但它使你的業務邏輯隱含/隱藏,並與基礎設施緊密耦合。我們認為業務代碼應該是明確的、清晰的、容易理解的。
學習幫助
圍繞DDD和ABP Framework兩個核心技術,后面還會陸續發布核心構件實現、綜合案例實現系列文章,敬請關注!
ABP Framework 研習社(QQ群:726299208)
專注 ABP Framework 學習及DDD實施經驗分享;示例源碼、電子書共享,歡迎加入!