EF Core中避免贫血模型的三种行之有效的方法(翻译)
[Paul Hiles: 3 ways to avoid an anemic domain model in EF Core :https://www.devtrends.co.uk/blog/3-ways-to-avoid-an-anemic-domain-model-in-ef-core]
1.引言
在使用ORM中(比如Entity?Framework)貧血領(lǐng)域模型十分常見(jiàn)?。本篇文章將先探討貧血模型的問(wèn)題,再去探究在EF?Core中使用Code?First時(shí)如何使用簡(jiǎn)單的方法來(lái)避免貧血模型。
2.什么是貧血模型
在對(duì)領(lǐng)域建模后,輸出一系列類(lèi)中僅包含一些簡(jiǎn)單屬性聲明而不包含業(yè)務(wù)邏輯的模型,就屬于貧血模型。當(dāng)使用Entity?Framework時(shí),它們不僅僅是簡(jiǎn)單的數(shù)據(jù)持有者而且包含有一堆public?getter和public?setters:
public?class?BlogPost
{
????public?int?Id?{?get;?set;?}
????[Required]
????[StringLength(250)]
????public?string?Title?{?get;?set;?}
????[Required]
????[StringLength(500)]
????public?string?Summary?{?get;?set;?}
????[Required]
????public?string?Body?{?get;?set;?}
????public?DateTime?DateAdded?{?get;?set;?}
????public?DateTime??DatePublished?{?get;?set;?}
????public?BlogPostStatus?Status?{?get;?set;?}
????...
}
由于其完全缺乏面向?qū)ο缶幊痰脑瓌t,因此貧血模型通常被描述為反模式。他們需要調(diào)用者來(lái)完善驗(yàn)證和其他業(yè)務(wù)邏輯。由于缺乏相應(yīng)的抽象,就會(huì)導(dǎo)致代碼重復(fù)、較差的數(shù)據(jù)完整性,以及增加高層模塊的復(fù)雜性。 貧血模型是十分常見(jiàn)的。從我的經(jīng)驗(yàn)來(lái)看,EF中超過(guò)80%的領(lǐng)域模型都是貧血模型。這并不奇怪。幾乎所有的文檔和其他博客文章都以最簡(jiǎn)單的方式展示了EF。他們專(zhuān)注于盡可能快地開(kāi)始工作,而不是主張最佳實(shí)踐。
3.改造為更豐富的領(lǐng)域模型(充血模型)
下面我們將討論三種簡(jiǎn)單的方式去豐富你的貧血模型。這幾種方法都非常簡(jiǎn)單,僅需要最小的改動(dòng)。
3.1.移除無(wú)參公共構(gòu)造函數(shù)
除非你指定一個(gè)構(gòu)造函數(shù),否則你的類(lèi)將有一個(gè)默認(rèn)的無(wú)參數(shù)構(gòu)造函數(shù)。這意味著你可以用下面的方式實(shí)例化你的類(lèi):
var blogPost = new BlogPost();
在大多數(shù)情況下,這是沒(méi)有意義的。領(lǐng)域?qū)ο笸ǔV辽傩枰恍?shù)據(jù)才能使其有效。創(chuàng)建沒(méi)有任何數(shù)據(jù)(如標(biāo)題或URL)的BlogPost實(shí)例是沒(méi)有意義的,因?yàn)槠鋬H僅是一個(gè)實(shí)例化對(duì)象,但對(duì)象卻不包含狀態(tài)和行為,不滿(mǎn)足數(shù)據(jù)有效性。有些人不同意,但是DDD社區(qū)普遍認(rèn)為確保領(lǐng)域?qū)ο笫冀K有效是有意義的。為了解決這個(gè)問(wèn)題,我們可以像處理其他OO類(lèi)一樣對(duì)待我們的域類(lèi),并引入一個(gè)參數(shù)化的構(gòu)造函數(shù):
public BlogPost(string title, string summary, string body)
{
? ?if (string.IsNullOrWhiteSpace(title))
? ?{
? ? ? ?throw new ArgumentException("Title is required");
? ?}
? ?...
? ?Title = title;
? ?Summary = summary;
? ?Body = body;
? ?DateAdded = DateTime.UtcNow;
}
現(xiàn)在在調(diào)用代碼必須提供最少的數(shù)據(jù)來(lái)滿(mǎn)足約束(構(gòu)造函數(shù))。這一變化提供了兩個(gè)積極成果:
任何新實(shí)例化的BlogPost對(duì)象現(xiàn)在都保證有效。作用于BlogPost的任何代碼都無(wú)需檢查其有效性。領(lǐng)域?qū)ο笤趯?shí)例化時(shí)自動(dòng)校驗(yàn)自身的有效性。
任何調(diào)用代碼都知道實(shí)例化對(duì)象所需的內(nèi)容。使用無(wú)參數(shù)的構(gòu)造函數(shù),很容易構(gòu)造對(duì)象,但卻不知道必須要構(gòu)建的數(shù)據(jù)才能保證數(shù)據(jù)有效性。
但不幸的是,在進(jìn)行此更改后,您將發(fā)現(xiàn)在從數(shù)據(jù)庫(kù)中檢索實(shí)體時(shí),您的EF代碼不再有效:
InvalidOperationException:在實(shí)體類(lèi)型'BlogPost'上找不到無(wú)參數(shù)的構(gòu)造函數(shù)。為了創(chuàng)建'BlogPost'的實(shí)例,EF需要聲明一個(gè)無(wú)參數(shù)的構(gòu)造函數(shù)。
EF需要一個(gè)無(wú)參數(shù)的構(gòu)造函數(shù)來(lái)查詢(xún)?cè)撟鍪裁?#xff1f;幸運(yùn)的是,盡管EF確實(shí)需要無(wú)參數(shù)構(gòu)造函數(shù),但它并不要求構(gòu)造函數(shù)必須為public,所以我們可以為EF增加一個(gè)無(wú)參private構(gòu)造函數(shù),同時(shí)強(qiáng)制調(diào)用代碼使用參數(shù)化構(gòu)造函數(shù)。擁有額外的構(gòu)造函數(shù)顯然并不理想,但這些妥協(xié)通常可以時(shí)ORM與OO代碼更好地配合。
private BlogPost()
{
? ?// just for EF
}
public BlogPost(string title, string summary, string body)
{
? ?...
}
3.2. 刪除公共屬性中的set方法
上面介紹的參數(shù)化構(gòu)造函數(shù)確保在實(shí)例化時(shí)對(duì)象處于有效狀態(tài)。盡管如此,這并沒(méi)有阻止您將屬性值更改為無(wú)效值。要解決這個(gè)問(wèn)題,我們有兩個(gè)選擇:
將驗(yàn)證邏輯添加到屬性設(shè)置器
防止直接修改屬性,改為使用與用戶(hù)操作相對(duì)應(yīng)的方法
向?qū)傩栽O(shè)置器添加驗(yàn)證是完全可以接受的,但意味著我們不能再使用自動(dòng)屬性并且必須引入一個(gè)后臺(tái)字段。顯然這不是什么大問(wèn)題:
private string title;
public string Title
{
? ?get { return title; }
? ?set
? ?{
? ? ? ?if (string.IsNullOrWhiteSpace(value))
? ? ? ?{
? ? ? ? ? ?throw new ArgumentException("Title must contain a value");
? ? ? ?}
? ? ? ?title = value;
? ?}
}
第二種方式更受歡迎的主要原因在于它更接近地模擬了現(xiàn)實(shí)世界中發(fā)生的事情。用戶(hù)不是孤立地更新單個(gè)屬性,而是傾向于執(zhí)行一組已知操作(由UI或API接口確定)。這些操作可能會(huì)導(dǎo)致一個(gè)或多個(gè)屬性被更新,但通常情況下更多。業(yè)務(wù)邏輯依賴(lài)于上下文的場(chǎng)景是非常普遍的,這將會(huì)導(dǎo)致對(duì)屬性進(jìn)行賦值的set中的驗(yàn)證邏輯變得復(fù)雜而難以理解。作為基本示例,請(qǐng)考慮以下博客文章發(fā)布流程:
public void Publish()
{
? ?if (Status == BlogPostStatus.Draft || Status == BlogPostStatus.Archived)
? ?{
? ? ? ?if (Status == BlogPostStatus.Draft)
? ? ? ?{
? ? ? ? ? ?DatePublished = DateTime.UtcNow;
? ? ? ?}
? ? ? ?Status = BlogPostStatus.Published;
? ?}
}
在這個(gè)例子中,我們有一個(gè)Publish()方法,它有一些簡(jiǎn)單的邏輯和兩個(gè)可以更新的屬性。我們也可以將其作為一個(gè)屬性的setter來(lái)實(shí)現(xiàn),但它不太清晰,尤其是從另一個(gè)類(lèi)中調(diào)用它時(shí):
blogPost.Status = BlogPostStatus.Published;
VS
blogPost.Publish();
第一種方式的副作用是不能清晰的表達(dá)業(yè)務(wù)用例。
當(dāng)然,你在大多數(shù)代碼庫(kù)中看到的是根本不在領(lǐng)域?qū)ο笾羞M(jìn)行驗(yàn)證。相反,這種類(lèi)型的邏輯可以在下一層找到。這可能導(dǎo)致:
更長(zhǎng)的方法將領(lǐng)域特定的邏輯與編排、持久性和其他關(guān)注點(diǎn)混合在一起。
不同動(dòng)作之間重復(fù)的驗(yàn)證邏輯。
由于外部依賴(lài)性(需要使用Mock)而難以測(cè)試純領(lǐng)域邏輯。
正如我們現(xiàn)在所期望的那樣,如果我們從每個(gè)屬性中徹底移除setter,EF將無(wú)法正常運(yùn)行,但將訪(fǎng)問(wèn)級(jí)別更改為private就可以很好地解決問(wèn)題:
public class BlogPost
{
? ?public int Id { get; private set; }
? ?...
}
這樣,所有屬性在類(lèi)之外都是只讀的。為了允許更新我們的領(lǐng)域類(lèi),我們引入了相應(yīng)類(lèi)型動(dòng)作的方法,如上面所示的Publish方法。
通過(guò)刪除無(wú)參數(shù)構(gòu)造函數(shù)和公共屬性設(shè)置器并添加動(dòng)作類(lèi)型的方法,我們現(xiàn)在擁有了始終有效的領(lǐng)域?qū)ο?#xff0c;并包含了與所討論的實(shí)體直接相關(guān)的所有業(yè)務(wù)邏輯,這是一個(gè)很大的改進(jìn)。我們已經(jīng)使我們的代碼同時(shí)更加健壯和簡(jiǎn)單。
雖然我們可以討論其他DDD概念,例如領(lǐng)域事件以及通過(guò)雙派遣模式([double-dispatch pattern:http://idior.cnblogs.com/articles/325036.html])使用領(lǐng)域服務(wù),但它們的優(yōu)勢(shì),特別是簡(jiǎn)單性方面的優(yōu)勢(shì)遠(yuǎn)不是那么明顯。 通常DDD概念中可以簡(jiǎn)化代碼的是我們將在下面討論的值對(duì)象的使用。
3.3.引入值對(duì)象
[值對(duì)象:https://martinfowler.com/bliki/ValueObject.html]是不可變的(實(shí)例化后不允許更改)沒(méi)有身份標(biāo)識(shí)的對(duì)象。值對(duì)象通??梢杂脕?lái)代替領(lǐng)域?qū)ο笾械囊粋€(gè)或多個(gè)屬性。
值對(duì)象的經(jīng)典示例包括貨幣,地址和坐標(biāo),但也可以使用值類(lèi)型替換單個(gè)屬性,而不是使用字符串或整型。例如,不是將電話(huà)號(hào)碼存儲(chǔ)為字符串,而是可以創(chuàng)建一個(gè)帶有內(nèi)置驗(yàn)證的PhoneNumber值類(lèi)型以及提取撥號(hào)代碼的方法等。
下面的代碼顯示了一個(gè)實(shí)現(xiàn)為EF類(lèi)使用的貨幣值對(duì)象:
public class Money
{
? ?[StringLength(3)]
? ?public string Currency { get; private set; }
? ?public int Amount { get; private set; }
? ?private Money()
? ?{
? ? ? ?// just for EF
? ?}
? ?public Money(string currency, int amount)
? ?{
? ? ? ?// todo validation
? ? ? ?Currency = currency;
? ? ? ?Amount = amount;
? ?}
}
貨幣和金額是內(nèi)在聯(lián)系的。為了使數(shù)據(jù)有效,這兩條信息都是必需的。因此,對(duì)它們進(jìn)行建模是有道理的。請(qǐng)注意,參數(shù)化的構(gòu)造函數(shù)和私有屬性設(shè)置器的使用方式與我們?cè)诮nI(lǐng)域?qū)ο髸r(shí)所使用的完全相同。實(shí)體框架也需要一個(gè)私有無(wú)參數(shù)構(gòu)造函數(shù)。
在(RDBMS)數(shù)據(jù)持久性的上下文中,值類(lèi)型不存在于單獨(dú)的數(shù)據(jù)庫(kù)表中。為了讓我們?cè)趯?shí)體框架中使用值對(duì)象,需要一個(gè)小的改動(dòng)。這取決于您使用的EF版本。
在EF6中,我們只需用[ComplexType]屬性修飾值對(duì)象:
[ComplexType]
public class Money
{
? ?...
}
在EF Core中,從版本2開(kāi)始,我們可以使用Fluent API中不常用的OwnsOne方法:
public class BlogContext : DbContext
{
? ?...
? ?public DbSet<BlogPost> BlogPosts { get; set; }
? ?protected override void OnModelCreating(ModelBuilder modelBuilder)
? ?{
? ? ? ?modelBuilder.Entity<BlogPost>().OwnsOne(x => x.AdvertisingFee);
? ?}
}
這里假定在我們的BlogPost實(shí)體上使用Money值對(duì)象,如下所示:
public class BlogPost
{
? ?...
? ?public Money AdvertisingFee { get; private set; }
? ?...
}
創(chuàng)建并運(yùn)行遷移后,我們會(huì)發(fā)現(xiàn)我們的數(shù)據(jù)庫(kù)表現(xiàn)在包含兩個(gè)額外的列:
AdvertisingFee_Currency
AdvertisingFee_Amount
使用值對(duì)象的好處與向富領(lǐng)域模型的轉(zhuǎn)變非常相似。豐富的領(lǐng)域模型不需要調(diào)用代碼來(lái)驗(yàn)證領(lǐng)域模型,并提供了一個(gè)定義良好的抽象來(lái)進(jìn)行編程。一個(gè)值對(duì)象進(jìn)行自我驗(yàn)證,因此包含值對(duì)象屬性的領(lǐng)域模型本身不需要知道如何驗(yàn)證值類(lèi)型。所有非常清晰和簡(jiǎn)單。
4. 溫馨提示
當(dāng)您打算從貧血域模型轉(zhuǎn)移到更豐富的領(lǐng)域模型時(shí),您將立即體會(huì)到將領(lǐng)域級(jí)的業(yè)務(wù)邏輯封裝在領(lǐng)域?qū)ο笾械暮锰?。?qǐng)注意,盡管如此,嘗試并不是件容易的事。在您的領(lǐng)域?qū)ο笊蟿?chuàng)建一個(gè)方法來(lái)執(zhí)行驗(yàn)證,然后更新多個(gè)屬性無(wú)疑是件好事。但從領(lǐng)域?qū)ο蟀l(fā)送電子郵件或保存到數(shù)據(jù)庫(kù)并不是您可能想要做的事情。重要的是要意識(shí)到,擁有豐富的領(lǐng)域模型并不否定另一層的需求來(lái)安排這些更高層次的關(guān)注。這是應(yīng)用服務(wù)或命令處理程序的工作,具體取決于您的體系結(jié)構(gòu)。
5.關(guān)于單元測(cè)試的說(shuō)明
一個(gè)豐富的、自我驗(yàn)證的領(lǐng)域模型的一個(gè)負(fù)面影響是它可以使測(cè)試變得更加困難。通過(guò)public setter,您可以簡(jiǎn)單地將各個(gè)值分配給任何領(lǐng)域?qū)ο蟮膶傩浴_@使您可以直接指定您需要的確切值,以便將對(duì)象置于特定狀態(tài)以進(jìn)行測(cè)試。如果你鎖定你的屬性和構(gòu)造函數(shù),那么這種方法是不可能的。但這也不是一件壞事,它使單元測(cè)試變得稍微困難一點(diǎn),但你所做的是確保你的測(cè)試是有效的。
另一方面,它也使得測(cè)試領(lǐng)域?qū)ο蟊旧淼倪壿嫹浅:?jiǎn)單。盡管你的應(yīng)用服務(wù)/命令處理程序的單元測(cè)試幾乎肯定會(huì)需要一定程度的模擬,但你應(yīng)該發(fā)現(xiàn)大部分領(lǐng)域?qū)ο鬁y(cè)試的構(gòu)建要簡(jiǎn)單得多,并且通常不需要依賴(lài)模擬。
6. 總結(jié)
本文介紹了三種非常簡(jiǎn)單的技術(shù),您可以使用Entity Framework和EF Core從貧血域模型轉(zhuǎn)換為更為豐富的領(lǐng)域模型。使用參數(shù)化的構(gòu)造函數(shù)可以確保我們的領(lǐng)域模型在實(shí)例化時(shí)有效。清除公共屬性setter確保我們的模型在其整個(gè)生命周期內(nèi)保持有效狀態(tài)。在領(lǐng)域模型上內(nèi)部執(zhí)行驗(yàn)證和引入更改狀態(tài)的方法使我們能夠集中業(yè)務(wù)邏輯并簡(jiǎn)化調(diào)用代碼。最后,我們考察了值對(duì)象的使用,并解釋了他們?nèi)绾芜M(jìn)一步推進(jìn)了這種簡(jiǎn)化和邏輯封裝。
總結(jié)
以上是生活随笔為你收集整理的EF Core中避免贫血模型的三种行之有效的方法(翻译)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 在ASP.NET Core中使用EPPl
- 下一篇: 我们为什么要搞长沙.NET技术社区(三)