.NET重構(型別碼的設計、重構方法)

王清培發表於2013-11-18

閱讀目錄:

  • 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類圖基本上可以知道程式碼結構;

 

相關文章