简介
重构是持续改进代码的基础。抵制重构将带来技术麻烦:忘记代码片断的功能、建立没法测试的代码等等。算法
而有了重构,使用单元测试、共享代码以及更可靠的无bug 的代码这些最佳实践就显得简单多了。编程
鉴于重构的重要性,我决定在整个8 月份天天介绍一个重构。在开始以前,请容许我事先声明,尽管我试着对每一个重构进行额外的描述和讨论,但我并非在声明它们的全部权。网络
我介绍的大多数重构均可以在Refactoring.com 中找到,有一些来自《代码大全(第2 版)》,剩下的则是我本身常用或从其余网站找到的。我以为注明每一个重构的出处并非重要的,由于你能够在网上不一样的帖子或文章中找到名称相似的重构。框架
本着这一精神,我将在明天发布第一篇帖子并开始长达31天的重构马拉松之旅。但愿大家可以享受重构并从中获益。ide
代码重构第1天:封装集合
在某些场景中,向类的使用者隐藏类中的完整集合是一个很好的作法,好比对集合的add/remove操做中包含其余的相关逻辑时。所以,以可迭代但不直接在集合上进行操做的方式来暴露集合,是个不错的主意。咱们来看代码:函数
- public class Order
- {
- private int _orderTotal;
- private List<OrderLine> _orderLines;
-
- public IEnumerable<OrderLine> OrderLines
- {
- get { return _orderLines; }
- }
-
- public void AddOrderLine(OrderLine orderLine)
- {
- _orderTotal += orderLine.Total;
- _orderLines.Add(orderLine);
- }
-
- public void RemoveOrderLine(OrderLine orderLine)
- {
- orderLine = _orderLines.Find(o => o == orderLine);
- if (orderLine == null) return;
- _orderTotal -= orderLine.Total;
- _orderLines.Remove(orderLine);
- }
- }
如你所见,咱们对集合进行了封装,没有将Add/Remove 方法暴露给类的使用者。在.NET Framework中,有些类如ReadOnlyCollection,会因为封装集合而产生不一样的行为,但它们各自都有防止误解的说明。这是一个很是简单但却极具价值的重构,能够确保用户不会误用你暴露的集合,避免代码中的一些bug。工具
代码重构第2天:移动方法
今天的重构一样很是简单,以致于人们并不认为这是一个有价值的重构。迁移方法(Move Method),顾名思义就是将方法迁移到合适的位置。在开始重构前,咱们先看看一下代码:post
- public class BankAccount
- {
- public BankAccount(int accountAge, int creditScore, AccountInterest accountInterest)
- {
- AccountAge = accountAge;
- CreditScore = creditScore;
- AccountInterest = accountInterest;
- }
-
- public int AccountAge { get; private set; }
- public int CreditScore { get; private set; }
- public AccountInterest AccountInterest { get; private set; }
-
- public double CalculateInterestRate()
- {
- if (CreditScore > 800)
- return 0.02;
-
- if (AccountAge > 10)
- return 0.03;
-
- return 0.05;
- }
- }
- public class AccountInterest
- {
- public BankAccount Account { get; private set; }
-
- public AccountInterest(BankAccount account)
- {
- Account = account;
- }
-
- public double InterestRate
- {
- get { return Account.CalculateInterestRate(); }
- }
-
- public bool IntroductoryRate
- {
- get { return Account.CalculateInterestRate() < 0.05; }
- }
- }
这里值得注意的是BankAccount.CalculateInterest 方法。当一个方法被其余类使用比在它所在类中的使用还要频繁时,咱们就须要使用迁移方法重构了——将方法迁移到更频繁地使用它的类中。因为依赖关系,该重构并不能应用于全部实例,但人们仍是常常低估它的价值。单元测试
最终的代码应该是这样的:学习
- public class BankAccount
- {
- public BankAccount(int accountAge, int creditScore, AccountInterest accountInterest)
- {
- AccountAge = accountAge;
- CreditScore = creditScore;
- AccountInterest = accountInterest;
- }
-
- public int AccountAge { get; private set; }
- public int CreditScore { get; private set; }
- public AccountInterest AccountInterest { get; private set; }
- }
- public class AccountInterest
- {
- public BankAccount Account { get; private set; }
- public AccountInterest(BankAccount account)
- {
- Account = account;
- }
-
- public double InterestRate
- {
- get { return CalculateInterestRate(); }
- }
-
- public bool IntroductoryRate
- {
- get { return CalculateInterestRate() < 0.05; }
- }
-
- public double CalculateInterestRate()
- {
- if (Account.CreditScore > 800)
- return 0.02;
-
- if (Account.AccountAge > 10)
- return 0.03;
-
- return 0.05;
- }
- }
够简单吧?
代码重构第3天:提高方法
提高方法(Pull Up Method)重构是将方法向继承链上层迁移的过程。用于一个方法被多个实现者使用时。
- public abstract class Vehicle
- {
- // other methods
- }
-
- public class Car : Vehicle
- {
- public void Turn(Direction direction)
- {
- // code here
- }
- }
-
- public class Motorcycle : Vehicle
- {
- }
-
- public enum Direction
- {
- Left,
- Right
- }
如你所见,目前只有Car类中包含Turn方法,但咱们也但愿在Motorcycle 类中使用。所以,若是没有基类,咱们就建立一个基类并将该方法“上移”到基类中,这样两个类就均可以使用Turn 方法了。这样作惟一的缺点是扩充了基类的接口、增长了其复杂性,所以需谨慎使用。只有当一个以上的子类须要使用该方法时才须要进行迁移。若是滥用继承,系统将会很快崩溃。这时你应该使用组合代替继承。重构以后的代码以下:
- public abstract class Vehicle
- {
- public void Turn(Direction direction)
- {
- // code here
- }
- }
-
- public class Car : Vehicle
- {
- }
-
- public class Motorcycle : Vehicle
- {
- }
-
- public enum Direction
- {
- Left,
- Right
- }
代码重构第4天:下降方法
昨天咱们介绍了将方法迁移到基类以供多个子类使用的上移方法重构,今天咱们来看看相反的操做。重构前的代码以下:
- public abstract class Animal
- {
- public void Bark()
- {
- // code to bark
- }
- }
-
- public class Dog : Animal
- {
- }
-
- public class Cat : Animal
- {
- }
这里的基类有一个Bark方法。或许咱们的猫咪们一时半会也无法学会汪汪叫(bark),所以Cat 类中再也不须要这个功能了。尽管基类不须要这个方法,但在显式处理Dog 类时也许还须要,所以咱们将Bark 方法“下降”到Dog 类中。这时,有必要评估Animal基类中是否还有其余行为。若是没有,则是一个将Animal抽象类转换成接口的好时机。由于契约中不须要任何代码,能够认为是一个标记接口。
- public abstract class Animal
- {
- }
-
- public class Dog : Animal
- {
- public void Bark()
- {
- // code to bark
- }
- }
-
- public class Cat : Animal
- {
- }
代码重构第5天:提高字段
今天咱们来看看一个和提高方法十分相似的重构。不过今天咱们处理的不是方法,而是字段。
- public abstract class Account
- {
- }
-
- public class CheckingAccount : Account
- {
- private decimal _minimumCheckingBalance = 5m;
- }
-
- public class SavingsAccount : Account
- {
- private decimal _minimumSavingsBalance = 5m;
- }
在这个例子中,两个子类中包含重复的常量。为了提升复用性咱们将字段上移到基类中,并简化其名称。
- public abstract class Account
- {
- protected decimal _minimumBalance = 5m;
- }
-
- public class CheckingAccount : Account
- {
- }
-
- public class SavingsAccount : Account
- {
- }
重构代码第6天:下降字段
与提高字段相反的重构是下降字段。一样,这也是一个无需多言的简单重构。
- public abstract class Task
- {
- protected string _resolution;
- }
-
- public class BugTask : Task
- {
- }
-
- public class FeatureTask : Task
- {
- }
在这个例子中,基类中的一个字符串字段只被一个子类使用,所以能够进行下移。只要没有其余子类使用基类的字段时,就应该当即执行该重构。保留的时间越长,就越有可能不去重构而保持原样。
- public abstract class Task
- {
- }
-
- public class BugTask : Task
- {
- private string _resolution;
- }
-
- public class FeatureTask : Task
- {
- }
重构代码第7天:重命名(方法,类,参数)
这是我最经常使用也是最有用的重构之一。咱们对方法/类/参数的命名每每不那么合适,以致于误导阅读者对于方法/类/参数功能的理解。这会形成阅读者的主观臆断,甚至引入bug。这个重构看起来简单,但却十分重要。
- public class Person
- {
- public string FN { get; set; }
- public decimal ClcHrlyPR()
- {
- // code to calculate hourly payrate
- return 0m;
- }
- }
如你所见,咱们的类/方法/参数的名称十分晦涩难懂,能够理解为不一样的含义。应用这个重构你只需随手将
名称修改得更具描述性、更容易传达其含义便可。简单吧。
- // Changed the class name to Employee
- public class Employee
- {
- public string FirstName { get; set; }
-
- public decimal CalculateHourlyPay()
- {
- // code to calculate hourly payrate
- return 0m;
- }
- }
重构代码第8天:使用委派代替继承
继承的误用十分广泛。它只能用于逻辑环境,但却常常用于简化,这致使复杂的没有意义的继承层次。看下面的代码:
- public class Sanitation
- {
- public string WashHands()
- {
- return "Cleaned!";
- }
- }
-
- public class Child : Sanitation
- {
- }
在该例中,Child 并非Sanitation,所以这样的继承层次是毫无心义的。咱们能够这样重构:在Child 的构造函数里实现一个Sanitation实例,并将方法的调用委托给这个实例。若是你使用依赖注入,能够经过构造函数传递Sanitation实例,尽管在我看来还要向IoC容器注册模型是一种坏味道,但领会精神就能够了。继承只能用于严格的继承场景,并非用来快速编写代码的工具。
- public class Sanitation
- {
- public string WashHands()
- {
- return "Cleaned!";
- }
- }
-
- public class Child
- {
- private Sanitation Sanitation { get; set; }
-
- public Child()
- {
- Sanitation = new Sanitation();
- }
-
- public string WashHands()
- {
- return Sanitation.WashHands();
- }
- }
代码重构第9天:提取接口
今天咱们来介绍一个经常被忽视的重构:提取接口。若是你发现多于一个类使用另一个类的某些方法,引入接口解除这种依赖每每十分有用。该重构实现起来很是简单,而且可以享受到松耦合带来的好处。
- public class ClassRegistration
- {
- public void Create()
- {
- // create registration code
- }
-
- public void Transfer()
- {
- // class transfer code
- }
-
- public decimal Total { get; private set; }
- }
-
- public class RegistrationProcessor
- {
- public decimal ProcessRegistration(ClassRegistration registration)
- {
- registration.Create();
- return registration.Total;
- }
- }
在下面的代码中,你能够看到我提取出了消费者所使用的两个方法,并将其置于一个接口中。如今消费者没必要关心和了解实现了这些方法的类。咱们解除了消费者与实际实现之间的耦合,使其只依赖于咱们建立的契约。
- public interface IClassRegistration
- {
- void Create();
- decimal Total { get; }
- }
-
- public class ClassRegistration : IClassRegistration
- {
- public void Create()
- {
- // create registration code
- }
-
- public void Transfer()
- {
- // class transfer code
- }
-
- public decimal Total { get; private set; }
- }
-
- public class RegistrationProcessor
- {
- public decimal ProcessRegistration(IClassRegistration registration)
- {
- registration.Create();
- return registration.Total;
- }
- }
代码重构第10天:提取方法
今天咱们要介绍的重构是提取方法。这个重构极其简单但却大有裨益。首先,将逻辑置于命名良好的方法内有助于提升代码的可读性。当方法的名称能够很好地描述这部分代码的功能时,能够有效地减小其余开发者的研究时间。假设越少,代码中的bug 也就越少。重构以前的代码以下:
- public class Receipt
- {
- private IList<decimal> Discounts { get; set; }
- private IList<decimal> ItemTotals { get; set; }
-
- public decimal CalculateGrandTotal()
- {
- decimal subTotal = 0m;
- foreach (decimal itemTotal in ItemTotals)
- subTotal += itemTotal;
- if (Discounts.Count > 0)
- {
- foreach (decimal discount in Discounts)
- subTotal -= discount;
- }
-
- decimal tax = subTotal * 0.065m;
- subTotal += tax;
- return subTotal;
- }
- }
你会发现CalculateGrandTotal方法一共作了3件不一样的事情:计算总额、折扣和发票税额。开发者为了搞清楚每一个功能如何处理而不得不将代码从头看到尾。相比于此,向下面的代码那样将每一个任务分解成单独的方法则要节省更多时间,也更具可读性:
- public class Receipt
- {
- private IList<decimal> Discounts { get; set; }
- private IList<decimal> ItemTotals { get; set; }
- public decimal CalculateGrandTotal()
- {
- decimal subTotal = CalculateSubTotal();
- subTotal = CalculateDiscounts(subTotal);
- subTotal = CalculateTax(subTotal);
- return subTotal;
- }
-
- private decimal CalculateTax(decimal subTotal)
- {
- decimal tax = subTotal * 0.065m;
- subTotal += tax;
- return subTotal;
- }
-
- private decimal CalculateDiscounts(decimal subTotal)
- {
- if (Discounts.Count > 0)
- {
- foreach (decimal discount in Discounts)
- subTotal -= discount;
- }
- return subTotal;
- }
-
- private decimal CalculateSubTotal()
- {
- decimal subTotal = 0m;
- foreach (decimal itemTotal in ItemTotals)
- subTotal += itemTotal;
- return subTotal;
- }
- }
代码重构第11天:使用策略类
今天的重构没有固定的形式,多年来我使用过不一样的版本,而且我敢打赌不一样的人也会有不一样的版本。该重构适用于这样的场景:switch 语句块很大,而且会随时引入新的判断条件。这时,最好使用策略模式将每一个条件封装到单独的类中。实现策略模式的方式是不少的。我在这里介绍的策略重构使用的是字典策略,这么作的好处是调用者没必要修改原来的代码。
- 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;
- }
- }
- }
要应用该重构,需将每一个测试条件至于单独的类中,这些类实现了一个共同的接口。而后将枚举做为字典的键,这样就能够获取正确的实现,并执行其代码了。之后若是但愿添加新的条件,只需添加新的实现类,并将其添加至ShippingCalculations 字典中。正如前面说过的,这不是实现策略模式的惟一方式。我在这里将字体加粗显示,是由于确定会有人在评论里指出这点:)用你以为好用的方法。我用这种方式实现重构的好处是,不用修改客户端代码。全部的修改都在ShippingInfo 类内部。
Jayme Davis指出这种重构因为仍然须要在构造函数中进行绑定,因此只不过是增长了一些类而已,但若是绑定IShippingCalculation的策略能够置于IoC中,带来的好处仍是不少的,它可使你更灵活地捆绑策略。
- namespace LosTechies.DaysOfRefactoring.SwitchToStrategy.After
- {
- public class ClientCode
- {
- public decimal CalculateShipping()
- {
- ShippingInfo shippingInfo = new ShippingInfo();
- return shippingInfo.CalculateShippingAmount(State.Alaska);
- }
- }
-
- public enum State
- {
- Alaska,
- NewYork,
- Florida
- }
-
- public class ShippingInfo
- {
- private IDictionary<State, IShippingCalculation> ShippingCalculations
- { get; set; }
-
- public ShippingInfo()
- {
- ShippingCalculations = new Dictionary<State, IShippingCalculation>
- {
- { State.Alaska, new AlaskShippingCalculation() },
- { State.NewYork, new NewYorkShippingCalculation() },
- { State.Florida, new FloridaShippingCalculation() }
- };
- }
-
- public decimal CalculateShippingAmount(State shipToState)
- {
- return ShippingCalculations[shipToState].Calculate();
- }
- }
-
- public interface IShippingCalculation
- {
- decimal Calculate();
- }
-
- public class AlaskShippingCalculation : IShippingCalculation
- {
- public decimal Calculate()
- {
- return 15m;
- }
- }
-
- public class NewYorkShippingCalculation : IShippingCalculation
- {
- public decimal Calculate()
- {
- return 10m;
- }
- }
-
- public class FloridaShippingCalculation : IShippingCalculation
- {
- public decimal Calculate()
- {
- return 3m;
- }
- }
- }
为了使这个示例圆满,咱们来看看在ShippingInfo 构造函数中使用Ninject 为IoC容器时如何进行绑定。须要更改的地方不少,主要是将state 的枚举放在策略内部,以及Ninject 向构造函数传递一个IShippingInfo 的IEnumerable泛型。接下来咱们使用策略类中的state属性建立字典,其他部分保持不变。(感谢Nate Kohari和Jayme Davis)
- 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天:分解依赖
有些单元测试须要恰当的测试“缝隙”(test seam)来模拟/隔离一些不想被测试的部分。若是你正想在代码中引入这种单元测试,那么今天介绍的重构就十分有用。在这个例子中,咱们的客户端代码使用一个静态类来实现功能。但当须要单元测试时,问题就来了。咱们没法在单元测试中模拟静态类。解决的方法是使用一个接口将静态类包装起来,造成一个缝隙来切断与静态类之间的依赖。
- 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
- }
- }
重构时咱们所要作的就是引入一个接口和简单调用上面那个静态类的类。所以行为仍是同样的,只是调用的方式产生了变化。这是一个不错的重构起始点,也是向代码添加单元测试的简单方式。
- 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 interface IFeederService
- {
- void ReplenishFood();
- }
-
- public class FeederService : IFeederService
- {
- public void ReplenishFood()
- {
- Feeder.ReplenishFood();
- }
- }
-
- public static class Feeder
- {
- public static void ReplenishFood()
- {
- // fill up bowl
- }
- }
如今,咱们能够在单元测试中将模拟的IFeederService 传入AnimalFeedingService 构造函数。测试成功后,咱们能够将静态方法中的代码移植到FeederService 类中,并删除静态类。
代码重构第13天:提取方法对象
今天的重构来自于Martin Fowler的重构目录。在我看来,这是一个比较罕见的重构,但有时却终能派上用场。当你尝试进行提取方法的重构时,须要引入大量的方法。在一个方法中使用众多的本地变量有时会使代码变得丑陋。所以最好使用提取方法对象这个重构,将执行任务的逻辑分开。
- 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;
- }
- }
咱们经过构造函数,将返回计算结果的类的引用传递给包含多个计算方法的新建对象,或者向方法对象的构造函数中单独传递各个参数。以下面的代码:
- 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天:分离职责
把一个类的多个职责进行拆分,这贯彻了SOLID 中的单一职责原则(SRP)。尽管对于如何划分“职责”常常存在争论,但应用这项重构仍是十分简单的。我这里并不会回答划分职责的问题,只是演示一个结构清晰的示例,将类划分为多个负责具体职责的类。
- 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类包含两个职责,一个负责处理录像租赁,另外一个负责管理管理用户的租赁总数。要分离职责,咱们能够将用户的逻辑转移到用户类中。
- 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天:移除重复内容
这大概是处理一个方法在多处使用时最多见的重构。若是不加以注意的话,你会慢慢地养成重复的习惯。开发者经常因为懒惰或者在想要尽快生成尽量多的代码时,向代码中添加不少重复的内容。我想也不必过多解释了吧,直接看代码把。
- public class MedicalRecord
- {
- public DateTime DateArchived { get; private set; }
- public bool Archived { get; private set; }
-
- public void ArchiveRecord()
- {
- Archived = true;
- DateArchived = DateTime.Now;
- }
-
- public void CloseRecord()
- {
- Archived = true;
- DateArchived = DateTime.Now;
- }
- }
咱们用共享方法的方式来删除重复的代码。看!没有重复了吧?请务必在必要的时候执行这项重构。它能有效地减小bug,由于你不会将有bug的代码复制/粘贴到各个角落。
- public class MedicalRecord
- {
- public DateTime DateArchived { get; private set; }
- public bool Archived { get; private set; }
-
- public void ArchiveRecord()
- {
- SwitchToArchived();
- }
-
- public void CloseRecord()
- {
- SwitchToArchived();
- }
-
- private void SwitchToArchived()
- {
- Archived = true;
- DateArchived = DateTime.Now;
- }
- }
代码重构第16天:封装条件
当代码中充斥着若干条件判断时,代码的真正意图会迷失于这些条件判断之中。这时我喜欢将条件判断提取到一个易于读取的属性或方法(若是有参数)中。重构以前的代码以下:
- 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";
- }
- }
重构以后,代码的可读性更强,意图更明显:
- public class RemoteControl
- {
- private string[] Functions { get; set; }
- private string Name { get; set; }
- private int CreatedYear { get; set; }
-
- private bool HasExtraFunctions
- {
- get
- {
- return Functions.Length > 1 && Name == "RCA" &&
- CreatedYear > DateTime.Now.Year - 2;
- }
- }
-
- public string PerformCoolFunction(string buttonPressed)
- {
- // Determine if we are controlling some extra function
- // that requires special conditions
- if (HasExtraFunctions)
- return "doSomething";
- }
- }
代码重构第17天:提取父类
今天的重构来自于Martin Fowler的重构目录,当一个类有不少方法但愿将它们“提拔”到基类以供同层次的其余类使用时,会常用该重构。下面的类包含两个方法,咱们但愿提取这两个方法并容许其余类使用。
- public class Dog
- {
- public void EatFood()
- {
- // eat some food
- }
-
- public void Groom()
- {
- // perform grooming
- }
- }
重构以后,咱们仅仅将须要的方法转移到了一个新的基类中。这很相似“Pull Up”重构,只是在重构以前,并不存在基类。
- public class Animal
- {
- public void EatFood()
- {
- // eat some food
- }
-
- public void Groom()
- {
- // perform grooming
- }
- }
-
- public class Dog : Animal
- {
- }
代码重构第18天:使用条件判断代替异常
今天的重构没有什么出处,是我平时常用而总结出来的。欢迎您发表任何改进意见或建议。我相信必定还有其余比较好的重构能够解决相似的问题。
我曾无数次面对的一个代码坏味道就是,使用异常来控制程序流程。您可能会看到相似的代码:
- public class Microwave
- {
- private IMicrowaveMotor Motor { get; set; }
-
- public bool Start(object food)
- {
- bool foodCooked = false;
- try
- {
- Motor.Cook(food);
- foodCooked = true;
- }
- catch (InUseException)
- {
- foodcooked = false;
- }
- return foodCooked;
- }
- }
异常应该仅仅完成本身的本职工做:处理异常行为。大多数状况你均可以将这些代码用恰当的条件判断替换,并进行恰当的处理。下面的代码能够称之为契约式设计,由于咱们在执行具体工做以前明确了Motor类的状态,而不是经过异常来进行处理。
- public class Microwave
- {
- private IMicrowaveMotor Motor { get; set; }
- public bool Start(object food)
- {
- if (Motor.IsInUse)
- return false;
- Motor.Cook(food);
- return true;
- }
- }
代码重构第19天:提取工厂类
今天的重构是由GangOfFour首先提出的,网络上有不少关于该模式不一样的用法。
在代码中,一般须要一些复杂的对象建立工做,以使这些对象达到一种可使用的状态。一般状况下,这种建立不过是新建对象实例,并以咱们须要的方式进行工做。可是,有时候这种建立对象的需求会极具增加,而且混淆了建立对象的原始代码。这时,工厂类就派上用场了。最复杂的工厂模式是使用抽象工厂建立对象族。而咱们只是使用最基本的方式,用一个工厂类建立
一个特殊类的实例。来看下面的代码:
- public class PoliceCarController
- {
- public PoliceCar New(int mileage, bool serviceRequired)
- {
- PoliceCar policeCar = new PoliceCar();
- policeCar.ServiceRequired = serviceRequired;
- policeCar.Mileage = mileage;
- return policeCar;
- }
- }
如您所见,New方法负责建立PoliceCar并根据一些外部输入初始化PoliceCar的某些属性。对于简单的建立工做来讲,这样作能够从容应对。可是长此以往,建立的工做量愈来愈大,而且被附加在controller 类上,但这并非controller类的职责。这时,咱们能够将建立代码提取到一个Factory类中去,由该类负责PoliceCar实例的建立。
- 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.ReadForService = 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);
- }
- }
因为将建立的逻辑转移到了工厂中,咱们能够添加一个类来专门负责实例的建立,而没必要担忧在建立或复制代码的过程当中有所遗漏。
代码重构第20天:提取子类
今天的重构来自于Martin Fowler的模式目录。你能够在他的目录中找到该重构。
当一个类中的某些方法并非面向全部的类时,可使用该重构将其迁移到子类中。我这里举的例子十分简单,它包含一个Registration类,该类处理与学生注册课程相关的全部信息。
- public class Registration
- {
- public NonRegistrationAction Action { get; set; }
- public decimal RegistrationTotal { get; set; }
- public string Notes { get; set; }
- public string Description { get; set; }
- public DateTime RegistrationDate { get; set; }
- }
当使用了该类以后,咱们就会意识到问题所在——它应用于两个彻底不一样的场景。属性NonRegistrationAction和Notes 只有在处理与普通注册略有不一样的NonRegistration 时才会使用。所以,咱们能够提取一个子类,并将这两个属性转移到NonRegistration类中,这样才更合适。
- public class Registration
- {
- public decimal RegistrationTotal { get; set; }
- public string Description { get; set; }
- public DateTime RegistrationDate { get; set; }
- }
-
- public class NonRegistration : Registration
- {
- public NonRegistrationAction Action { get; set; }
- public string Notes { get; set; }
- }
代码重构第21天:合并继承
今天的重构来自于Martin Fowler的模式目录。你能够在他的目录中找到该重构。昨天,咱们经过提取子类来下放职责。而今天,当咱们意识到再也不须要某个子类时,可使用Collapse Hierarchy 重构。若是某个子类的属性(以及其余成员)能够被合并到基类中,这时再保留这个子类已经没有任何意义了。
- public class Website
- {
- public string Title { get; set; }
- public string Description { get; set; }
- public IEnumerable<Webpage> Pages { get; set; }
- }
-
- public class StudentWebsite : Website
- {
- public bool IsActive { get; set; }
- }
这里的子类并无过多的功能,只是表示站点是否激活。这时咱们会意识到判断站点是否激活的功能应该是通用的。所以能够将子类的功能放回到Website 中,并删除StudentWebsite 类型。
- public class Website
- {
- public string Title { get; set; }
- public string Description { get; set; }
- public IEnumerable<Webpage> Pages { get; set; }
- public bool IsActive { get; set; }
- }
代码重构第22天:分解方法
今天的重构没有任何出处。可能已经有其余人使用了相同的重构,只是名称不一样罢了。若是你知道谁的名字比Break Method更好,请转告我。
这个重构是一种元重构(meta-refactoring),它只是不停地使用提取方法重构,直到将一个大的方法分解成若干个小的方法。下面的例子有点作做,AcceptPayment 方法没有丰富的功能。所以为了使其更接近真实场景,咱们只能假设该方法中包含了其余大量的辅助代码。
下面的AcceptPayment 方法能够被划分为多个单独的方法。
- public class CashRegister
- {
- public CashRegister()
- {
- Tax = 0.06m;
- }
-
- private decimal Tax { get; set; }
- public void AcceptPayment(Customer customer, IEnumerable<Product> products, decimal payment)
- {
- decimal subTotal = 0m;
-
- foreach (Product product in products)
- {
- subTotal += product.Price;
- }
-
- foreach (Product product in products)
- {
- subTotal -= product.AvailableDiscounts;
- }
-
- decimal grandTotal = subTotal * Tax;
- customer.DeductFromAccountBalance(grandTotal);
- }
- }
-
- public class Customer
- {
- public void DeductFromAccountBalance(decimal amount)
- {
- // deduct from balance
- }
- }
-
- public class Product
- {
- public decimal Price { get; set; }
- public decimal AvailableDiscounts { get; set; }
- }
如您所见,AcceptPayment方法包含多个功能,能够被分解为多个子方法。所以咱们屡次使用提取方法重构,结果以下:
- public class CashRegister
- {
- public CashRegister()
- {
- Tax = 0.06m;
- }
-
- private decimal Tax { get; set; }
- private IEnumerable<Product> Products { get; set; }
-
- public void AcceptPayment(Customer customer, IEnumerable<Product> products, decimal payment)
- {
- decimal subTotal = CalculateSubtotal();
- subTotal = SubtractDiscounts(subTotal);
- decimal grandTotal = AddTax(subTotal);
- SubtractFromCustomerBalance(customer, grandTotal);
- }
-
- private void SubtractFromCustomerBalance(Customer customer, decimal grandTotal)
- {
- customer.DeductFromAccountBalance(grandTotal);
- }
-
- private decimal AddTax(decimal subTotal)
- {
- return subTotal * Tax;
- }
-
- private decimal SubtractDiscounts(decimal subTotal)
- {
- foreach (Product product in Products)
- {
- subTotal -= product.AvailableDiscounts;
- }
- return subTotal;
- }
-
- private decimal CalculateSubtotal()
- {
- decimal subTotal = 0m;
- foreach (Product product in Products)
- {
- subTotal += product.Price;
- }
- return subTotal;
- }
- }
-
- public class Customer
- {
- public void DeductFromAccountBalance(decimal amount)
- {
- // deduct from balance
- }
- }
-
- public class Product
- {
- public decimal Price { get; set; }
- public decimal AvailableDiscounts { get; set; }
- }
代码重构第23天:引入参数对象
该重构来自于Fowler的重构目录。有时当使用一个包含多个参数的方法时,因为参数过多会致使可读性严重降低,如:
- public void Create(decimal amount, Student student, IEnumerable<Course> courses, decimal credits)
- {
- // do work
- }
这时有必要新建一个类,负责携带方法的参数。若是要增长更多的参数,只需为对参数对象增长其余的字段就能够了,代码显得更加灵活。要注意,仅仅在方法的参数确实过多时才使用该重构,不然会使类的数量暴增,而这本应该越少越好。
- public class RegistrationContext
- {
- public decimal Amount { get; set; }
- public Student Student { get; set; }
- public IEnumerable<Course> Courses { get; set; }
- public decimal Credits { get; set; }
- }
-
- public class Registration
- {
- public void Create(RegistrationContext registrationContext)
- {
- // do work
- }
- }
代码重构第24天:分解复杂判断
今天的重构基于c2的wiki条目。Los Techies的Chris Missal一样也些了一篇关于反模式的post。简单地说,当你使用大量的嵌套条件判断时,造成了箭头型的代码,这就是箭头反模式(arrowhead
antipattern)。我常常在不一样的代码库中看到这种现象,这提升了代码的圈复杂度(cyclomatic complexity)。下面的例子演示了箭头反模式:
- public class Security
- {
- public ISecurityChecker SecurityChecker { get; set; }
- public Security(ISecurityChecker securityChecker)
- {
- SecurityChecker = securityChecker;
- }
-
- public bool HasAccess(User user, Permission permission, IEnumerable<Permission> exemptions)
- {
- bool hasPermission = false;
- if (user != null)
- {
- if (permission != null)
- {
- if (exemptions.Count() == 0)
- {
- if (SecurityChecker.CheckPermission(user, permission) || exemptions.Contains(permission))
- {
- hasPermission = true;
- }
- }
- }
- }
- return hasPermission;
- }
- }
移除箭头反模式的重构和封装条件判断同样简单。这种方式的重构在方法执行以前每每会评估各个条件,这有点相似于契约式设计验证。下面是重构以后的代码:
- public class Security
- {
- public ISecurityChecker SecurityChecker { get; set; }
-
- public Security(ISecurityChecker securityChecker)
- {
- SecurityChecker = securityChecker;
- }
-
- public bool HasAccess(User user, Permission permission, IEnumerable<Permission> exemptions)
- {
- if (user == null || permission == null)
- return false;
-
- if (exemptions.Contains(permission))
- return true;
- return SecurityChecker.CheckPermission(user, permission);
- }
- }
如你所见,该方法大大整价了可读性和之后的可维护性。不难看出,该方法的全部可能的路径都会通过验证。
代码重构第25天:引入契约式设计
契约式设计(DBC,Design By Contract)定义了方法应该包含输入和输出验证。所以,能够确保全部的工做都是基于可用的数据,而且全部的行为都是可预料的。不然,将返回异常或错误并在方法中进行处理。要了解更多关于DBC的内容,能够访问wikipedia。
在咱们的示例中,输入参数极可能为null。因为没有进行验证,该方法最终会抛出NullReferenceException。在方法最后,咱们也并不肯定是否为用户返回了一个有效的decimal,这可能致使在别的地方引入其余方法。
- public class CashRegister
- {
- public decimal TotalOrder(IEnumerable<Product> products, Customer customer)
- {
- decimal orderTotal = products.Sum(product => product.Price);
- customer.Balance += orderTotal;
- return orderTotal;
- }
- }
在此处引入DBC 验证是十分简单的。首先,咱们要声明customer 不能为null,而且在计算总值时至少要有一个product。在返回订单总值时,咱们要肯定其值是否有效。若是此例中任何一个验证失败,咱们将以友好的方式抛出相应的异常来描述具体信息,而不是抛出一个晦涩的NullReferenceException。
在.NET Framework 3.5的Microsoft.Contracts命名空间中包含一些DBC框架和异常。我我的尚未使用,但它们仍是值得一看的。关于该命名空间只有在MSDN上能找到点资料。
- public class CashRegister
- {
- public decimal TotalOrder(IEnumerable<Product> products, Customer customer)
- {
- if (customer == null)
- throw new ArgumentNullException("customer", "Customer cannot be null");
- if (products.Count() == 0)
- throw new ArgumentException("Must have at least one product to total", "products");
-
- decimal orderTotal = products.Sum(product => product.Price);
- customer.Balance += orderTotal;
-
- if (orderTotal == 0)
- throw new ArgumentOutOfRangeException("orderTotal", "Order Total should not be zero");
-
- return orderTotal;
- }
- }
在验证过程当中确实增长了很多代码,你也许会认为过分使用了DBC。但我认为在大多数状况下,处理这些棘手的问题所作的努力都是值得的。追踪无详细内容的NullReferenceException的确不是什么美差。
代码重构第26天:避免双重否认
今天的重构来自于Fowler的重构目录。
尽管我在不少代码中发现了这种严重下降可读性并每每传达错误意图的坏味道,但这种重构自己仍是很容易实现的。这种毁灭性的代码所基于的假设致使了错误的代码编写习惯,并最终致使bug。以下例所示:
- public class Order
- {
- public void Checkout(IEnumerable<Product> products, Customer customer)
- {
- if (!customer.IsNotFlagged)
- {
- // the customer account is flagged
- // log some errors and return
- return;
- }
-
- // normal order processing
- }
- }
-
- public class Customer
- {
- public decimal Balance { get; private set; }
-
- public bool IsNotFlagged
- {
- get { return Balance < 30m; }
- }
- }
如你所见,这里的双重否认十分难以理解,咱们不得不找出什么才是双重否认所要表达的确定状态。修改代码是很容易的。若是咱们找不到确定的判断,能够添加一个处理双重否认的假设,而不要在获得结果以后再去验证。
- public class Order
- {
- public void Checkout(IEnumerable<Product> products, Customer customer)
- {
- if (customer.IsFlagged)
- {
- // the customer account is flagged
- // log some errors and return
- return;
- }
-
- // normal order processing
- }
- }
-
- public class Customer
- {
- public decimal Balance { get; private set; }
-
- public bool IsFlagged
- {
- get { return Balance >= 30m; }
- }
- }
代码重构第27天:去除上帝类
在传统的代码库中,咱们经常会看到一些违反了SRP原则的类。这些类一般以Utils或Manager结尾,有时也没有这么明显的特征而仅仅是普通的包含多个功能的类。这种God 类还有一个特征,使用语句或注释将代码分隔为多个不一样角色的分组,而这些角色正是这一个类所扮演的。
长此以往,这些类成为了那些没有时间放置到恰当类中的方法的垃圾桶。这时的重构须要将方法分解成多个负责单一职责的类。
- public class CustomerService
- {
- public decimal CalculateOrderDiscount(IEnumerable<Product> products, Customer customer)
- {
- // do work
- }
-
- public bool CustomerIsValid(Customer customer, Order order)
- {
- // do work
- }
-
- public IEnumerable<string> GatherOrderErrors(IEnumerable<Product> products, Customer customer)
- {
- // do work
- }
-
- public void Register(Customer customer)
- {
- // do work
- }
-
- public void ForgotPassword(Customer customer)
- {
- // do work
- }
- }
使用该重构是很是简单明了的,只需把相关方法提取出来并放置到负责相应职责的类中便可。这使得类的粒度更细、职责更分明、往后的维护更方便。上例的代码最终被分解为两个类:
- public class CustomerOrderService
- {
- public decimal CalculateOrderDiscount(IEnumerable<Product> products, Customer customer)
- {
- // do work
- }
-
- public bool CustomerIsValid(Customer customer, Order order)
- {
- // do work
- }
-
- public IEnumerable<string> GatherOrderErrors(IEnumerable<Product> products, Customer customer)
- {
- // do work
- }
- }
-
- public class CustomerRegistrationService
- {
- public void Register(Customer customer)
- {
- // do work
- }
-
- public void ForgotPassword(Customer customer)
- {
- // do work
- }
- }
代码重构第28天:为布尔方法命名
今天的重构不是来自于Fowler的重构目录。若是谁知道这项“重构”的确切出处,请告诉我。
固然,你也能够说这并非一个真正的重构,由于方法实际上改变了,但这是一个灰色地带,能够开放讨论。一个拥有大量布尔类型参数的方法将很快变得没法控制,产生难以预期的行为。参数的数量将决定分解的方法的数量。来看看该重构是如何开始的:
- public class BankAccount
- {
- public void CreateAccount(Customer customer, bool withChecking, bool withSavings, bool withStocks)
- {
- // do work
- }
- }
要想使这样的代码运行得更好,咱们能够经过命名良好的方法暴露布尔参数,并将原始方法更改成private以阻止外部调用。显然,你可能须要进行大量的代码转移,也许重构为一个Parameter Object 会更有意义。
- public class BankAccount
- {
- public void CreateAccountWithChecking(Customer customer)
- {
- CreateAccount(customer, true, false);
- }
-
- public void CreateAccountWithCheckingAndSavings(Customer customer)
- {
- CreateAccount(customer, true, true);
- }
-
- private void CreateAccount(Customer customer, bool withChecking, bool withSavings)
- {
- // do work
- }
- }
代码重构第29天:去除中间人对象
今天的重构来自于Fowler的重构目录。
有时你的代码里可能会存在一些“Phantom”或“Ghost”类,Fowler 称之为“中间人(Middle Man)”。这些中间人类仅仅简单地将调用委托给其余组件,除此以外没有任何功能。这一层是彻底没有必要的,咱们能够不费吹灰之力将其彻底移除。
- public class Consumer
- {
- public AccountManager AccountManager { get; set; }
-
- public Consumer(AccountManager accountManager)
- {
- AccountManager = accountManager;
- }
-
- public void Get(int id)
- {
- Account account = AccountManager.GetAccount(id);
- }
- }
-
- public class AccountManager
- {
- public AccountDataProvider DataProvider { get; set; }
-
- public AccountManager(AccountDataProvider dataProvider)
- {
- DataProvider = dataProvider;
- }
-
- public Account GetAccount(int id)
- {
- return DataProvider.GetAccount(id);
- }
- }
-
- public class AccountDataProvider
- {
- public Account GetAccount