聊聊Dto 和 Poco(或 Pojo)有什么區(qū)別,你知道嗎?
本文轉(zhuǎn)載自微信公眾號(hào)「DotNET技術(shù)圈」,作者Ardalis Steve 。轉(zhuǎn)載本文請(qǐng)聯(lián)系DotNET技術(shù)圈公眾號(hào)。
在討論 .NET 和 C# 中的軟件開(kāi)發(fā)時(shí)經(jīng)常出現(xiàn)的兩個(gè)術(shù)語(yǔ)是 DTO 和 POCO。一些開(kāi)發(fā)人員交替使用這些術(shù)語(yǔ)。那么,DTO 和 POCO 之間有什么區(qū)別?首先,讓我們定義每個(gè)術(shù)語(yǔ)。
數(shù)據(jù)傳輸對(duì)象 (DTO)
DTO 是“數(shù)據(jù)傳輸對(duì)象”。它是一個(gè)目的是傳輸數(shù)據(jù)的對(duì)象。根據(jù)定義,DTO 應(yīng)該只包含數(shù)據(jù),而不是邏輯或行為。如果 DTO 包含邏輯,則它不是 DTO。但是等等,什么是“邏輯”或“行為”?
通常,邏輯和行為是指類型上的方法。在 C# 中,DTO 應(yīng)該只有屬性,并且這些屬性應(yīng)該只獲取和設(shè)置數(shù)據(jù),而不是驗(yàn)證數(shù)據(jù)或?qū)ζ鋱?zhí)行其他操作。
屬性和數(shù)據(jù)注釋呢?
將元數(shù)據(jù)添加到 DTO 以使其支持模型驗(yàn)證或類似目的并不罕見(jiàn)。這些屬性不會(huì)向 DTO 本身添加任何行為,而是促進(jìn)系統(tǒng)中其他地方的行為。因此,它們不會(huì)違反 DTO 不應(yīng)包含任何行為的“規(guī)則”。
ViewModel、API 模型等呢?
DTO 一詞非常含糊。它只是說(shuō)一個(gè)對(duì)象只包含數(shù)據(jù),而不是行為。它沒(méi)有說(shuō)明其預(yù)期用途。在許多架構(gòu)中,DTO 可以充當(dāng)多種角色。例如,在大多數(shù)具有支持綁定到數(shù)據(jù)類型的視圖的 MVC 架構(gòu)中,DTO 用于將數(shù)據(jù)傳遞和綁定到視圖。這些 DTO 通常稱為 ViewModel,理想情況下它們應(yīng)該沒(méi)有行為,只有按照 View 期望的格式設(shè)置數(shù)據(jù)。因此,在這種情況下,ViewModel 是一種特定類型的 DTO。但是,要小心。然后你不能得出所有 ViewModel 都是 DTO 的結(jié)論,因?yàn)樵贛VVM 架構(gòu)中[1]ViewModel 通常包含大量行為。因此,在做出任何廣泛假設(shè)之前考慮上下文非常重要。即使在 MVC 應(yīng)用程序中,有時(shí)邏輯也會(huì)添加到 ViewModel 中,這樣它們就不再是 DTO。
DTO 和 ViewModels
只要可能,請(qǐng)根據(jù)其預(yù)期用途命名您的 DTO。命名一個(gè)類FooDTO并沒(méi)有說(shuō)明在應(yīng)用程序的體系結(jié)構(gòu)中應(yīng)該如何或在何處使用該類型。相反,更喜歡像FooViewModel.
C# 中的示例 DTO
下面是 C# 中的示例 DTO 對(duì)象:
- public class ProductViewModel
- {
- public int ProductId { get; set; }
- public string Name { get; set; }
- public string Description { get; set; }
- public string ImageUrl { get; set; }
- public decimal UnitPrice { get; set; }
- }
封裝和數(shù)據(jù)傳輸對(duì)象
封裝是面向?qū)ο笤O(shè)計(jì)的重要原則。但它不適用于 DTO。封裝用于防止類的協(xié)作者過(guò)于依賴有關(guān)類如何執(zhí)行其操作或存儲(chǔ)其數(shù)據(jù)的特定實(shí)現(xiàn)細(xì)節(jié)。由于 DTO 沒(méi)有操作或行為,并且應(yīng)該沒(méi)有隱藏狀態(tài),因此它們不需要封裝。不要通過(guò)使用私有 setter 或試圖讓你的 DTO 表現(xiàn)得像不可變的值對(duì)象,從而使你的生活變得更艱難。您的 DTO 應(yīng)該易于創(chuàng)建、易于編寫(xiě)和易于閱讀。他們應(yīng)該支持序列化而不需要任何自定義工作來(lái)支持它。
字段或?qū)傩?/h3>
既然 DTO 不關(guān)心封裝,為什么要使用屬性呢?為什么不只使用字段?您可以使用任何一種,但某些序列化框架僅適用于屬性。我通常使用屬性,因?yàn)檫@是 C# 中的約定,但是如果您更喜歡公共字段或有為什么它們更可取的設(shè)計(jì)原因,您當(dāng)然可以使用它們。無(wú)論您選擇哪種方式,我都會(huì)嘗試在您的應(yīng)用程序中使用字段或?qū)傩詴r(shí)保持一致。有利弊的一些討論在這里[3]。
不變性和記錄類型
不變性在軟件開(kāi)發(fā)中有很多好處,并且在 DTO 中也是一個(gè)有用的特性。Jimmy Bogard 寫(xiě)過(guò)關(guān)于嘗試在 DTO 中實(shí)現(xiàn)不變性的文章[4],而Mark Seeman[5]在對(duì)該文章的評(píng)論中(以及在上面的堆棧溢出問(wèn)題中)采用了相反的方法。就我個(gè)人而言,我通常不會(huì)將 DTO 構(gòu)建為不可變的,正如您從上面顯示的示例中看到的那樣。不過(guò),這可能會(huì)隨著C# 9 及其引入的記錄類型而改變[6]。順便說(shuō)一下,您可能會(huì)看到的另一個(gè)首字母縮寫(xiě)詞是數(shù)據(jù)傳輸記錄或 DTR。這是使用 C# 9 定義 DTR 的一種方法:
- public record ProductDTO(int Id, string Name, string Description);
當(dāng)使用記錄類型和上述位置聲明時(shí),會(huì)為您生成一個(gè)構(gòu)造函數(shù),其順序與聲明相同。因此,您將使用以下語(yǔ)法創(chuàng)建此 DTR:
- var dto = new ProductDTO(1, "devBetter Membership", "A one-year subscription to devBetter.com");
或者,您可以以更傳統(tǒng)的方式定義屬性并在構(gòu)造函數(shù)中設(shè)置它們。另一個(gè)新特性是 init-only 屬性,它支持在創(chuàng)建時(shí)初始化,但在其他方面是只讀的,保持記錄不可變。一個(gè)例子:
- public record ProductDTO
- {
- public int Id { get; init; }
- public string Name { get; init; }
- }
- // usage
- var dto = new ProductDTO { Id = 1, Name = "some name" };
C# 記錄類型在使用位置聲明時(shí)無(wú)需任何特殊努力即可支持序列化。如果您創(chuàng)建自己的自定義構(gòu)造函數(shù),則可能需要向序列化程序提供一些提示。隨著 C# 9、.NET 5 和記錄類型越來(lái)越流行,我希望能經(jīng)常將它們用于 DTR。
普通舊 CLR 對(duì)象或普通舊 C# 對(duì)象 (POCO)
一個(gè)普通的舊 CLR/C# 對(duì)象是一個(gè) POCO。Java 有普通的舊 Java 對(duì)象,或 POJO。你真的可以將這些統(tǒng)稱為“Plain Old Objects”,但我猜有人不喜歡產(chǎn)生的首字母縮略詞。那么,一個(gè)對(duì)象“老舊”是什么意思呢?基本上,它不依賴于特定的框架或庫(kù)來(lái)運(yùn)行。一個(gè)普通的舊對(duì)象可以在您的應(yīng)用程序或測(cè)試中的任何地方實(shí)例化,并且不需要涉及特定的數(shù)據(jù)庫(kù)或第三方框架來(lái)運(yùn)行。
通過(guò)展示反例來(lái)演示 POCO 是最簡(jiǎn)單的。以下類依賴于一些引用數(shù)據(jù)庫(kù)的靜態(tài)方法,這使得該類完全依賴于數(shù)據(jù)庫(kù)的存在才能發(fā)揮作用。它還繼承自(組成的)第三方持久性框架中定義的類型。
- public class Product : DataObject<Product>
- {
- public Product(int id)
- {
- Id = id;
- InitializeFromDatabase();
- }
- private void InitializeFromDatabase()
- {
- DataHelpers.LoadFromDatabase(this);
- }
- public int Id { get; private set; }
- // other properties and methods
- }
給定這個(gè)類定義,假設(shè)您想對(duì) 上的某個(gè)方法進(jìn)行單元測(cè)試Product。你編寫(xiě)測(cè)試,你做的第一件事就是實(shí)例化一個(gè)新的實(shí)例,Product這樣你就可以調(diào)用它的方法。并且您的測(cè)試立即失敗,因?yàn)槟形礊镈ataHelpers.LoadFromDatabase要使用的方法配置連接字符串。這是Active Record 模式的[7]一個(gè)例子,它可以使單元測(cè)試變得更加困難。此類不是Persistence Ignorant (PI),[8]因?yàn)樗某志眯灾苯尤谌氲筋惐旧碇?,并且該類需要從與持久性相關(guān)的基類繼承。POCO 的一個(gè)特點(diǎn)是它們往往對(duì)持久性一無(wú)所知,或者至少比 Active Record 等替代方法更是如此。
一個(gè)示例 POCO
下面是一個(gè)產(chǎn)品的普通舊 C# 對(duì)象示例。
- public class Product
- {
- public Product(int id)
- {
- Id = id;
- }
- private Product()
- {
- // required for EF
- }
- public int Id { get; private set; }
- // other properties and methods
- }
這個(gè)Product類是一個(gè) POCO,因?yàn)樗灰蕾嚨谌降男袨榭蚣埽绕涫浅志没袨?。它不需要基類,尤其是另一個(gè)庫(kù)中的基類。它與靜態(tài)助手沒(méi)有任何緊密耦合。它可以在任何地方輕松實(shí)例化。它比前面的示例更不了解持久性,但它并非完全不了解持久性,因?yàn)樗幸粋€(gè)無(wú)用的私有構(gòu)造函數(shù)聲明。正如您從評(píng)論中看到的那樣,私有無(wú)參數(shù)構(gòu)造函數(shù)之所以存在,是因?yàn)閷?shí)體框架在從持久性讀取類時(shí)需要它來(lái)實(shí)例化類。
為了論證起見(jiàn),假設(shè)這兩個(gè)Product類都包含除了顯示的構(gòu)造函數(shù)和屬性之外的具有行為的方法。這些可以在應(yīng)用程序中用作DDD 實(shí)體[9],對(duì)系統(tǒng)內(nèi)產(chǎn)品的狀態(tài)和行為進(jìn)行建模。
POCO 和 DTO
好的,所以我們已經(jīng)看到 DTO 只是一個(gè)數(shù)據(jù)傳輸對(duì)象,而 POCO 是一個(gè)普通的舊 C#(或 CLR)對(duì)象。但是它們之間的關(guān)系是什么,為什么開(kāi)發(fā)人員經(jīng)?;煜@兩個(gè)術(shù)語(yǔ)?除了首字母縮略詞的相似性之外,最大的因素可能是所有 DTO 都是(或應(yīng)該是)POCO。
請(qǐng)記住,DTO 的唯一目的是盡可能簡(jiǎn)單地傳輸數(shù)據(jù)。它們應(yīng)該易于創(chuàng)建、閱讀和編寫(xiě)。它們對(duì)第三方框架中定義的特殊基類的任何依賴或?qū)⑺鼈兣c某些行為緊密耦合的靜態(tài)調(diào)用都會(huì)破壞使類成為 DTO 的規(guī)則。為了成為 DTO,類必須是 POCO。所有 DTO 都是 POCO。
DTO 和 POCO 維恩圖
如果反過(guò)來(lái)也成立,那么我們可以說(shuō)這兩個(gè)術(shù)語(yǔ)是等價(jià)的。但我們知道事實(shí)并非如此。在前面的代碼示例中,Product使用 Entity Framework 的實(shí)體具有私有的 setter 和行為,使其無(wú)法成為 DTO。但正如我們所見(jiàn),它是 POCO 的一個(gè)很好的例子。因此,雖然所有 DTO 都是 POCO,但并非所有 POCO 都是 DTO。
References
[1] MVVM 架構(gòu)中: https://en.wikipedia.org/wiki/Model–view–viewmodel
[2]: https://ardalis.com/static/ef316c071c0c70148e4b0008830eeae1/d52e5/dto-viewmodel-venn.png
[3] 在這里: https://stackoverflow.com/questions/10831314/dtos-properties-or-fields
[4] Jimmy Bogard 寫(xiě)過(guò)關(guān)于嘗試在 DTO 中實(shí)現(xiàn)不變性的文章: https://jimmybogard.com/immutability-in-dtos/
[5] Mark Seeman: http://blog.ploeh.dk/
[6] C# 9 及其引入的記錄類型而改變: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9
[7] Active Record 模式的: https://www.martinfowler.com/eaaCatalog/activeRecord.html
[8] Persistence Ignorant (PI),: https://deviq.com/principles/persistence-ignorance
[9] DDD 實(shí)體:
https://deviq.com/domain-driven-design/entity : https://ardalis.com/static/517f68e51029cdcba6b2d49ca477b863/7fee5/dto-poco-venn.png
原文鏈接:https://ardalis.com/dto-or-poco/
作者:Ardalis Steve