顧名思義他就是一個拿來傳遞資料的物件。
- 用來傳遞資料
- 沒有任何操作行為、業務邏輯在裡面
- 通常不設定建構子(不使用建構子設定初始值)
我會在這幾個時候使用 DTO
- 方法簽章參數多(long parameter)
- 分層架構中層與層的分離
情境一:refactor 遇到 long parameter 的時候會改用 DTO。
改成 DTO 前應該思考方法簽章中的參數是否可以聚合出有意義集合,因為有意義的集合更能讓人理解這是為了傳遞什麼資料,產生出的 DTO 更有機會在專案中重複利用。
//超多參數閱讀困難
public void SetUserInfo(string name, string address, string country, DateTime birth, string phoneNumber)
{
}
//Transform parameters
public class UserInfo
{
public string Name { get; set; }
public string Address { get; set; }
public string Country { get; set; }
public DateTime Birth { get; set; }
public string PhoneNumber { get; set; }
}
//參數變成一個而已
public void SetUserInfo(UserInfo userInfo)
{
}
//使用端直接給定屬性值
UserInfo userInfo = new UserInfo
{
Name = "AMA",
Address = "",
Country = "TW",
Birth = new DateTime(1990, 1, 1),
PhoneNumber = "0910-000-000"
};
通常不使用建構子設定初始值是因為仍然難以閱讀,這樣就失去了使用 DTO 的意義。
public class UserInfo
{
public string Name { get; set; }
public string Address { get; set; }
public string Country { get; set; }
public DateTime Birth { get; set; }
public string PhoneNumber { get; set; }
//如果使用建構子初始值,建構子的參數跟一開始的方法簽章一樣長
public UserInfo(string name, string address, string country, DateTime birth, string phoneNumber)
{
Name = name;
Address = address;
Country = country;
Birth = birth;
PhoneNumber = phoneNumber;
}
}
情境二:分層架構中層與層的分離
//Controller
//GET api/UserInfo/AMA
public UserInfoResponseDto GetUserInfo(string name)
{
//DB 存取層回來的物件是 UserInfo
UserInfo userInfo = db.GetUserInfo(name);
//將 UserInfo 轉換成 UserInfoResponseDto
return userInfo == null ? null: new UserInfoResponseDto()
{
Name = userInfo.Name,
Address = userInfo.Address,
Country = userInfo.Country,
Birth = userInfo.Birth,
PhoneNumber = userInfo.PhoneNumber
});
}
如果回傳型態是用 UserInfo,而不是 UserInfoResponseDto,當 UserInfo 加一個 Property 是用戶端不需要的,那該怎麼辦呢?
如果沒有用 DTO 隔離的話,改 UserInfo 就會直接影響到 API 接口,而這並不是我們預期的情況,這時有使用 DTO 的話 API 接口就可以避免受到影響。
雖然說通常不使用建構子設定初始值,但遇到這種情況我還是會使用建構子設定初始值
//ClassA 與 ClassB 分別對應資料庫中的兩張資料表
//ClassA 原本擺所有產品的資料,但資料量大且異動頻率高導致一些問題,將部分產品擺到 ClassB
//Property ABC 資料型態一致,名稱也一致
//Property D 名稱一致,但資料型態一個是 double, 一個是 decimal。因為 double 運算會有溢位的問題,我們在 ClassA 時運算前都會先轉成 decimal 才計算,因此,新開的 ClassB 當時就直接將資料型態開成 decimal
//產品分在 ClassA 及 ClassB 之後因為產品有各自的特性,因此又出現了不一致的屬性 ClassA.E 跟 ClassB.F
public class ClassA
{
public string A { get; set; }
public string B { get; set; }
public DateTime C { get; set; }
//資料型態 double
public double D { get; set; }
//只有 ClassA 才有的屬性
public string E { get; set; }
}
public class ClassB
{
public string A { get; set; }
public string B { get; set; }
public DateTime C { get; set; }
//資料型態 decimal
public decimal D { get; set; }
//只有 ClassB 才有的屬性
public string F { get; set; }
}
因為有很多使用情況我需要同時處理這些產品,這樣就會需要建簽章不一樣,內容一樣的兩個方法
public void Method(ClassA a)
{
//to do
}
public void Method(ClassB b)
{
//to do
}
這時候方法簽章改用 DTO 就比較好維護
public class ProductDto
{
public string A { get; set; }
public string B { get; set; }
public DateTime C { get; set; }
//資料型態使用 decimal, 使用於計算時就不用再轉換型態
public decimal D { get; set; }
public ProductDto(ClassA a)
{
A = a.A;
B = a.B;
C = a.C;
D = Convert.ToDecimal(a.D);
}
public ProductDto(ClassB b)
{
A = b.A;
B = b.B;
C = b.C;
D = b.D;
}
}
public void Method(ProductDto p)
{
//to do
}
DTO 還有很多種使用情境,只要不破壞 DTO 不處理邏輯的原則,其餘都可以彈性調整,至於命名規則要不要後墜 DTO,部門內有共識即可。
參考資料:
https://learn.microsoft.com/en-us/aspnet/web-api/overview/data/using-web-api-with-entity-framework/part-5
https://stackoverflow.com/questions/72535903/asp-net-c-sharp-dto-initialization-by-constructor-with-parameters-or-paramless-c