EF Core中避免貧血模型的三種行之有效的方法(翻譯)
1.引言
在使用ORM中(比如Entity Framework)貧血領域模型十分常見 。本篇文章將先探討貧血模型的問題,再去探究在EF Core中使用Code First時如何使用簡單的方法來避免貧血模型。
2.什麼是貧血模型
在對領域建模後,輸出一系列類中僅包含一些簡單屬性宣告而不包含業務邏輯的模型,就屬於貧血模型。當使用Entity Framework時,它們不僅僅是簡單的資料持有者而且包含有一堆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; } ... }
由於其完全缺乏面向物件程式設計的原則,因此貧血模型通常被描述為反模式。他們需要呼叫者來完善驗證和其他業務邏輯。由於缺乏相應的抽象,就會導致程式碼重複、較差的資料完整性,以及增加高層模組的複雜性。
貧血模型是十分常見的。從我的經驗來看,EF中超過80%的領域模型都是貧血模型。這並不奇怪。幾乎所有的文件和其他部落格文章都以最簡單的方式展示了EF。他們專注於盡可能快地開始工作,而不是主張最佳實踐。
3.改造為更豐富的領域模型(充血模型)
下面我們將討論三種簡單的方式去豐富你的貧血模型。這幾種方法都非常簡單,僅需要最小的改動。
3.1移除無參公共建構函式
除非你指定一個建構函式,否則你的類將有一個預設的無引數建構函式。這意味著你可以用下面的方式例項化你的類:
var blogPost = new BlogPost();
在大多數情況下,這是沒有意義的。領域物件通常至少需要一些資料才能使其有效。建立沒有任何資料(如標題或URL)的BlogPost例項是沒有意義的,因為其僅僅是一個例項化物件,但物件卻不包含狀態和行為,不滿足資料有效性。有些人不同意,但是DDD社群普遍認為確保領域物件始終有效是有意義的。為了解決這個問題,我們可以像處理其他OO類一樣對待我們的域類,並引入一個引數化的建構函式:
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; }
現在在呼叫程式碼必須提供最少的資料來滿足約束(建構函式)。這一變化提供了兩個積極成果:
- 任何新例項化的BlogPost物件現在都保證有效。作用於BlogPost的任何程式碼都無需檢查其有效性。領域物件在例項化時自動校驗自身的有效性。
- 任何呼叫程式碼都知道例項化物件所需的內容。使用無引數的建構函式,很容易構造物件,但卻不知道必須要構建的資料才能保證資料有效性。
但不幸的是,在進行此更改後,您將發現在從資料庫中檢索實體時,您的EF程式碼不再有效:
InvalidOperationException:在實體型別'BlogPost'上找不到無引數的建構函式。為了建立'BlogPost'的例項,EF需要宣告一個無引數的建構函式。
EF需要一個無引數的建構函式來查詢該做什麼?幸運的是,儘管EF確實需要無引數建構函式,但它並不要求建構函式必須為public,所以我們可以為EF增加一個無參private建構函式,同時強制呼叫程式碼使用引數化建構函式。擁有額外的建構函式顯然並不理想,但這些妥協通常可以時ORM與OO程式碼更好地配合。
private BlogPost()
{
// just for EF
}
public BlogPost(string title, string summary, string body)
{
...
}
3.2. 刪除公共屬性中的set方法
上面介紹的引數化建構函式確保在例項化時物件處於有效狀態。儘管如此,這並沒有阻止您將屬性值更改為無效值。要解決這個問題,我們有兩個選擇:
- 將驗證邏輯新增到屬性設定器
- 防止直接修改屬性,改為使用與使用者操作相對應的方法
向屬性設定器新增驗證是完全可以接受的,但意味著我們不能再使用自動屬性並且必須引入一個後臺欄位。顯然這不是什麼大問題:
private string title;
public string Title
{
get { return title; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Title must contain a value");
}
title = value;
}
}
第二種方式更受歡迎的主要原因在於它更接近地模擬了現實世界中發生的事情。使用者不是孤立地更新單個屬性,而是傾向於執行一組已知操作(由UI或API介面確定)。這些操作可能會導致一個或多個屬性被更新,但通常情況下更多。業務邏輯依賴於上下文的場景是非常普遍的,這將會導致對屬性進行賦值的set中的驗證邏輯變得複雜而難以理解。作為基本示例,請考慮以下部落格文章釋出流程:
public void Publish()
{
if (Status == BlogPostStatus.Draft || Status == BlogPostStatus.Archived)
{
if (Status == BlogPostStatus.Draft)
{
DatePublished = DateTime.UtcNow;
}
Status = BlogPostStatus.Published;
}
}
在這個例子中,我們有一個Publish()方法,它有一些簡單的邏輯和兩個可以更新的屬性。我們也可以將其作為一個屬性的setter來實現,但它不太清晰,尤其是從另一個類中呼叫它時:
blogPost.Status = BlogPostStatus.Published;
VS
blogPost.Publish();
第一種方式的副作用是不能清晰的表達業務用例。
當然,你在大多數程式碼庫中看到的是根本不在領域物件中進行驗證。相反,這種型別的邏輯可以在下一層找到。這可能導致:
- 更長的方法將領域特定的邏輯與編排、永續性和其他關注點混合在一起。
- 不同動作之間重複的驗證邏輯。
- 由於外部依賴性(需要使用Mock)而難以測試純領域邏輯。
正如我們現在所期望的那樣,如果我們從每個屬性中徹底移除setter,EF將無法正常執行,但將訪問級別更改為private就可以很好地解決問題:
public class BlogPost
{
public int Id { get; private set; }
...
}
這樣,所有屬性在類之外都是隻讀的。為了允許更新我們的領域類,我們引入了相應型別動作的方法,如上面所示的Publish方法。
通過刪除無引數建構函式和公共屬性設定器並新增動作型別的方法,我們現在擁有了始終有效的領域物件,幷包含了與所討論的實體直接相關的所有業務邏輯,這是一個很大的改進。我們已經使我們的程式碼同時更加健壯和簡單。
雖然我們可以討論其他DDD概念,例如領域事件以及通過雙派遣模式(double-dispatch pattern)使用領域服務,但它們的優勢,特別是簡單性方面的優勢遠不是那麼明顯。
通常DDD概念中可以簡化程式碼的是我們將在下面討論的值物件的使用。
3.3.引入值物件
值物件是不可變的(例項化後不允許更改)沒有身份標識的物件。值物件通常可以用來代替領域物件中的一個或多個屬性。
值物件的經典示例包括貨幣,地址和座標,但也可以使用值型別替換單個屬性,而不是使用字串或整型。例如,不是將電話號碼儲存為字串,而是可以建立一個帶有內建驗證的PhoneNumber值型別以及提取撥號程式碼的方法等。
下面的程式碼顯示了一個實現為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;
}
}
貨幣和金額是內在聯絡的。為了使資料有效,這兩條資訊都是必需的。因此,對它們進行建模是有道理的。請注意,引數化的建構函式和私有屬性設定器的使用方式與我們在建模領域物件時所使用的完全相同。實體框架也需要一個私有無引數建構函式。
在(RDBMS)資料永續性的上下文中,值型別不存在於單獨的資料庫表中。為了讓我們在實體框架中使用值物件,需要一個小的改動。這取決於您使用的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; }
...
}
建立並執行遷移後,我們會發現我們的資料庫表現在包含兩個額外的列:
AdvertisingFee_Currency
AdvertisingFee_Amount
使用值物件的好處與向富領域模型的轉變非常相似。豐富的領域模型不需要呼叫程式碼來驗證領域模型,並提供了一個定義良好的抽象來進行程式設計。一個值物件進行自我驗證,因此包含值物件屬性的領域模型本身不需要知道如何驗證值型別。所有非常清晰和簡單。
4. 溫馨提示
當您打算從貧血域模型轉移到更豐富的領域模型時,您將立即體會到將領域級的業務邏輯封裝在領域物件中的好處。請注意,儘管如此,嘗試並不是件容易的事。在您的領域物件上建立一個方法來執行驗證,然後更新多個屬性無疑是件好事。但從領域物件傳送電子郵件或儲存到資料庫並不是您可能想要做的事情。重要的是要意識到,擁有豐富的領域模型並不否定另一層的需求來安排這些更高層次的關注。這是應用服務或命令處理程式的工作,具體取決於您的體系結構。
5.關於單元測試的說明
一個豐富的、自我驗證的領域模型的一個負面影響是它可以使測試變得更加困難。通過public setter,您可以簡單地將各個值分配給任何領域物件的屬性。這使您可以直接指定您需要的確切值,以便將物件置於特定狀態以進行測試。如果你鎖定你的屬性和建構函式,那麼這種方法是不可能的。但這也不是一件壞事,它使單元測試變得稍微困難一點,但你所做的是確保你的測試是有效的。
另一方面,它也使得測試領域物件本身的邏輯非常簡單。儘管你的應用服務/命令處理程式的單元測試幾乎肯定會需要一定程度的模擬,但你應該發現大部分領域物件測試的構建要簡單得多,並且通常不需要依賴模擬。
6. 總結
本文介紹了三種非常簡單的技術,您可以使用Entity Framework和EF Core從貧血域模型轉換為更為豐富的領域模型。使用引數化的建構函式可以確保我們的領域模型在例項化時有效。清除公共屬性setter確保我們的模型在其整個生命週期內保持有效狀態。在領域模型上內部執行驗證和引入更改狀態的方法使我們能夠集中業務邏輯並簡化呼叫程式碼。最後,我們考察了值物件的使用,並解釋了他們如何進一步推進了這種簡化和邏輯封裝。
相關推薦
EF Core中避免貧血模型的三種行之有效的方法(翻譯)
1.引言 在使用ORM中(比如Entity Framework)貧血領域模型十分常見 。本篇文章將先探討貧血模型的問題,再去探究在EF Core中使用Code First時如何使用簡單的方法來避免貧血模型。 2.什麼是貧血模型 在對領域建模後,輸出一系列類中僅包含一些簡單屬性宣告而不包含業務邏輯的模型,就
android開發中監聽器的三種實現方法(OnClickListener)
宣告:本寶寶的畢業設計是基於Android開發的******** 所以對Android開發有用的文章就先轉載過來 對9月份寫論文起一定幫助作用 標籤: Android開發中監聽器的實現有三種方法,對於初學者來說,能夠很好地理解這三種方法,將能更好地增進自己對a
Android實現日夜間模式的三種常用方法(一)
1、使用 setTheme 的方法讓 Activity 重新設定主題; 2、設定 Android Support Library 中的 UiMode 來支援日間/夜間模式的切換; 3、通過資源 id 對映,回撥自定義 ThemeChangeListener 介
Rails專案中避免濫用這三種特性
Ruby有很多令人喜愛的優質特性,但如果僅僅是為了用而用,那麼好的特性也會變成壞的毒瘤。下面筆者就為大家盤點最常見的三種濫用特性。 proc/block/lambda Rails 專案裡,一般除了使用Ruby的Enumerable會使用到block,除此之外,需要自己
servlet的介紹 & xml中配置 以及 & 三種實現方式(補充設定瀏覽器不快取的方法)
開始時間:2018年10月13日20:53:30 | 2018年10月14日16:10:56 結束時間:2018年10月13日21:53:30 | 2018年10月14日17:02:23 累計時間:2小時 備註:幾乎每一句話都很有收穫,複習的時候務必要仔細一點 Servlet
c++ STL中sort函式的三種使用方法
複習一下~ STL,C++中的標準模板庫, 使用起來方便並且效率較高; sort函式有三種用法: 一:對基本型別陣列從小到大排序 sort( 陣列名+n1,陣列名+n2); 將陣列中下標從n1到n2的元素進行從小到大排序,不包括n2,通過n1,n2 可以對整
Python學習【第20篇】:互斥鎖以及程序之間的三種通訊方式(IPC)以及生產者個消費者模型 python併發程式設計之多程序1-----------互斥鎖與程序間的通訊
python併發程式設計之多程序1-----------互斥鎖與程序間的通訊 一、互斥鎖 程序之間資料隔離,但是共享一套檔案系統,因而可以通過檔案來實現程序直接的通訊,
JS中三種主要方法(函式定義)類別理解 —(JS面向物件&原型)
JS中三種主要方法(函式定義)類別理解 —(JS面向物件&原型) 首先理解在JavaScript中: 函式是“第一等公民” 一切皆物件 javascript的方法可以分為三類: 類方法 物件方法 原型方法 程式碼示例:
OLE:物件的類沒有在註冊資料庫中註冊 問題的三種解決方法
我在網上下載了破解版的SAS9.3,用了一段時間之後,今天開啟就填出一個提示框: OLE:物件的類沒有在註冊資料庫中註冊 啟用該物件所需的應用程式不可用。是否用“轉換……”將其轉換為或啟用為另一型別
PIM Sparse-Mode 中 RP 的三種定義方法(static、AutoRP、BSR)
ip multicast中,我們最常用的就是PIM了,因為它獨立於路由協議的特性,當之無愧地成為了最重要的多播路由協議。 PIM 分為三種模式:Sparse Dense Sparse-Dense 在非DENSE模式下,PIM需要藉助RP來實現多播路由轉發,多
當 xml中存在名稱空間,三種處理辦法(dom4j)
{ Map map = new HashMap(); map.put( " design " , " http://www.eclipse.org/birt/2005/design " ); SAXReader saxReader = new SAX
spring boot-mybatis三種動態sql(5)
內部 轉換成 ava .get bat class ide div upd 腳本sql XML配置方式的動態SQL我就不講了,有興趣可以自己了解,下面是用<script>的方式把它照搬過來,用註解來實現。適用於xml配置轉換到註解配置 @Select("&l
Java實現二維數組轉置的三種輸出方法(IntelliJ IDEA 2017.2.6 x64)
color intellij 實現 ret ati create tel eat clas 1 import java.util.Arrays; 2 3 /** 4 * Created by Stefango at 9:54 on 2018/7/22
ASP.NET Core 中如何給中間件傳參數(轉載)
inject its mes str project dsc format blank sam Passing Parameters to Middleware in ASP.NET Core 2.0 Problem How do you pass paramet
樹的三種儲存結構(轉)
出處為: http://blog.csdn.net/smile_from_2015/article/details/63687696 6.2樹的定義 之前我們一直在談的是一對一的線性結構,可現實中,還有很多一對多的情況需要處理,所以我們需要研究這種一對多的資料結構----
Java程式設計:刪除 List 元素的三種正確方法(面試與開發必備)
刪除 List 中的元素會產生兩個問題: 刪除元素後 List 的元素數量會發生變化; 對 List 進行刪除操作可能會產生併發問題; 我們通過程式碼示例演示正確的刪除邏輯 package com.ips.list; import java.util.ArrayList; import jav
初夏小談:斐波那契三種實現方法(C語言版)(第三種相信你沒見過)
斐波那契數列(Fibonaccisequnce),又稱黃金分割數列。研究斐波那契數列有相當重要的價值,例在現代物理、準晶體結構、化學等領域都有直接的應用。因此研究斐波那契數列也是很有必要的。 今天初夏將為大家帶來計算斐波那契數列第n位的三種方法 第一種利用遞迴的方法計算,程式碼相當簡單,但其
vue.js 三種方式安裝(轉)
的版本號,則說明你安裝成功了。 npm包管理器,是整合在node中的,所以安裝了node也就有了npm,直接輸入 npm -v 命令,顯示npm的版本資訊。
Java的三種代理模式(轉載)
Java的三種代理模式 1.代理模式 代理(Proxy)是一種設計模式,提供了對目標物件另外的訪問方式;即通過代理物件訪問目標物件.這樣做的好處是:可以在目標物件實現的基礎上,增強額外的功能操作,即擴充套件目標物件的功能. 這裡使用到程式設計中的一個思想:不要隨意去修改別人已經寫好的程式碼或
Hive之——metastore三種配置方式(轉)
轉自:https://blog.csdn.net/l1028386804/article/details/51564235 Hive的meta資料支援以下三種儲存方式,其中兩種屬於本地儲存,一種為遠端儲存。遠端儲存比較適合生產環境。Hive官方wiki詳細介紹了這三種方式,連結