什麼是DTO
在說明什麼DTO之前,先來看一個名詞Business Object (BO)。以下引述自Dino Esposito與Andrea Saltarello合著的Microsoft .NET: Architecting Applications for the Enterprise中對BO的定義
Most of the time, a business object (BO) is just the implementation of a domain entity—namely, a class that encapsulates both data and behavior.
A business object contains both data and behavior, and it can be considered a full-fledged active object participating in the domain logic.從以上定義不難看出,BO即是一個帶有資料及操作行為的物件,如下範例類別
public class UserInfo { public string Account { get; set; } public string Password { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string PostCode { get; set; } public string Address { get; set; } public DateTime LastModifiedTime { get; set; } public DateTime CreatedTime { get; set; } public string GetFullName() { return string.Concat(this.FirstName, this.LastName); } public string GetFullAddress() { return string.Concat(this.PostCode, this.Address); } }
那DTO呢?
引述自Dino Esposito與Andrea Saltarello合著的Microsoft .NET: Architecting Applications for the Enterprise
A data-transfer object is a sort of value object—namely, a mere container of data with no attached behavior.引述自Martin Folwer的Patterns of Enterprise Application Architecture
An object that carries data between processes in order to reduce the number of method calls.
就DTO[註1]的字面定義來看,"資料傳遞物件"。沒錯,它只是一個帶有資料的物件,沒有任何的操作行為,如以下類別
public class UserInfoDto { public string Account { get; set; } public string Password { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string PostCode { get; set; } public string Address { get; set; } public DateTime LastModifiedTime { get; set; } }
特性
- DTO為一個或多個BO的子集合
- 一個BO可能有多種DTO子集合
- DTO僅包含屬性值而無任何操作行為
- 減少曝露過多資訊(屬性或參數)給client code (僅提供client code會使用到的資料)
使用時機
那麼DTO可以用在什麼情況下呢?當你想縮短Presentation Layer呼叫Service Layer時所需傳遞的參數數量及次數時。
以上面的兩個類別來舉個例子,當Presentation Layer(PL)要去呼叫Service Layer(SL)[註2]某一支Helper類別(如更新使用者資料的Helper class)時,如以下Helper類別
public class UserInfoHelper { public bool UpdateUserInfo(string password) { } }如果要更新的資料少,例如只允許更新密碼,那這個Helper函式我們只會傳一個參數給它。
如果系統允許使用者更新更多的資料,如密碼、姓、名、郵遞區號、住址時,Helper函式就需要5個參數。那如果允許更新更多資料呢?
想必這個Helper函式的signature一定是更長。這時我們會藉助DTO來縮短參數數量,如下Helper類別及client使用範例
public class UserInfoHelper { public static bool UpdateUserInfo(UserInfoDto dto) { } }
class Program { static void Main(string[] args) { UserInfoDto dto = new UserInfoDto(); dto.Password = "p@ssw0rd"; dto.FirstName = "Pete"; dto.LastName = "Chen"; dto.PostCode = "407"; dto.Address = "台中市西屯區"; dto.LastModifiedTime = DateTime.Now; if (UserInfoHelper.UpdateUserInfo(dto)) { Console.WriteLine("updated"); } } }
那在什麼情況下DTO可以減少呼叫Service Layer的次數?
假設有兩個資料表分別為Users與Addresses,使用者基本資料存放在Users;使用者住址存放在Addresses。Users與Addresses的關係是one-to-many,而我們的BO可能會長得如下
UserInfo.cs
public class UserInfo { public string Account { get; set; } public string Password { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string TelphoneNumer { get; set; } public DateTime LastModifiedTime { get; set; } public DateTime CreatedTime { get; set; } public string GetFullName() { return string.Concat(this.FirstName, this.LastName); } }
AddressInfo.cs
public class AddressInfo { public string PostCode { get; set; } public string Address { get; set; } public string GetFullAddress() { return string.Concat(this.PostCode, this.Address); } }
現在有一Helper類別提供新增使用者資料(Users),另一Helper類別提供新增住址(Addresses),如下
UserInfoHelper.cs
public class UserInfoHelper { public static bool InserBasicUserInfo(string account, string password, string firstName, string lastName, string telephoneNumber) { } }
public class AddressInfoHelper { public static bool InsertAddresses(string account, Collection<AddressInfo> addresses) { } }
現在有一使用案例,我們在PL提供使用者註冊資料,使用者的住址可以多個(假設無上限),當使用者按下註冊鈕時(假設系統不需審核使用者資料或使用者不需經過帳號啟動程序),可能的client code會長得像下列程式碼
class Program { static void Main(string[] args) { if (UserInfoHelper.InserBasicUserInfo("pete", "p@ssw0rd", "Pete", "Chen", "28825252")) { Collection<Addressinfo> addresses = new Collection<Addressinfo>(); addresses.Add(new AddressInfo() { PostCode = "407", Address = "台中市西屯區" }); addresses.Add(new AddressInfo() { PostCode = "412", Address = "台中市大里區" }); if (AddressInfoHelper.InsertAddresses("pete", addresses)) { Console.WriteLine("inserted"); return; } } Console.WriteLine("failed"); } }以上範例暫不考慮transaction的問題。
單就以上程式碼來看,InserBasicUserInfo的參數不儘有5個,且對於PL來說,相同的一個流程(針對新增使用者資料來看),
呼叫了兩個Business Layer(BL)的函式(UserInfoHelper.InserBasicUserInfo及AddressInfoHelper.InsertAddresses)。
若聯絡電話也要分出一個table來儲存,也提供一個Helper類別來操作它,則在相同一個流程中,就得呼叫三支BL的函式,以此類推。
此時我們可以使用DTO將所需要用到的資料集中在一個類別中,如下
public class UserInfoDto { public string Account { get; set; } public string Password { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string TelephoneNumber { get; set; } public Collection<AddressInfo> Addresses { get; set; } }
接下來在UserInfoHelper.cs只要將參數型別修改為只有UserInfoDto,如下
public class UserInfoHelper { private static bool InserBasicUserInfo(string account, string password, string firstName, string lastName, string telephoneNumber) { // 請根據實際需求修改 return true; } public static bool InsertUserInfo(UserInfoDto dto) { if (InserBasicUserInfo(dto.Account, dto.Password, dto.FirstName, dto.LastName, dto.TelephoneNumber)) { return AddressInfoHelper.InsertAddresses(dto.Account, dto.Addresses); } return false; } }在client code我們即可以呼叫一支Helper來達到我們的需求,如下
class Program { static void Main(string[] args) { UserInfoDto dto = new UserInfoDto(); dto.Account = "pete"; dto.Password = "p@ssw0rd"; dto.FirstName = "Pete"; dto.LastName = "Chen"; dto.TelephoneNumber = "28825252"; Collection<AddressInfo> addresses = new Collection<AddressInfo>(); addresses.Add(new AddressInfo() { PostCode = "407", Address = "台中市西屯區" }); addresses.Add(new AddressInfo() { PostCode = "412", Address = "台中市大里區" }); dto.Addresses = addresses; if (UserInfoHelper.InsertUserInfo(dto)) { Console.WriteLine("inserted"); return; } Console.WriteLine("failed"); } }
由以上程式碼可看出經由DTO可以減少呼叫BL函式的次數(這也呼應了Martin Fowler對DTO的定義"in order to reduce the number of method calls"),且transaction機制我們可以加入至SL而不必暴露於PL。
DTO除了可以用在參數的傳遞上,也可用在資料的請求與回應[註3],例如資料的查詢回應。當回應資料給client時,若將整個BO回應給client code,不免提供過多用不到資料,甚至是空資料。此時DTO也可派上用場,將要呈現給client code的資料放在DTO即可,而不必將整個BO回傳,如下程式碼
public class UserInfoRequestDto { public string Account { get; set; } }
public class UserInfoResponseDto { public string FullName { get; set; } public string TelephoneNumber { get; set; } }
class Program { static void Main(string[] args) { UserInfoRequestDto request = new UserInfoRequestDto(); request.Account = "pete"; UserInfoResonseDto response = UserInfoHelper.GetUser(request); Console.WriteLine("FullName: {0}",response.FullName); Console.WriteLine("TelephoneNumber: {0}", response.TelephoneNumber); } }邊際效應
- 過多的小類別,因為一個BO可能產生多個DTO
- 過多的code duplication,因為DTO是一個或多個以上BO的子集合,所以屬性大多是與BO的屬性一樣,當建立DTO時,會有很多的copy&paste動作
備註
- Data Transfer Object亦稱為Value Object,因此物件僅包含屬性值而無任何操作行為(Behavior)
- Service Layer(SL)為Presentation Layer(PL)與Business Layer(BL)的中介層,用於降低PL與BL的耦合性,並將實際的商務邏輯功能委派給BL執行,一個SL操作(coarse-grained operation)通常會包含一個以上的BL操作(fine-grained operation)
- 此種應用亦稱為Request-Response Pattern
延伸閱讀
使用AutoMapper簡化Data Transfer Object與Business Entity的對應程式碼