EF Core中避免貧血模型的三種行之有效的方法(翻譯)

圖文無關(guān)

Paul Hiles: 3 ways to avoid an anemic domain model in EF Core

1.引言

在使用ORM中(比如Entity Framework)貧血領(lǐng)域模型十分常見 。本篇文章將先探討貧血模型的問題站辉,再去探究在EF Core中使用Code First時如何使用簡單的方法來避免貧血模型感论。

2.什么是貧血模型

在對領(lǐng)域建模后,輸出一系列類中僅包含一些簡單屬性聲明而不包含業(yè)務(wù)邏輯的模型,就屬于貧血模型坪圾。當使用Entity Framework時垮衷,它們不僅僅是簡單的數(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)用者來完善驗證和其他業(yè)務(wù)邏輯苔咪。由于缺乏相應(yīng)的抽象,就會導(dǎo)致代碼重復(fù)柳骄、較差的數(shù)據(jù)完整性团赏,以及增加高層模塊的復(fù)雜性。
貧血模型是十分常見的耐薯。從我的經(jīng)驗來看舔清,EF中超過80%的領(lǐng)域模型都是貧血模型。這并不奇怪曲初。幾乎所有的文檔和其他博客文章都以最簡單的方式展示了EF体谒。他們專注于盡可能快地開始工作,而不是主張最佳實踐臼婆。

3.改造為更豐富的領(lǐng)域模型(充血模型)

下面我們將討論三種簡單的方式去豐富你的貧血模型抒痒。這幾種方法都非常簡單,僅需要最小的改動颁褂。

3.1.移除無參公共構(gòu)造函數(shù)

除非你指定一個構(gòu)造函數(shù)故响,否則你的類將有一個默認的無參數(shù)構(gòu)造函數(shù)。這意味著你可以用下面的方式實例化你的類:

var blogPost = new BlogPost();

在大多數(shù)情況下颁独,這是沒有意義的彩届。領(lǐng)域?qū)ο笸ǔV辽傩枰恍?shù)據(jù)才能使其有效。創(chuàng)建沒有任何數(shù)據(jù)(如標題或URL)的BlogPost實例是沒有意義的誓酒,因為其僅僅是一個實例化對象樟蠕,但對象卻不包含狀態(tài)和行為,不滿足數(shù)據(jù)有效性。有些人不同意坯墨,但是DDD社區(qū)普遍認為確保領(lǐng)域?qū)ο笫冀K有效是有意義的寂汇。為了解決這個問題,我們可以像處理其他OO類一樣對待我們的域類捣染,并引入一個參數(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ù)來滿足約束(構(gòu)造函數(shù))骄瓣。這一變化提供了兩個積極成果:

  1. 任何新實例化的BlogPost對象現(xiàn)在都保證有效。作用于BlogPost的任何代碼都無需檢查其有效性耍攘。領(lǐng)域?qū)ο笤趯嵗瘯r自動校驗自身的有效性榕栏。
  2. 任何調(diào)用代碼都知道實例化對象所需的內(nèi)容。使用無參數(shù)的構(gòu)造函數(shù)蕾各,很容易構(gòu)造對象扒磁,但卻不知道必須要構(gòu)建的數(shù)據(jù)才能保證數(shù)據(jù)有效性。

但不幸的是式曲,在進行此更改后妨托,您將發(fā)現(xiàn)在從數(shù)據(jù)庫中檢索實體時,您的EF代碼不再有效:

InvalidOperationException:在實體類型'BlogPost'上找不到無參數(shù)的構(gòu)造函數(shù)吝羞。為了創(chuàng)建'BlogPost'的實例兰伤,EF需要聲明一個無參數(shù)的構(gòu)造函數(shù)。

EF需要一個無參數(shù)的構(gòu)造函數(shù)來查詢該做什么钧排?幸運的是敦腔,盡管EF確實需要無參數(shù)構(gòu)造函數(shù),但它并不要求構(gòu)造函數(shù)必須為public恨溜,所以我們可以為EF增加一個無參private構(gòu)造函數(shù)符衔,同時強制調(diào)用代碼使用參數(shù)化構(gòu)造函數(shù)。擁有額外的構(gòu)造函數(shù)顯然并不理想糟袁,但這些妥協(xié)通撑凶澹可以時ORM與OO代碼更好地配合。

private BlogPost()
{
    // just for EF
}

public BlogPost(string title, string summary, string body)
{
    ...
}

3.2. 刪除公共屬性中的set方法

上面介紹的參數(shù)化構(gòu)造函數(shù)確保在實例化時對象處于有效狀態(tài)项戴。盡管如此形帮,這并沒有阻止您將屬性值更改為無效值。要解決這個問題肯尺,我們有兩個選擇:

  1. 將驗證邏輯添加到屬性設(shè)置器
  2. 防止直接修改屬性,改為使用與用戶操作相對應(yīng)的方法

向?qū)傩栽O(shè)置器添加驗證是完全可以接受的躯枢,但意味著我們不能再使用自動屬性并且必須引入一個后臺字段则吟。顯然這不是什么大問題:

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)實世界中發(fā)生的事情。用戶不是孤立地更新單個屬性锄蹂,而是傾向于執(zhí)行一組已知操作(由UI或API接口確定)氓仲。這些操作可能會導(dǎo)致一個或多個屬性被更新,但通常情況下更多。業(yè)務(wù)邏輯依賴于上下文的場景是非常普遍的敬扛,這將會導(dǎo)致對屬性進行賦值的set中的驗證邏輯變得復(fù)雜而難以理解晰洒。作為基本示例,請考慮以下博客文章發(fā)布流程:

public void Publish()
{
    if (Status == BlogPostStatus.Draft || Status == BlogPostStatus.Archived)
    {
        if (Status == BlogPostStatus.Draft)
        {
            DatePublished = DateTime.UtcNow;
        }

        Status = BlogPostStatus.Published;
    }
}

在這個例子中啥箭,我們有一個Publish()方法谍珊,它有一些簡單的邏輯和兩個可以更新的屬性。我們也可以將其作為一個屬性的setter來實現(xiàn)急侥,但它不太清晰砌滞,尤其是從另一個類中調(diào)用它時:

blogPost.Status = BlogPostStatus.Published;

VS

blogPost.Publish();

第一種方式的副作用是不能清晰的表達業(yè)務(wù)用例。

當然坏怪,你在大多數(shù)代碼庫中看到的是根本不在領(lǐng)域?qū)ο笾羞M行驗證贝润。相反,這種類型的邏輯可以在下一層找到铝宵。這可能導(dǎo)致:

  1. 更長的方法將領(lǐng)域特定的邏輯與編排打掘、持久性和其他關(guān)注點混合在一起。
  2. 不同動作之間重復(fù)的驗證邏輯鹏秋。
  3. 由于外部依賴性(需要使用Mock)而難以測試純領(lǐng)域邏輯尊蚁。

正如我們現(xiàn)在所期望的那樣,如果我們從每個屬性中徹底移除setter拼岳,EF將無法正常運行枝誊,但將訪問級別更改為private就可以很好地解決問題:

public class BlogPost
{
    public int Id { get; private set; }
    ...
}

這樣,所有屬性在類之外都是只讀的惜纸。為了允許更新我們的領(lǐng)域類叶撒,我們引入了相應(yīng)類型動作的方法,如上面所示的Publish方法耐版。

通過刪除無參數(shù)構(gòu)造函數(shù)和公共屬性設(shè)置器并添加動作類型的方法祠够,我們現(xiàn)在擁有了始終有效的領(lǐng)域?qū)ο螅伺c所討論的實體直接相關(guān)的所有業(yè)務(wù)邏輯粪牲,這是一個很大的改進古瓤。我們已經(jīng)使我們的代碼同時更加健壯和簡單。

雖然我們可以討論其他DDD概念腺阳,例如領(lǐng)域事件以及通過雙派遣模式(double-dispatch pattern)使用領(lǐng)域服務(wù)落君,但它們的優(yōu)勢,特別是簡單性方面的優(yōu)勢遠不是那么明顯亭引。
通常DDD概念中可以簡化代碼的是我們將在下面討論的值對象的使用绎速。

3.3.引入值對象

值對象是不可變的(實例化后不允許更改)沒有身份標識的對象。值對象通潮候荆可以用來代替領(lǐng)域?qū)ο笾械囊粋€或多個屬性纹冤。

值對象的經(jīng)典示例包括貨??幣洒宝,地址和坐標,但也可以使用值類型替換單個屬性萌京,而不是使用字符串或整型雁歌。例如,不是將電話號碼存儲為字符串知残,而是可以創(chuàng)建一個帶有內(nèi)置驗證的PhoneNumber值類型以及提取撥號代碼的方法等靠瞎。

下面的代碼顯示了一個實現(xiàn)為EF類使用的貨幣值對象:

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ù)有效橡庞,這兩條信息都是必需的较坛。因此,對它們進行建模是有道理的扒最。請注意丑勤,參數(shù)化的構(gòu)造函數(shù)和私有屬性設(shè)置器的使用方式與我們在建模領(lǐng)域?qū)ο髸r所使用的完全相同。實體框架也需要一個私有無參數(shù)構(gòu)造函數(shù)吧趣。

在(RDBMS)數(shù)據(jù)持久性的上下文中法竞,值類型不存在于單獨的數(shù)據(jù)庫表中。為了讓我們在實體框架中使用值對象强挫,需要一個小的改動岔霸。這取決于您使用的EF版本。

在EF6中俯渤,我們只需用[ComplexType]屬性修飾值對象:

[ComplexType]
public class Money
{
    ...
}

在EF Core中呆细,從版本2開始,我們可以使用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實體上使用Money值對象八匠,如下所示:

public class BlogPost
{
    ...
    public Money AdvertisingFee { get; private set; }
    ...
}

創(chuàng)建并運行遷移后絮爷,我們會發(fā)現(xiàn)我們的數(shù)據(jù)庫表現(xiàn)在包含兩個額外的列:

AdvertisingFee_Currency
AdvertisingFee_Amount 

使用值對象的好處與向富領(lǐng)域模型的轉(zhuǎn)變非常相似。豐富的領(lǐng)域模型不需要調(diào)用代碼來驗證領(lǐng)域模型梨树,并提供了一個定義良好的抽象來進行編程坑夯。一個值對象進行自我驗證,因此包含值對象屬性的領(lǐng)域模型本身不需要知道如何驗證值類型抡四。所有非常清晰和簡單柜蜈。

4. 溫馨提示

當您打算從貧血域模型轉(zhuǎn)移到更豐富的領(lǐng)域模型時,您將立即體會到將領(lǐng)域級的業(yè)務(wù)邏輯封裝在領(lǐng)域?qū)ο笾械暮锰幹秆病U堊⒁馐缏模M管如此,嘗試并不是件容易的事藻雪。在您的領(lǐng)域?qū)ο笊蟿?chuàng)建一個方法來執(zhí)行驗證秘噪,然后更新多個屬性無疑是件好事。但從領(lǐng)域?qū)ο蟀l(fā)送電子郵件或保存到數(shù)據(jù)庫并不是您可能想要做的事情阔涉。重要的是要意識到缆娃,擁有豐富的領(lǐng)域模型并不否定另一層的需求來安排這些更高層次的關(guān)注。這是應(yīng)用服務(wù)或命令處理程序的工作瑰排,具體取決于您的體系結(jié)構(gòu)贯要。

5.關(guān)于單元測試的說明

一個豐富的、自我驗證的領(lǐng)域模型的一個負面影響是它可以使測試變得更加困難椭住。通過public setter崇渗,您可以簡單地將各個值分配給任何領(lǐng)域?qū)ο蟮膶傩浴_@使您可以直接指定您需要的確切值京郑,以便將對象置于特定狀態(tài)以進行測試宅广。如果你鎖定你的屬性和構(gòu)造函數(shù),那么這種方法是不可能的些举。但這也不是一件壞事跟狱,它使單元測試變得稍微困難??一點,但你所做的是確保你的測試是有效的户魏。

另一方面驶臊,它也使得測試領(lǐng)域?qū)ο蟊旧淼倪壿嫹浅:唵巍1M管你的應(yīng)用服務(wù)/命令處理程序的單元測試幾乎肯定會需要一定程度的模擬叼丑,但你應(yīng)該發(fā)現(xiàn)大部分領(lǐng)域?qū)ο鬁y試的構(gòu)建要簡單得多关翎,并且通常不需要依賴模擬。

6. 總結(jié)

本文介紹了三種非常簡單的技術(shù)鸠信,您可以使用Entity Framework和EF Core從貧血域模型轉(zhuǎn)換為更為豐富的領(lǐng)域模型纵寝。使用參數(shù)化的構(gòu)造函數(shù)可以確保我們的領(lǐng)域模型在實例化時有效。清除公共屬性setter確保我們的模型在其整個生命周期內(nèi)保持有效狀態(tài)星立。在領(lǐng)域模型上內(nèi)部執(zhí)行驗證和引入更改狀態(tài)的方法使我們能夠集中業(yè)務(wù)邏輯并簡化調(diào)用代碼爽茴。最后,我們考察了值對象的使用贞铣,并解釋了他們?nèi)绾芜M一步推進了這種簡化和邏輯封裝闹啦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市辕坝,隨后出現(xiàn)的幾起案子窍奋,更是在濱河造成了極大的恐慌,老刑警劉巖酱畅,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件琳袄,死亡現(xiàn)場離奇詭異,居然都是意外死亡纺酸,警方通過查閱死者的電腦和手機窖逗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來餐蔬,“玉大人碎紊,你說我怎么就攤上這事佑附。” “怎么了仗考?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵音同,是天一觀的道長。 經(jīng)常有香客問我秃嗜,道長权均,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任锅锨,我火速辦了婚禮叽赊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘必搞。我一直安慰自己必指,他們只是感情好,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布恕洲。 她就那樣靜靜地躺著取劫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪研侣。 梳的紋絲不亂的頭發(fā)上谱邪,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音庶诡,去河邊找鬼惦银。 笑死,一個胖子當著我的面吹牛末誓,可吹牛的內(nèi)容都是我干的扯俱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼喇澡,長吁一口氣:“原來是場噩夢啊……” “哼迅栅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起晴玖,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤读存,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后呕屎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體让簿,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年秀睛,在試婚紗的時候發(fā)現(xiàn)自己被綠了尔当。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蹂安,死狀恐怖椭迎,靈堂內(nèi)的尸體忽然破棺而出锐帜,到底是詐尸還是另有隱情,我是刑警寧澤畜号,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布抹估,位于F島的核電站,受9級特大地震影響弄兜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瓷式,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一替饿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贸典,春花似錦视卢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至妒挎,卻和暖如春绳锅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酝掩。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工鳞芙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人期虾。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓原朝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親镶苞。 傳聞我的和親對象是個殘疾皇子喳坠,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353