閱讀目錄:
- 1.開篇介紹
- 2.不影響對象中的邏輯行為(枚舉、常量、Entity子類來替代類型碼)
- 3.影響對象中的邏輯行為(抽象出類型碼,使用多態解決)
- 4.無法直接抽象出類型碼(使用策略模式解決)
1】開篇介紹
說到類型碼,我們都會很有印象,在某個Entity內部多多少少會出現一兩個類型碼來表示當前Entity在某個抽象角度屬於哪一種層面,比如在EmployeeEntity中,基本上會有一個表示性別的Sex的屬性,同時Sex屬性的最終保存是在某個sex字段中的,它就是很典型的類型碼元素;Sex類型碼屬性用來表達了在用性別這一個抽象角度對實體進行分類時,那么實體會存在着兩種被歸納的層面(男、女);
在這個Sex類型碼屬性被使用到的任何一個邏輯的地方都會有可能因為它的值不同而進行不同的邏輯分支,就好比我們在EmployeeCollectionEntity對象中定義一個方法,用來返回指定類型的所有EmployeeEntity,我們簡單假設在EmployeeeCollectionEntity的內部肯定有一塊邏輯是用來根據當前方法的參數進行判斷,然后調用不同的方法返回當前集合中的所有執行參數的EmployeeEntity;
上述只是一個簡單的使用場景,但是足以能簡單說明類型碼的意義和使用場景,下面我們將針對上面提到的這一個簡單的例子進行三種類型碼的使用分析和如何重構設計;在類型碼不被任何邏輯使用只是提供給外部一個簡單的標識時,我們如何處理;在類型碼會直接影響實體內部行為邏輯的情況下,我們如何處理;在類型碼會影響實體內部邏輯的時候,但是我們又無法將其直接提取抽象出來時,我們如何處理;
我們帶着這個三個簡單的問題進行下面的具體分析;
2】不影響對象中的邏輯行為(枚舉、常量、Entity子類來替代類型碼)
在不影響對象內部邏輯的情況下,問題很好處理;既然不影響對象內部邏輯,那么它的表現形式起碼對於實體內部邏輯來說無關緊要;這個時候我們對它的設計可以遵循一個原則就是OO,如果我們使用的是一個簡單的數字來表示類型碼的狀態,那么我們就可以通過三個方式對它進行設計或者重構;
這里有一個小小問題的就是,如果我們正在進行一項局部DomainModel內部的重構時,我們的工作量會很大而且需要很好的單元測試來支撐;但是如果我們目前正在設計一個Entity問題就很簡單;
下面我們用上面1】節提到的簡單場景作為本節演示示例的領域模型;
EmployeeEntity 代碼:
1 public class EmployeeEntity 2 { 3 private int sex; 4 5 public int Sex 6 { 7 get { return sex; } 8 set { sex = value; } 9 } 10 }
EmployeeCollectionEntity代碼:
1 public class EmployeeCollectionEntity : List<EmployeeEntity> 2 { 3 public IEnumerable<EmployeeEntity> GetEntityBySex(int sex) 4 { 5 return from item in this where item.Sex== sex select item; 6 } 7 }
測試代碼,為了方便起見,我就沒有特地創建UnitTests項目,而是簡單的使用控制台程序模擬:
1 EmployeeCollectionEntity empCollection = new EmployeeCollectionEntity() 2 { 3 new EmployeeEntity() { Sex= 1 }, 4 new EmployeeEntity() { Sex = 2 }, 5 new EmployeeEntity() { Sex = 2 } 6 }; 7 8 var resultList = empCollection.GetEntityBySex(2); 9 if (resultList.Count() == 2 && resultList.ToList()[0].Sex== 2 && resultList.ToList()[1].Sex==2) 10 Console.WriteLine("is ok"); 11 12 Console.ReadLine();
上述代碼很簡單,一個Employee用來表示員工實體,EmployeeCollectionEntity表示員工實體集,用來封裝一組包含業務邏輯的Empoyee集合;目前在EmployeeCollectionEntity中有一個方法GetEntityBySex(int sex),用來根據性別類型碼來獲取集合內部中滿足條件的所有EmpoyeeEntity,在單元測試中的代碼,我們使用1表示女性,2表示男性,單元測試通過測試代碼正確的查詢出兩組男性EmployeeEntity實體;
下面我們將逐步使用三種方式對這種類型的業務場景進行重新設計也可以稱為重構;
第一:使用枚舉類型替換類型碼數字;
EmployeeEntity代碼:
1 public class EmployeeEntity 2 { 3 public enum EmployeeSex 4 { 5 Male, 6 Female 7 } 8 9 private EmployeeSex sex; 10 11 public EmployeeSex Sex 12 { 13 get { return sex; } 14 set { sex= value; } 15 } 16 }
EmployeeCollectionEntity代碼:
1 public class EmployeeCollectionEntity : List<EmployeeEntity> 2 { 3 public IEnumerable<EmployeeEntity> GetEntityBySex(EmployeeEntity.EmployeeSex sex) 4 { 5 return from item in this where item.Sex== sexselect item; 6 } 7 }
測試代碼:
1 EmployeeCollectionEntity empCollection = new EmployeeCollectionEntity() 2 { 3 new EmployeeEntity() { Sex= EmployeeEntity.EmployeeSex.Female }, 4 new EmployeeEntity() { Sex= EmployeeEntity.EmployeeSex.Male }, 5 new EmployeeEntity() { Sex= EmployeeEntity.EmployeeSex.Male } 6 }; 7 8 var resultList = empCollection.GetEntityBySex(EmployeeEntity.EmployeeSex.Male); 9 if (resultList.Count() == 2 && resultList.ToList()[0].Sex== EmployeeEntity.EmployeeSex.Male && 10 resultList.ToList()[1].Sex== EmployeeEntity.EmployeeSex.Male) 11 Console.WriteLine("is ok"); 12 13 Console.ReadLine();
通過使用枚舉我們能很好的使用OOD的好處,這樣代碼中不會到處充斥這亂七八糟的魔幻數字;
第二:使用常量來代替類型碼;
其實使用常量來代替類型碼時,比較常見的業務場景是在和遠程交互的時候,因為在我們將Entity翻譯成某種傳輸對象的時候需要將它的屬性使用字符串的形式表達;比如這里的EmployeeEntity,假設我們需要將某一個EmployeeEntity發送到某個消息隊列,然后消息隊列的后端程序需要將它直接插入到數據庫中,這個時候,我們的DomainModel在消息隊列的后端程序中是不存在的,也就是說並沒有和數據庫映射過,這里的屬性類型碼將是和數據庫等價的字符串;所以如果我們在選擇使用枚舉還是常量來替代類型碼是,選擇的標准就是類型碼是否需要持久化,也就是字符串化;
EmployeeEntity代碼:
1 public class EmployeeEntity 2 { 3 public const int Male = 2; 4 public const int Female = 2; 5 6 private int sex; 7 8 public int Sex 9 { 10 get { return sex; } 11 set { Sex= value; } 12 } 13 }
EmployeeCollectionEntity代碼:
1 public IEnumerable<EmployeeEntity> GetEntityBySex(int sex) 2 { 3 return from item in this where item.Sex== sexselect item; 4 }
測試代碼:
1 EmployeeCollectionEntity empCollection = new EmployeeCollectionEntity() 2 { 3 new EmployeeEntity() { Sex= EmployeeEntity.Female}, 4 new EmployeeEntity() { Sex= EmployeeEntity.Male }, 5 new EmployeeEntity() { Sex= EmployeeEntity.Male} 6 }; 7 8 var resultList = empCollection.GetEntityBySex(EmployeeEntity.Male); 9 if (resultList.Count() == 2 && resultList.ToList()[0].Sex== EmployeeEntity.Male && 10 resultList.ToList()[1].Sex == EmployeeEntity.Male) 11 Console.WriteLine("is ok"); 12 13 Console.ReadLine();
使用常量來代替類型碼就是在接口上只能使用數字來表示IEnumerable<EmployeeEntity> GetEntityBySex(int sex),然后我們在調用的時候會直接使用常量類型empCollection.GetEntityBySex(EmployeeEntity.Male);
第三:使用Entity子類來替代類型碼;
對於EmployeeEntity如果在Sex角度上存在繼承體系,那么我們就可以使用Entity子類的方式來解決;現假設,對於性別為男和女都分別從EmployeeEntity上繼承各自的體系,MaleEmployeeEntity為男,FemaleEmployeeEntity為女,當然真實場景中不會為了這一個小小的性別就獨立出一個繼承體系;
EmployeeEntity代碼:
1 public abstract class EmployeeEntity 2 { 3 public abstract bool IsFemale { get; } 4 public abstract bool IsMale { get; } 5 }
這個時候EmployeeEntity已經不在是一個真實的Employee了;
MaleEmployeeEntity代碼:
1 public class MaleEmployeeEntity : EmployeeEntity 2 { 3 public override bool IsFemale 4 { 5 get { return false; } 6 } 7 public override bool IsMale 8 { 9 get { return true; } 10 } 11 }
FemaleEmployeeEntity代碼:
1 public class FemaleEmployeeEntity : EmployeeEntity 2 { 3 public override bool IsFemale 4 { 5 get { return true; } 6 } 7 public override bool IsMale 8 { 9 get { return false; } 10 } 11 }
EmployeeCollectionEntity代碼:
1 public class EmployeeCollectionEntity : List<EmployeeEntity> 2 { 3 public IEnumerable<EmployeeEntity> FemaleEmployeeList 4 { 5 get 6 { 7 return from item in this where item.IsFemale select item; 8 } 9 } 10 11 public IEnumerable<EmployeeEntity> MaleEmployeeList 12 { 13 get 14 { 15 return from item in this where item.IsMale select item; 16 } 17 } 18 }
測試代碼:
1 EmployeeCollectionEntity empCollection = new EmployeeCollectionEntity() 2 { 3 new FemaleEmployeeEntity(), 4 new MaleEmployeeEntity() , 5 new MaleEmployeeEntity() 6 }; 7 8 var resultList = empCollection.MaleEmployeeList; 9 if (resultList.Count() == 2 && resultList.ToList()[0].IsMale && resultList.ToList()[1].IsMale) 10 Console.WriteLine("is ok"); 11 12 Console.ReadLine();
既然咱們不存在類型碼了,那么就不會存在根據參數來獲取數據的接口,所以我們稍微變換一下,將參數拆成具體的屬性用來直接返回數據集合;
3】影響對象中的邏輯行為(抽象出類型碼,使用多態解決)
上面2】節中講到的方式都是類型碼不影響程序具體業務邏輯的情況下的設計方式,但是一旦當類型碼直接影響到我們DomainModel中的具體業務邏輯的情況下我就需要將類型碼進行提取並抽象出繼承體系,然后將具體的邏輯跟類型碼繼承體系走,這也是面向對象中的面向職責設計,將行為盡可能的放入它調用最平凡的對象中去;
現在假設EmployeeEntity中有一組訂單OrderCollection,現在要根據EmployeeEntity的不同級別EmployeeLevel獲取(GetDistributionOrders)需要配送的OrderCollection,這里有一個業務規則就是不同的等級在每次獲取配送訂單的時候是有不同的條件限制的,具體的條件限制跟當前的EmployeeLevel有關系,那么這個時候我們就需要將跟level相關的邏輯封裝進EmployeeLevel中去;
圖1:
Order代碼:
1 public class Order 2 { 3 public DateTime SubmitDtime { get; set; } 4 }
OrderCollection代碼:
1 public class OrderCollection : List<Order> 2 { 3 4 }
EmployeeEntity代碼:
1 public class EmployeeEntity 2 { 3 public EmployeeLevel Level { get; set; } 4 5 public OrderCollection AllDistributeionOrders { get; set; } 6 7 public OrderCollection GetDistributionOrders() 8 { 9 return Level.GetDistributionOrders();//將邏輯推入到類型碼之后的調用方式; 10 } 11 }
EmployeeLevel代碼:
1 public abstract class EmployeeLevel 2 { 3 public EmployeeEntity employee; 4 public abstract OrderCollection GetDistributionOrders(); 5 } 6 7 public class Normal : EmployeeLevel 8 { 9 public override OrderCollection GetDistributionOrders() 10 { 11 if (employee.AllDistributeionOrders == null && employee.AllDistributeionOrders.Count == 0) return null; 12 var orders = from order in employee.AllDistributeionOrders 13 where order.SubmitDtime <= DateTime.Now.AddDays(-5)//Normal 推遲五天配送 14 select order; 15 16 if (orders.ToList().Count == 0) return null; 17 18 OrderCollection result = new OrderCollection(); 19 20 orders.ToList().ForEach(order => { result.Add(order); }); 21 22 return result; 23 24 } 25 } 26 27 public class Super : EmployeeLevel 28 { 29 public override OrderCollection GetDistributionOrders() 30 { 31 if (employee.AllDistributeionOrders == null && employee.AllDistributeionOrders.Count == 0) return null; 32 var orders = from order in employee.AllDistributeionOrders 33 where order.SubmitDtime <= DateTime.Now.AddDays(-1)//Super 推遲一天配送 34 select order; 35 36 if (orders.ToList().Count == 0) return null; 37 38 OrderCollection result = new OrderCollection(); 39 40 orders.ToList().ForEach(order => { result.Add(order); }); 41 42 return result; 43 } 44 }
測試代碼:
1 OrderCollection orderColl = new OrderCollection(); 2 orderColl.Add(new Order() { SubmitDtime = DateTime.Now.AddDays(-2) }); 3 orderColl.Add(new Order() { SubmitDtime = DateTime.Now.AddDays(-7) }); 4 EmployeeEntity employee = new EmployeeEntity() 5 { 6 AllDistributeionOrders = orderColl 7 }; 8 9 EmployeeLevel level = new Super() { employee = employee }; 10 employee.Level = level; 11 12 var result = employee.GetDistributionOrders(); 13 if (result.Count == 2) 14 Console.WriteLine("Is ok"); 15 16 Console.ReadLine();
我們定義了兩個EmployeeLevel,一個是Normal的,也就是普通的,他的配送限制條件是:配送必須推遲五天;二個Super,也就是超級的,他的配送只推遲一天;這樣的邏輯分支,如果我們沒有將類型碼抽象出來進行設計,那么我們將面臨着一個條件分支的判斷,當后面需要加入其他Level的時候我們就會慢慢的陷入到判斷分支的泥潭;
4】無法直接抽象出類型碼(使用策略模式解決)
在3】節中,我們能很好的將類型碼抽象出來,但是如果我們面臨着一個重構項目時,我們很難去直接修改大面積的代碼,只能平衡一下將類型碼設計成具有策略意義的方式,不同的類型碼對應着不同的策略方案;
我們還是拿3】節中的示例來說,現在假設我們在重構一個直接使用int作為類型碼的EmployeeEntity,那么我們不可能去直接修改EmployeeEntity內部的邏輯,而是要通過引入策略工廠將不同的類型碼映射到策略方法中;
圖2:
由於該節代碼比較簡單,所以就不提供示例代碼,根據上面的UML類圖基本上可以知道代碼結構;