重构笔记Ⅱ
时间:2010-10-05 来源:Tivon.S
重构学习笔记11. 使用策略类
概念:本文中的“使用策略类” 是指用设计模式中的策略模式来替换原来的switch case和if else语句,这样可以解开耦合,同时也使维护性和系统的可扩展性大大增强。
正文:如下面代码所示,ClientCode 类会更加枚举State的值来调用ShippingInfo 的不同方法,但是这样就会产生很多的判断语句,如果代码量加大,类变得很大了的话,维护中改动也会变得很大,每次改动一个地方,都要对整个结构进行编译(假如是多个工程),所以我们想到了对它进行重构,剥开耦合。
namespace LosTechies.DaysOfRefactoring.SwitchToStrategy.Before { public class ClientCode { public decimal CalculateShipping() { ShippingInfo shippingInfo = new ShippingInfo(); return shippingInfo.CalculateShippingAmount(State.Alaska); } } public enum State { Alaska, NewYork, Florida } public class ShippingInfo { public decimal CalculateShippingAmount(State shipToState) { switch (shipToState) { case State.Alaska: return GetAlaskaShippingAmount(); case State.NewYork: return GetNewYorkShippingAmount(); case State.Florida: return GetFloridaShippingAmount(); default: return 0m; } } private decimal GetAlaskaShippingAmount() { return 15m; } private decimal GetNewYorkShippingAmount() { return 10m; } private decimal GetFloridaShippingAmount() { return 3m; } } }
重构后的代码如下所示,抽象出一个IShippingCalculation 接口,然后把ShippingInfo 类里面的GetAlaskaShippingAmount、GetNewYorkShippingAmount、 GetFloridaShippingAmount三个方法分别提炼成三个类,然后继承自IShippingCalculation 接口,这样在调用的时候就可以通过IEnumerable<IShippingCalculation> 来解除之前的switch case语句,这和IOC的做法颇为相似。
using System; using System.Collections.Generic; using System.Linq; namespace Reconstruction { public interface IShippingInfo { decimal CalculateShippingAmount(State state); } public class ClientCode { [Inject] public IShippingInfo ShippingInfo { get; set; } public decimal CalculateShipping() { return ShippingInfo.CalculateShippingAmount(State.Alaska); } } public enum State { Alaska, NewYork, Florida } public class ShippingInfo : IShippingInfo { private IDictionary<State, IShippingCalculation> ShippingCalculations { get; set; } public ShippingInfo(IEnumerable<IShippingCalculation> shippingCalculations) { ShippingCalculations = shippingCalculations.ToDictionary(calc => calc.State); } public decimal CalculateShippingAmount(State shipToState) { return ShippingCalculations[shipToState].Calculate(); } } public interface IShippingCalculation { State State { get; } decimal Calculate(); } public class AlaskShippingCalculation : IShippingCalculation { public State State { get { return State.Alaska; } } public decimal Calculate() { return 15m; } } public class NewYorkShippingCalculation : IShippingCalculation { public State State { get { return State.NewYork; } } public decimal Calculate() { return 10m; } } public class FloridaShippingCalculation : IShippingCalculation { public State State { get { return State.Florida; } } public decimal Calculate() { return 3m; } } }
总结:这种重构在设计模式当中把它单独取了一个名字——策略模式,这样做的好处就是可以隔开耦合,以注入的形式实现功能,这使增加功能变得更加容易和简便,同样也增强了整个系统的稳定性和健壮性。
重构学习笔记12. 分解依赖
概念:本文中的“分解依赖” 是指对部分不满足我们要求的类和方法进行依赖分解,通过装饰器来达到我们需要的功能。
正文:正如下面代码所示,如果你要在你的代码中加入单元测试但有一部分代码是你不想测试的,那么你应用使用这个的重构。下面的例子中我们应用静态类来完成某些工作,但问题是在单元测试时我们无法mock静态类,所以我们只能引入静态类的装饰接口来分解对静态类的依赖。从而我们使我们的调用类只需要依赖于装饰接口就能完成这个操作。
namespace Reconstruction { public class AnimalFeedingService { private bool FoodBowlEmpty { get; set; } public void Feed() { if (FoodBowlEmpty) Feeder.ReplenishFood(); // more code to feed the animal } } public static class Feeder { public static void ReplenishFood() { // fill up bowl } } }
重构后代码如下,我们添加一个接口和一个实现类,在实现类中调用静态类的方法,所以说具体做什么事情没有改变,改变的只是形式,但这样做的一个好处是增加了了代码的可测试性。
在应用了分解依赖模式后,我们就可以在单元测试的时候mock一个IFeederService对象并通过AnimalFeedingService的构造函数传递给它。这样就可以完成我们需要的功能。
namespace Reconstruction { public class AnimalFeedingService { public IFeederService FeederService { get; set; } public AnimalFeedingService(IFeederService feederService) { FeederService = feederService; } private bool FoodBowlEmpty { get; set; } public void Feed() { if (FoodBowlEmpty) FeederService.ReplenishFood(); // more code to feed the animal } } public interfce IFeederService { void ReplenishFood(); } public class FeederService : IFeederService { public void ReplenishFood() { Feeder.ReplenishFood(); } } public static class Feeder { public static void ReplenishFood() { // fill up bowl } } }
总结:这个重构在很多时候和设计模式中的一些思想类似,使用中间的装饰接口来分解两个类之间的依赖,对类进行装饰,然后使它满足我们所需要的功能。
重构学习笔记13. 提取方法对象
概念:本文中的“提取方法对象”是指当你发现一个方法中存在过多的局部变量时,你可以通过使用“提取方法对象”重构来引入一些方法,每个方法完成任务的一个步骤,这样可以使得程序变得更具有可读性。
正文:如下代码所示,Order 类中的Calculate方法要完成很多功能,在之前我们用“提取方法”来进行重构,现在我们采取“提取方法对象”来完成重构。
namespace Reconstruction { public class OrderLineItem { public decimal Price { get; private set; } } public class Order { private IList<OrderLineItem> OrderLineItems { get; set; } private IList<decimal> Discounts { get; set; } private decimal Tax { get; set; } public decimal Calculate() { decimal subTotal = 0m; // Total up line items foreach (OrderLineItem lineItem in OrderLineItems) { subTotal += lineItem.Price; } // Subtract Discounts foreach (decimal discount in Discounts) subTotal -= discount; // Calculate Tax decimal tax = subTotal * Tax; // Calculate GrandTotal decimal grandTotal = subTotal + tax; return grandTotal; } } }
正如下代码所示,我们引入了OrderCalculator类,该类实现了所有的计算方法,Order类将自身传递给 OrderCalculator类并调用Calculate方法完成计算过程。
namespace Reconstruction { public class OrderLineItem { public decimal Price { get; private set; } } public class Order { public IEnumerable<OrderLineItem> OrderLineItems { get; private set;} public IEnumerable<decimal> Discounts { get; private set;} public decimal Tax { get; private set;} public decimal Calculate() { return new OrderCalculator(this).Calculate(); } } public class OrderCalculator { private decimal SubTotal { get; set; } private IEnumerable<OrderLineItem> OrderLineItems { get; set; } private IEnumerable<decimal> Discounts { get; set; } private decimal Tax { get; set; } public OrderCalculator(Order order) { OrderLineItems = order.OrderLineItems; Discounts = order.Discounts; Tax = order.Tax; } public decimal Calculate() { CalculateSubTotal(); SubtractDiscounts(); CalculateTax(); return SubTotal; } private void CalculateSubTotal() { // Total up line items foreach (OrderLineItem lineItem in OrderLineItems) SubTotal += lineItem.Price; } private void SubtractDiscounts() { // Subtract Discounts foreach (decimal discount in Discounts) SubTotal -= discount; } private void CalculateTax() { // Calculate Tax SubTotal += SubTotal * Tax; } } }
总结:本文的重构方法在有的时候还是比较有用,但这样会造成字段的增加,同时也会带来一些维护的不便,它和“提取方法”最大的区别就是一个通过方法返回需要的数据,另一个则是通过字段来存储方法的结果值,所以在很大程度上我们都会选择“提取方法”。
重构学习笔记14. 分离职责
概念:本文中的“分离职责”是指当一个类有许多职责时,将部分职责分离到独立的类中,这样也符合面向对象的五大特征之一的单一职责原则,同时也可以使代码的结构更加清晰,维护性更高。
正文:如下代码所示,Video类有两个职责,一个是处理video rental,另一个是计算每个客户的总租金。我们可以将这两个职责分离出来,因为计算每个客户的总租金可以在Customer计算,这也比较符合常理。
namespace Reconstruction { public class Video { public void PayFee(decimal fee) { } public void RentVideo(Video video, Customer customer) { customer.Videos.Add(video); } public decimal CalculateBalance(Customer customer) { return customer.LateFees.Sum(); } } public class Customer { public IList<decimal> LateFees { get; set; } public IList<Video> Videos { get; set; } } }
重构后的代码如下,这样Video 的职责就变得很清晰,同时也使代码维护性更好。
namespace Reconstruction { public class Video { public void RentVideo(Video video, Customer customer) { customer.Videos.Add(video); } } public class Customer { public IList<decimal> LateFees { get; set; } public IList<Video> Videos { get; set; } public void PayFee(decimal fee) { } public decimal CalculateBalance(Customer customer) { return customer.LateFees.Sum(); } } }
总结:这个重构经常会用到,它和之前的“移动方法”有几分相似之处,让方法放在合适的类中,并且简化类的职责,同时这也是面向对象五大原则之一和设计模式中的重要思想。
重构学习笔记15. 移除重复内容
概念:本文中的“移除重复内容”是指把一些很多地方都用到的逻辑提炼出来,然后提供给调用者统一调用。
总结:这个重构很简单,绝大多数程序员都会使用这种重构方法,但有时由于习惯、时间、赶进度等原因而忽略它,所以会使得整个系统杂乱无章,到处都是Ctrl+C和Ctrl+V的痕迹。
重构学习笔记16. 封装条件
概念:本文中的“封装条件”是指条件关系比较复杂时,代码的可读性会比较差,所以这时我们应当根据条件表达式是否需要参数将条件表达式提取成可读性更好的属性或者方法,如果条件表达式不需要参数则可以提取成属性,如果条件表达式需要参数则可以提取成方法。
正文:如下代码所示,PerformCoolFunction里面的if条件判断比较复杂,看起来有点杂乱,所以就把它提出来。
namespace Reconstruction { public class RemoteControl { private string[] Functions { get; set; } private string Name { get; set; } private int CreatedYear { get; set; } public string PerformCoolFunction(string buttonPressed) { // Determine if we are controlling some extra function // that requires special conditions if (Functions.Length > 1 && Name == "RCA" && CreatedYear > DateTime.Now.Year - 2) return "doSomething"; else return "doNothing"; } } }
如下代码所示,我们把条件表达式封装成HasExtraFunctions属性,这样先前的条件判断就成了if (HasExtraFunctions) ,所以这样就在很大程度上提高了可读性。
namespace Reconstruction { public class RemoteControl { private string[] Functions { get; set; } private string Name { get; set; } private int CreatedYear { get; set; } public string PerformCoolFunction(string buttonPressed) { if (HasExtraFunctions) return "doSomething"; else return "doNothing"; } private bool HasExtraFunctions { get { return Functions.Length > 1 && Name == "RCA" && CreatedYear > DateTime.Now.Year - 2; } } } }
总结:这个重构在很大程度上能改善代码的可读性,尤其是在一个逻辑很复杂的应用中,把这些条件判断封装成一个有意义的名字,这样很复杂的逻辑也会立刻变得简单起来。
重构学习笔记17. 提取父类
概念:本文中的“提取父类”是指类中有一些字段或方法,你想把它们提取到父类中以便同一继承层次的其它类也可以访问他们,这个和之前的很多重构有异曲同工之处。
总结:这个重构是典型的继承用法,很多程序员都会选择这样做,但是要注意正确的使用,不要造成过度使用了继承,如果过度使用了,请考虑用接口、组合和聚合来实现。
重构学习笔记18. 使用条件判断代替异常
概念:本文中的“使用条件判断代替异常”是指把没有必要使用异常做判断的条件尽量改为条件判断。
总结: 这个重构在项目代码中也经常用到,因为对于一部分程序员,是很难把握什么时候用try catch ,什么地方该用try catch 。记得之前大家还专门讨论过这些,比如如何用好以及在大中型项目中应该把它放在哪一个组件中等。
重构学习笔记19. 提取工厂类
概念:本文中的“提取工厂类”是指如果要创建的对象很多,则代码会变的很复杂。一种很好的方法就是提取工厂类。
正文:一般来说我们需要在代码中设置一些对象,以便获得它们的状态,从而使用对象,所谓的设置通常来说就是创建对象的实例并调用对象的方法。有时如果要创建的对象很多,则代码会变的很复杂。这便是工厂模式发挥作用的情形。工厂模式的复杂应用是使用抽象工厂创建对象集,但我们在这里只是使用基本的工厂类创建对象的一个简单应用。
如下代码所示,New方法包含创建类的整个逻辑,如果现在要创建的类比较多而且逻辑比较复杂的话(如根据不同条件创建对象,什么时候创建对象),我们的New方法逻辑会变得很大,同时代码也变得很难维护。所以我们就会采用提取工厂类的方式进行提炼。
namespace Reconstruction { public class PoliceCarController { public PoliceCar New(int mileage, bool serviceRequired) { PoliceCar policeCar = new PoliceCar(); policeCar.ServiceRequired = serviceRequired; policeCar.Mileage = mileage; return policeCar; } } public class PoliceCar { public bool ServiceRequired { get; set; } public int Mileage { get; set; } } }
那么重构后的代码如下,New方法变得很简单了,指需要调用实现接IPoliceCarFactory 接口的PoliceCarFactory 类就可以返回对象,这样就隔开了创建对象的逻辑,如果需求现在变为根据不同的条件创建不同的对象,什么时候创建对象等都变成了比较简单的事情,在后期可以把对象都配置在XML里面,使用反射的方式实现IOC注入创建。
namespace Reconstruction { public interface IPoliceCarFactory { PoliceCar Create(int mileage, bool serviceRequired); } public class PoliceCarFactory : IPoliceCarFactory { public PoliceCar Create(int mileage, bool serviceRequired) { PoliceCar policeCar = new PoliceCar(); policeCar.ServiceRequired = serviceRequired; policeCar.Mileage = mileage; return policeCar; } } public class PoliceCarController { public IPoliceCarFactory PoliceCarFactory { get; set; } public PoliceCarController(IPoliceCarFactory policeCarFactory) { PoliceCarFactory = policeCarFactory; } public PoliceCar New(int mileage, bool serviceRequired) { return PoliceCarFactory.Create(mileage, serviceRequired); } } public class PoliceCar { public bool ServiceRequired { get; set; } public int Mileage { get; set; } } }
总结:这个重构经常会在项目中使用,如果要创建的对象是一个,你可以采用简单工厂,但是这种方式还是会存在很多依赖,维护起来也比较不方便。所以推荐使用工厂方法模式,把实例化延迟到子类。如果你要创建一系列的对象,那么就推荐你使用抽象工厂模式,但是要注意不要过度设计,只要能满足不断变化的需求和给以后的维护和重构带来方便即可。
重构学习笔记20. 提取子类
概念:本文中的”提取子类”是指把基类中的一些不是所有子类都需要访问的方法调整到子类中。
正文:当你的基类中存在一些方法不是所有的子类都需要访问,你想将它们调整到子类中时,这个重构会变得很有用了。如下代码所示,我们需要一个 Registration类用来处理学生选课的信息。但是当Registration类开始工作后,我们意识到我们会在两种不同的上下文中使用 Registration类,NonRegistrationAction和Notes只有在我们处理未注册情况下才用到。
总结:这个重构方法经常用来规范类的职责,和之前的一些重构方法也有些类似。
参考资料:圣殿骑士