December 26, 2010

Data Transfer Object使用心得及時機

Data Transfer Object (DTO)一詞最早出現於何處筆者並不確定,但大部份對DTO的研究常會參考Martin Folwer的著作Patterns of Enterprise Application Architecture其中Data Transfer Object章節。事實上,許多開發人員可能早就已經使用它而不自知,以下是筆者在使用DTO上的心得

什麼是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 FolwerPatterns 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)
        {
        }
    }
AddressInfoHelper.cs
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動作

備註
  1. Data Transfer Object亦稱為Value Object,因此物件僅包含屬性值而無任何操作行為(Behavior)
  2. Service Layer(SL)為Presentation Layer(PL)與Business Layer(BL)的中介層,用於降低PL與BL的耦合性,並將實際的商務邏輯功能委派給BL執行,一個SL操作(coarse-grained operation)通常會包含一個以上的BL操作(fine-grained operation)
  3. 此種應用亦稱為Request-Response Pattern

延伸閱讀
使用AutoMapper簡化Data Transfer Object與Business Entity的對應程式碼