.NET重构(类型码的设计、重构方法)
阅读目录:
- 1.开篇介绍
- 2.不影响对象中的逻辑行为(枚举、常量、Entity子类来替代类型码)
- 3.影响对象中的逻辑行为(抽象出类型码,使用多态解决)
- 4.无法直接抽象出类型码(使用策略模式解决)
1】开篇介绍
说到类型码,我们都会很有印象,在某个Entity内部多多少少会出现一两个类型码来表示当前Entity在某个抽象角度属于哪一种层面,比如在EmployeeEntity中,基本上会有一个表示性别的Age的属性,同时Age属性的最终保存是在某个age字段中的,它就是很典型的类型码元素;Age类型码属性用来表达了在用性别这一个抽象角度对实体进行分类时,那么实体会存在着两种被归纳的层面(男、女);
在这个Age类型码属性被使用到的任何一个逻辑的地方都会有可能因为它的值不同而进行不同的逻辑分支,就好比我们在EmployeeCollectionEntity对象中定义一个方法,用来返回指定类型的所有EmployeeEntity,我们简单假设在EmployeeeCollectionEntity的内部肯定有一块逻辑是用来根据当前方法的参数进行判断,然后调用不同的方法返回当前集合中的所有执行参数的EmployeeEntity;
上述只是一个简单的使用场景,但是足以能简单说明类型码的意义和使用场景,下面我们将针对上面提到的这一个简单的例子进行三种类型码的使用分析和如何重构设计;在类型码不被任何逻辑使用只是提供给外部一个简单的标识时,我们如何处理;在类型码会直接影响实体内部行为逻辑的情况下,我们如何处理;在类型码会影响实体内部逻辑的时候,但是我们又无法将其直接提取抽象出来时,我们如何处理;
我们带着这个三个简单的问题进行下面的具体分析;
2】不影响对象中的逻辑行为(枚举、常量、Entity子类来替代类型码)
在不影响对象内部逻辑的情况下,问题很好处理;既然不影响对象内部逻辑,那么它的表现形式起码对于实体内部逻辑来说无关紧要;这个时候我们对它的设计可以遵循一个原则就是OO,如果我们使用的是一个简单的数字来表示类型码的状态,那么我们就可以通过三个方式对它进行设计或者重构;
这里有一个小小问题的就是,如果我们正在进行一项局部DomainModel内部的重构时,我们的工作量会很大而且需要很好的单元测试来支撑;但是如果我们目前正在设计一个Entity问题就很简单;
下面我们用上面1】节提到的简单场景作为本节演示示例的领域模型;
EmployeeEntity 代码:
1 public class EmployeeEntity 2 { 3 private int age; 4 5 public int Age 6 { 7 get { return age; } 8 set { age = value; } 9 } 10 }
EmployeeCollectionEntity代码:
1 public class EmployeeCollectionEntity : List<EmployeeEntity> 2 { 3 public IEnumerable<EmployeeEntity> GetEntityByAge(int age) 4 { 5 return from item in this where item.Age == age select item; 6 } 7 }
测试代码,为了方便起见,我就没有特地创建UnitTests项目,而是简单的使用控制台程序模拟:
1 EmployeeCollectionEntity empCollection = new EmployeeCollectionEntity() 2 { 3 new EmployeeEntity() { Age = 1 }, 4 new EmployeeEntity() { Age = 2 }, 5 new EmployeeEntity() { Age = 2 } 6 }; 7 8 var resultList = empCollection.GetEntityByAge(2); 9 if (resultList.Count() == 2 && resultList.ToList()[0].Age == 2 && resultList.ToList()[1].Age==2) 10 Console.WriteLine("is ok"); 11 12 Console.ReadLine();
上述代码很简单,一个Employee用来表示员工实体,EmployeeCollectionEntity表示员工实体集,用来封装一组包含业务逻辑的Empoyee集合;目前在EmployeeCollectionEntity中有一个方法GetEntityByAge(int age),用来根据性别类型码来获取集合内部中满足条件的所有EmpoyeeEntity,在单元测试中的代码,我们使用1表示女性,2表示男性,单元测试通过测试代码正确的查询出两组男性EmployeeEntity实体;
下面我们将逐步使用三种方式对这种类型的业务场景进行重新设计也可以称为重构;
第一:使用枚举类型替换类型码数字;
EmployeeEntity代码:
1 public class EmployeeEntity 2 { 3 public enum EmployeeAge 4 { 5 Male, 6 Female 7 } 8 9 private EmployeeAge age; 10 11 public EmployeeAge Age 12 { 13 get { return age; } 14 set { age = value; } 15 } 16 }
EmployeeCollectionEntity代码:
1 public class EmployeeCollectionEntity : List<EmployeeEntity> 2 { 3 public IEnumerable<EmployeeEntity> GetEntityByAge(EmployeeEntity.EmployeeAge age) 4 { 5 return from item in this where item.Age == age select item; 6 } 7 }
测试代码:
1 EmployeeCollectionEntity empCollection = new EmployeeCollectionEntity() 2 { 3 new EmployeeEntity() { Age = EmployeeEntity.EmployeeAge.Female }, 4 new EmployeeEntity() { Age = EmployeeEntity.EmployeeAge.Male }, 5 new EmployeeEntity() { Age = EmployeeEntity.EmployeeAge.Male } 6 }; 7 8 var resultList = empCollection.GetEntityByAge(EmployeeEntity.EmployeeAge.Male); 9 if (resultList.Count() == 2 && resultList.ToList()[0].Age == EmployeeEntity.EmployeeAge.Male && 10 resultList.ToList()[1].Age == EmployeeEntity.EmployeeAge.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 age; 7 8 public int Age 9 { 10 get { return age; } 11 set { age = value; } 12 } 13 }
EmployeeCollectionEntity代码:
1 public IEnumerable<EmployeeEntity> GetEntityByAge(int age) 2 { 3 return from item in this where item.Age == age select item; 4 }
测试代码:
1 EmployeeCollectionEntity empCollection = new EmployeeCollectionEntity() 2 { 3 new EmployeeEntity() { Age = EmployeeEntity.Female}, 4 new EmployeeEntity() { Age = EmployeeEntity.Male }, 5 new EmployeeEntity() { Age = EmployeeEntity.Male} 6 }; 7 8 var resultList = empCollection.GetEntityByAge(EmployeeEntity.Male); 9 if (resultList.Count() == 2 && resultList.ToList()[0].Age == EmployeeEntity.Male && 10 resultList.ToList()[1].Age == EmployeeEntity.Male) 11 Console.WriteLine("is ok"); 12 13 Console.ReadLine();
使用常量来代替类型码就是在接口上只能使用数字来表示IEnumerable<EmployeeEntity> GetEntityByAge(int age),然后我们在调用的时候会直接使用常量类型empCollection.GetEntityByAge(EmployeeEntity.Male);
第三:使用Entity子类来替代类型码;
对于EmployeeEntity如果在Age角度上存在继承体系,那么我们就可以使用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类图基本上可以知道代码结构;