一:面向對象設計中最簡單的部分與最難的部分html
若是說事務腳本是 面向過程 的,那麼領域模型就是 面向對象 的。面向對象的一個很重要的點就是:「把事情交給最適合的類去作」,即:「你得在一個個領域類之間跳轉,才能找出他們如何交互」,Martin Flower 說這是面向對象中最難的部分,這具備誤導的成份。確切地說,咱們做爲程序員若是已經掌握了 OOD 和 OOP 中技術手段,那麼如何尋找類之間的關係,可能就成了最難的部分。但在實際的狀況中,即使咱們不是程序員,也總能描述一件事情(即尋求關係),因此,找 對象之間的關係 還真的並非程序員最關係的部分,從技術層面來說,尋找類之間的關係由於與具體的編碼技巧無關,因此它如今對於程序員的咱們來講,應該是最簡單的部分,技術手段纔是這裏面的最難部分。程序員
好,切入正題。算法
二:構築類之間的關係(最簡單部分)sql
先來完成最簡單的部分,即找關係。也就是說,按照所謂的關係,咱們來重構 事務腳本 中的代碼。上篇「你在用什麼思想編碼:事務腳本 OR 面向對象?」中一樣的需求,若是用領域模式來作的話,咱們大概能夠這樣設計:數據庫
(備註:Product 和 RecognitionStrategy 爲 * –> 1 的關係是由於 一種確認算法能夠被多個產品的實例對象使用)緩存
從下面的示例代碼咱們就能夠看到這點:性能優化
class RevenueRecognition
{
private double amount;
private DateTime recognizedOn;
public RevenueRecognition(double amount, DateTime recognizedOn)
{
this.amount = amount;
this.recognizedOn = recognizedOn;
}
public double GetAmount()
{
return this.amount;
}
public bool IsRecognizedBy(DateTime asOf)
{
return asOf.CompareTo(this.recognizedOn) > 0 || asOf.CompareTo(this.recognizedOn) == 0;
}
}架構class Contract
{
// 多 對 1 的關係,* -> 1。即:一個產品可有多個合同訂單
private Product product;
private long id;
// 合同金額
private double revenue;
private DateTime whenSigned;
// 1 對 多 的關係, 1 -> *
private List<RevenueRecognition> revenueRecognitions = new List<RevenueRecognition>();
public Contract(Product product, double revenue, DateTime whenSigned)
{
this.product = product;
this.revenue = revenue;
this.whenSigned = whenSigned;
}
public void AddRevenueRecognition(RevenueRecognition r)
{
revenueRecognitions.Add(r);
}
public double GetRevenue()
{
return this.revenue;
}
public DateTime GetWhenSigned()
{
return this.whenSigned;
}
// 獲得哪天前入帳了多少
public double RecognizedRevenue(DateTime asOf)
{
double re = 0.0;
foreach(var r in revenueRecognitions)
{
if(r.IsRecognizedBy(asOf))
{
re += r.GetAmount();
}
}
return re;
}
public void CalculateRecognitions()
{
product.CalculateRevenueRecognitions(this);
}
}appclass Product
{
private string name;
private RecognitionStrategy recognitionStrategy;
public Product(string name, RecognitionStrategy recognitionStrategy)
{
this.name = name;
this.recognitionStrategy = recognitionStrategy;
}
public void CalculateRevenueRecognitions(Contract contract)
{
recognitionStrategy.CalculateRevenueRecognitions(contract);
}
public static Product NewWordProcessor(string name)
{
return new Product(name, new CompleteRecognitionStrategy());
}
public static Product NewSpreadsheet(string name)
{
return new Product(name, new ThreeWayRecognitionStrategy(60, 90));
}
public static Product NewDatabase(string name)
{
return new Product(name, new ThreeWayRecognitionStrategy(30, 60));
}
}分佈式abstract class RecognitionStrategy
{
public abstract void CalculateRevenueRecognitions(Contract contract);
}class CompleteRecognitionStrategy : RecognitionStrategy
{
public override void CalculateRevenueRecognitions(Contract contract)
{
contract.AddRevenueRecognition(new RevenueRecognition(contract.GetRevenue(), contract.GetWhenSigned()));
}
}class ThreeWayRecognitionStrategy : RecognitionStrategy
{
private int firstRecognitionOffset;
private int secondRecognitionOffset;
public ThreeWayRecognitionStrategy(int firstRoff, int secondRoff)
{
this.firstRecognitionOffset = firstRoff;
this.secondRecognitionOffset = secondRoff;
}
public override void CalculateRevenueRecognitions(Contract contract)
{
contract.AddRevenueRecognition(
new RevenueRecognition(contract.GetRevenue() / 3, contract.GetWhenSigned()));
contract.AddRevenueRecognition(
new RevenueRecognition(contract.GetRevenue() / 3, contract.GetWhenSigned().AddDays(firstRecognitionOffset)));
contract.AddRevenueRecognition(
new RevenueRecognition(contract.GetRevenue() / 3, contract.GetWhenSigned().AddDays(secondRecognitionOffset)));
}
}
正像我說的,以上的代碼是最簡單部分,每一個 OOP 的初學者都能寫出這樣的代碼來。可是我心想,即使咱們能寫出這樣的代碼來,咱們恐怕都不會心虛的告訴本身:是的,我正在進行領域驅動開發吧。
那麼,真正難的部分是什麼?
2.1 領域模型 對於程序員來講真正困難或者困惑的部分
是領域模型自己怎麼和其它模塊(或者其它層)進行交互,這些交互或者說關係是:
1:領域模型 自身具有些什麼語言層面的特性;
2:領域模型 和 領域模型 之間的關係;
3:領域模型 和 Repository 的關係;
4:工做單元 和 領域模型 及 Repository 的關係;
5:領域模型 的緩存;
6:領域模型 和 會話之間的關係;
三:那些交互與關係
3.1 領域模型 自身具有些什麼語言層面的特性
先看代碼:
public class User2 : DomainObj
{
#region Field
#endregion#region Property
#endregion
#region 領域自身邏輯
#endregion
#region 領域服務
#endregion
}
對於一個領域模型來講,從語言層面來說,它具有 5 方面的特性:
1:有父類,放置公共的屬性之類的內容,同時,存在一個父類,也表示它不是一個 值對象(領域概念中的值對象);
2:有實例字段;
3:有實例屬性;
4:領域自身邏輯,非 static 方法,有 public 的和 非public;
5:領域服務,static 方法,可獨立出去放置到對應的 服務類 中;
如今,咱們具體展開一下。不過,爲了展開講,咱們必須提供一個稍稍完整的 User2 的例子,它在真正的項目是這個樣子的:
public class User2 : DomainObj
{
#region Field
private Organization2 organization;private List<YhbjTest> myTests;
private List<YhbjClass> myClasses;
#endregion
#region Property
public override IRepository RootRep
{
get { return RepRegistory.UserRepository; }
}public string UserName { get; private set; }
public string Password { get; private set; }
/* 演示了同時存在 Organization 和 OrganizationId 兩個屬性的狀況 */
public string OrganizationId { get; private set; }public Organization2 Organization
{
get
{
if (organization == null && !string.IsNullOrEmpty(OrganizationId))
{
organization = Organization2.FindById(OrganizationId);
}return organization;
}
}/* 演示了存在 列表 屬性的狀況 */
public List<YhbjClass> MyClasses
{
get
{
if (myClasses == null)
{
myClasses = YhbjClass.GetClassesByUserId(this);
}return myClasses;
}
}public List<YhbjTest> MyTests
{
get
{
/* 個人考試來自兩個地方,1:班級、項目上的考試;2:選人的考試;
* 故,有兩種設計方法
* 1:選人的考試沒有疑議;
* 2:班級、項目考試,能夠從本模型的 Classes -> Projects -> Tests 獲取;
* 3:也能夠直接從數據庫獲得獲取;
* 在這裏的實際實現,採用第 2 種作法。由於:
* 1:數據自己是緩存的,第一獲取的時候,貌似存在屢次查詢,可是一旦獲取就緩存了;
* 2:存在不少地方的數據一致性問題,採用方法 3 貌似快速了,但會帶來不可知 BUG ;
* 3:即使未來考試還有課程上的考試,能夠很方便的獲取,否則還須要重改 SQL;
*/
if (myTests == null)
{
myTests = new List<YhbjTest>();/* 加指定人的考試,這些考試沒有對應的 項目 和 班級*/
myTests.AddRange(YhbjTest.GetTestsByUserId(this.Id));/* 加班級的考試,有對應的 班級 */
foreach (var c in MyClasses)
{
myTests.AddRange(c.Tests);
foreach (var t in c.Tests)
{
t.SetOwnerClass(c);
}/* 加項目的考試,有對應的 班級 和 項目,代碼略 */
}
}/* 其它邏輯 */
foreach (var test in myTests)
{
if (test.TestHistory == null)
{
test.SetHistory(MyTestHistories
.FirstOrDefault(p => p.TestId == test.Id && p.UserId == this.Id));
}
}return myTests;
}
}
#endregion#region 領域自身邏輯
public void InitWithOrganization(Organization2 o)
{
/* 在這個方法中不用 MakeDirty,由於至關於初始化分爲兩步進行了
*/
this.organization = o;
}
/* 不須要對外開放的邏輯,使用 internal*/
internal virtual void UpdateOnline(string loginTime, string token, string loginIp, string loginPort)
{
/* 這樣作的好處是什麼呢?
* createnew 方法用戶不負責本身的持久化,而是由事務代碼進行負責
* 可是 createnew 方法會標識本身爲 new,即 makenew 方法調用
* 而後,因爲 ut 用的都是同一個 ut,因此在事務這裏 commit 了
* 就是 commit 了 根 和 非根
* 這裏也同時演示了多個領域對象共用一個 ut
*/
this.UnitOfWork = new UnitOfWork();
UserOnline2 userOnline2Old = UserOnline2.GetUserOnline(this.UserName);
if (userOnline2Old != null)
{
userOnline2Old.UnitOfWork = this.UnitOfWork;
userOnline2Old.Delete();
}UserOnline2 = UserOnline2.CreateNew(UserName, loginIp, loginPort);
UnitOfWork.RegisterNew(UserOnline2);
UnitOfWork.Commit();
}/* 對外開放的邏輯,使用 public */
public List<YhbjTest> GetMyTest(string testName, int type, int page, int size, out int totalCount)
{
IEnumerable<YhbjTest> expName = from p in MyTests orderby p.CreateTime descending select p;
IEnumerable<YhbjTest> expState = null;switch (type)
{
case 0:
/* 未考
* 須要排除掉 有效期 以外
*/
expState =
from p in expName
where
p.StartTime <= DateTime.Now &&
p.EndTime >= DateTime.Now &&
(this.MyTestHistories.Exists(q => q.TestId == p.Id) == false
|| (this.MyTestHistories.Exists(q => q.TestId == p.Id) == true && this.myTestHistories.Find(h => h.TestId == p.Id).TestState != 1)) &&
p.AuditState == AuditState.Audited
select p;
break;
default:
throw new ArgumentOutOfRangeException();
}var re = expState.ToList();
totalCount = re.Count;
return re.Skip((page - 1) * size).Take(size).ToList();
}public YhbjTest StartTest(string testId)
{
// 邏輯略
}#endregion
/// <summary>
/// 1:服務是無狀態的,因此是 static 的
/// 2:服務是公開的,因此是 public 的
/// 3:服務實際是能夠建立專門的服務類的,這裏爲了演示須要,就放在一塊兒了
/// </summary>
#region 領域服務/* 這兩個字段演示其實服務部分的代碼是隨意的 */
private static readonly CookieWrapper CookieWrapper;private static readonly HttpWrapper HttpWrapper;
static User2()
{CookieWrapper = new CookieWrapper();
HttpWrapper = new HttpWrapper();
}/* 內部的方法固然是私有的 */
private static List<PaperQuestionStrategy3> GetUserPaperByUserAndTest(User2 user2, YhbjTest test)
{
var x = RepRegistory.UserRepository.FindTestUserPaper(user2, test);
return x;
}/* 獲取領域對象的方法,所有屬於領域服務部分,再次強調是靜態的 */
public static User2 GetUserByName(string username)
{
var user = (RepRegistory.UserRepository).FindByName(username);
return user as User2;
}
/* 領域對象的獲取和產生,還有另外的作法,就是在對象工廠中生成,但這不屬於本文要闡述的範疇 */
public static User2 CreateCreater(
string creatorOrganizationId, string creatorOrganizationName, string id, string name)
{
var user = new User2 { Id = id, Name = name, UnitOfWork = new UnitOfWork() };
user.MakeNew();
return user;
}
#endregion
}
請仔細查看上面代碼,爲了本文接下來的闡述,上面的代碼幾乎都是有意義的,我已經很精簡了。好了,基於上面這個例子,咱們展開講:
1:父類
public abstract class DomainObj
{
public Key Key { get; set; }/// <summary>
/// 根倉儲
/// TIP: 由於是充血模式,因此每一個領域模型都有一個根倉儲
/// 用於提交自身的變更
/// </summary>
public abstract IRepository RootRep { get; }protected DomainObj()
{
}public UnitOfWork UnitOfWork { get; set; }
public string Id { get; protected set; }
public string Name { get; protected set; }
protected void MakeNew()
{
UnitOfWork.RegisterNew(this);
}protected void MakeDirty()
{
UnitOfWork.RegisterDirty(this);
}protected void MakeRemoved()
{
UnitOfWork.RegisterRemoved(this);
}}
父類包含了,讓一個 領域模型 成爲 領域模型 所必備的那些特色,它有 標識映射(架構模式對象與關係結構模式之:標識域(Identity Field)),它持有 工做單元(),它負責調用 工做單元的API(換個角度說工做單元(Unit Of Work):建立、持有與API調用)。
若是咱們的對象是一個 領域模型對象,那麼它一定須要繼承之這個父類;
2:有實例字段
有人可能會有疑問,不是有屬性就能夠了嗎,爲何要有字段,一個理由是,若是咱們須要 延遲加載(),就須要使用字段來進行輔助。咱們在上面的源碼中看到的 if XX == NULL ,這樣的屬性代碼,就是延遲加載,其中使用到了字段。注意,若是使用了延遲加載,你應該會遇到序列化的問題,這是你須要注意的《延遲加載與序列化》。
3:有實例屬性
屬性是必然的,沒有屬性的領域模型很稀少的。有幾個地方須要你們注意,
1:屬性的 get 方法,能夠是很複雜的,其地位至關因而領域自身邏輯;
2:set 方法,都是 private 的,領域對象自身負責自身屬性的賦值;
3:在有必要的狀況下,使用 延遲加載,這可能須要另一個主題來說;
4:延遲加載的那些屬性,不少時候就是 導航屬性,即 Organization 和 MyClasses 這樣的屬性,就是導航屬性;
4:領域自身邏輯
領域自身邏輯,包含了應用系統大多數的業務邏輯,能夠理解爲:它就是傳統 3 層架構中的業務邏輯層的代碼。若是一段代碼,你不知道把它放到哪裏,那麼,它多半就屬於應該放在這裏。注意,只有應該公開的那些方法,才 public;
5:領域服務
領域服務,能夠獨立出去,成爲領域服務類。那麼,什麼樣的代碼是領域服務代碼?第一種狀況:
生成領域對象實例的方法,都應該是領域服務類。如 查詢 或者 Create New。
在實際場景中,咱們可能使用對象工廠來生成它們,這裏爲了純粹的演示哪些是 領域自身邏輯,哪些是 領域服務,特地使用了領域類的 static 方法來生成領域對象。即:
領域對象,不能隨便被外界生成,要嚴格控制其生成。因此領域父類的構造器,咱們看到是 protected 的。
那麼,實際上,除了上面這種狀況外,任何代碼都應該是 領域自身邏輯的。我在上面還演示了這樣的一段代碼:
private static List<PaperQuestionStrategy3> GetUserPaperByUserAndTest(User2 user2, YhbjTest test)
{
var x = RepRegistory.UserRepository.FindTestUserPaper(user2, test);
return x;
}
這段代碼,實際上做爲領域服務部分,就是錯誤的,它應該被放置在 YhbjTest 這個領域類中。
3.2 領域模型 和 領域模型 之間的關係
也就是說那些導航屬性和領域模型有什麼關係。導航屬性必須都是延遲加載的嗎?固然不是。好比, User 所在的 Organization,咱們在在使用到用戶這個對象的時候,幾乎老是要使用到其組織信息,那麼,咱們在獲取用戶的時候,就應該當即獲取到組織對象,那麼,咱們的持久化代碼是這樣的:
public override DomainObj Find(Key key)
{
var user = base.Find(key) as User2;
if (user == null)
{
//
string sql = @"
DECLARE @ORGID VARCHAR(32)='';
SELECT @ORGID=OrganizationId FROM [EL_Organization].[USER] WHERE ID=@Id
SELECT * FROM [EL_Organization].[USER] WHERE ID=@Id
SELECT * FROM [EL_Organization].[ORGANIZATION] WHERE ID=@ORGID";
var pms = new SqlParameter[]
{
new SqlParameter("@Id", key.GetId())
};var ds = SqlHelper.ExecuteDataset(CommandType.Text, sql, pms);
user = DataTableHelper.ToList<User2>(ds.Tables[0]).FirstOrDefault();
var o = DataTableHelper.ToList<Organization2>(ds.Tables[1]).FirstOrDefault();
if (user == null)
{
return null;
}user = Load(user);
// 注意,除了 Load User 還須要 Load Organization
user.InitWithOrganization(o);
Load(user.Organization);return user;
}return user;
}
能夠看到,咱們在一次 sql 執行的時候,就獲得了 organization,而後,User2 類型中,有個屬於領域自身邏輯方法:
public void InitWithOrganization(Organization2 o)
{
/* 在這個方法中不用 MakeDirty,由於至關於初始化分爲兩步進行了
*/
this.organization = o;
}
在這裏要多說一下,若是不是初始化時候的改屬性,如修改了用戶的組織信息,就應該 MakeDirty。
注意,還有一個比較重要的領域自身邏輯,就是 SetOwned,以下:
public void SetOwnerClass(YhbjClass yhbjClass)
{
this.OwnerClass = yhbjClass;
/* should not makeDirty, but if class repalced or removed, should makedirty*/
}
好比,領域模型 考試,就可能會有這個方法,考試自己須要知道:我屬於哪一個班級。
3.3 領域模型 和 Repository 之間的關係
第一,若是咱們在使用 領域模型,咱們必須使用 Repository 模式嗎?答案是:固然不是,咱們可使用 活動記錄模式(什麼是活動記錄,當前咱們能夠暫時理解爲傳統3層架構中的DAL層)。若是咱們在使用 Repository ,那麼,領域模型和 Respository 之間是什麼關係呢?這裏,有兩點須要闡述:
第一點是,通常的作法,Repository 是被注入的,它可能被注入到系統的某個地方,示例代碼是被注入到了類型 RepRegistory中。
領域模型要不要使用 Repository,個人答案是:要。
爲何,由於咱們要讓領域邏輯本身決定合適調用 Repository。
第二點是,每一個領域模型都有一個 RootRep,用於自身以及把自身當成根的那些導航屬性對象的持久化操做;
3.4 工做單元 和 領域模型 及 Repository 的關係
這一點比較複雜,咱們單獨在 《換個角度說工做單元(Unit Of Work):建立、持有與API調用》 進行了闡述。固然,跟 Repository 同樣,使用 領域模型,必須使用 工做單元 嗎?答案也是否是。只是,在使用 工做單元 後,更易於咱們處理 領域模型 中的事務問題。
3.5 領域模型的緩存
緩存分爲兩類,第一類咱們能夠稱之爲 一級緩存,這對於客戶端程序員來講,不可見,它被放置在 AbstractRepository 中,每每在當前請求中有用:
public abstract class AbstractRepository : IRepository
{
/* LoadedDomains 在有些文獻中能夠做爲高速緩存,可是這個緩存可不是指的
* 業務上的那個緩存,而是 片斷 的緩存,指在當前實例的生命週期中的緩存。
* 業務上的緩存在咱們的系統中,由每一個領域模型的服務部分自身持有。
*/
protected Dictionary<Key, DomainObj> LoadedDomains =
new Dictionary<Key, DomainObj>();public virtual DomainObj Find(Key key)
{
if (LoadedDomains.ContainsKey(key))
{
return LoadedDomains[key] as DomainObj;
}
else
{
return null;
}//return null;
}public abstract void Insert(DomainObj t);
public abstract void Update(DomainObj t);
public abstract void Delete(DomainObj t);
public void CheckLoaedDomains()
{
foreach (var m in LoadedDomains)
{
Console.WriteLine(m.Value);
}
}
/// <summary>
/// 當緩存內容發生變更時進行重置
/// </summary>
/// <param name="keyField">緩存key的id</param>
/// <param name="type">緩存的對象類型</param>
public void ResetLoadedDomainByKey(string keyId,Type type)
{
var key=new Key(keyId,type);
if (LoadedDomains.ContainsKey(key))
{
LoadedDomains.Remove(key);
}
}protected T Load<T>(T t) where T : DomainObj
{
var key = new Key(t.Id, typeof (T));
/* 1:這一句很重要,由於咱們不會想要放到每一個子類裏去賦值
* 2:其次,若是子類沒有調用 Load ,則永遠沒有 Key,不過這說得過去
*/
t.Key = key;if (LoadedDomains.ContainsKey(key))
{
return LoadedDomains[key] as T;
}
else
{
LoadedDomains.Add(key, t);
return t;
}//return t;
}protected List<T> LoadAll<T>(List<T> ts) where T : DomainObj
{
for (int i = 0; i < ts.Count; i++)
{
ts[i] = Load(ts[i]);
}return ts;
}
}
業務系統中的緩存,須要咱們隨着業務系統自身的特色,本身來建立,好比,若是咱們針對 User2 這個領域模型創建緩存,就應該把這個緩存掛接到當前會話中。此處不表。
3.6 領域模型 與 會話之間的關係
這是一個有意思的話題,不管是理論上仍是實際中,在一次會話當中(若是咱們會話的參照中,能夠回味下 ASP.NET 中的 Session,它們所表達的概念是一致的),只要會話不失效,那麼 領域對象 的狀態,就應該是被保持的。這裏難的是,咱們怎麼來建立這個 Session。Session 回到語言層面,就是一個類,它可能會將領域對象保持在 內存中,或者文件中,或者數據庫中,或者在一個分佈式系統中(如 Memcached,《ASP.NET性能優化之分佈式Session》)。
最簡單的,咱們可使用 ASP.NET 的 Session 來保存咱們的會話,而後把領域對象存儲到這裏。
四:總結
以上描述了讓領域模型成爲領域模型的一些最基本的技術手段。解決了這些技術手段,咱們的開發才基本算是 DDD 的,纔是面向領域模型的。解決了這些技術問題,接下來,咱們才能毫無後顧之憂地去解決 Martin Flower 所說的最難的部分:「你得在一個個領域類之間跳轉,才能找出他們如何交互」。