参考:http://www.cnblogs.com/KnightsWarrior/archive/2010/06/30/1767981.html,本文示例代码多来自此处;
参考:《重构:改善既有代码》;
完美而高档的摩天大厦应至少具备两个特点:房间内部是清洁的、结构上是无懈可击的。优秀的代码也应如此。码农要负责打扫房间,架构师负责搭建一个经得起考验的代码结构。有些人兼顾码农和架构的角色。如果你既不是码农,也不是架构师,那么就请离代码远点,离重构远点,要有多远滚多远。
一:打扫房间
1:避免重复代码
避免重复代码在大多数情况下适用,但是我有一个逆观点是:允许重复代码,如果它影响到你的架构。
2:提取方法原则,超过30行?
并不一定超过30行的代码就必须提取为方法,当然,原则上,大部分情况下应该是这样的。还有,如果提取方法让你的代码更清晰,你就应该提取方法,如下:
namespace LosTechies.DaysOfRefactoring.ExtractMethod.Before
{
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;
}
}
}
namespace LosTechies.DaysOfRefactoring.ExtractMethod.After
{
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;
}
}
}
3:警惕超过300行的类
如果它不是个门面类,那么超过300行的类很多时候过于复杂,俗称“上帝类”,因为它妄图做太多事情,可以考虑重构成更小的类;
4:过多的方法参数
方法参数超过5个几乎总是有问题的,可以把参数提取为一个实体类。当然,越接近于底层我越能容忍这种情况的发生,比如 DAL 类,查询条件多的情况下,我会允许带很多参数。
5:没有必要的注释
很多人拿微软的 FCL(基础类库) 来举反例,说 MS 的注释简直全面俱到。对不起,你要看清它在开发什么,它在开发 API,供我等小白使用的,所以它必须提供一份全面俱到的 API 说明。大多数情况下,干掉你代码中的注释,把代码写的让别人能直接看得懂。如果一定要写注释,则一定要按规范格式来,不是两个反斜杠后面跟一段话就叫做注释,你给自己身上贴满创可贴试试。
6:不要用异常
用 Tester-Doer 模式取代异常,不要尝试总是使用异常。
7:要用异常
不要使用这种代码:
public bool Insert(Model model)
{
//some other code
Dal dal = new Dal();
if (dal.Insert(model))
{
return true;
}
else
{
return false;
}
}
直接让异常往上抛,
public bool Insert(Model model)
{
//some other code
new Dal(.Insert(model));
}
直到某个地方愿意处理地方。
8:方法内的代码属于一个层级
穿衣服,穿裤子属于一个层级。穿衣服,造汽车,就不是同一个层级。
9:Dispose
如果某个东西需要 Close,就应该实现 IDispose。
10:Static Or Not
如果该类需要进入单元测试,则它不应该是 Static 的。如果静态了,代码就是在测试的收你得额外增加一个包装类。
11:Shotgun Surgery(霰弹式修改)
现象:当外部条件发生变化时,每次需要修改多个Class来适应这些变化,影响到很多地方。就像霰弹一样,发散到多个地方。
重构策略:使用Move Method和Move Field将Class中需要修改的方法及成员变量移植到同一个Class中。如果没有合适的Class,则创建一个新Class。实现目标是,将需要修改的地方集中到一个Class中进行处理。
12:Feature Envy(依恋情结)
现象:Class中某些方法“身在曹营心在汉”,没有安心使用Class中的成员变量,而需要大量访问另外Class中的成员变量。这样就违反了对象技术的基本定义:将数据和操作行为(方法)包装在一起。
重构策略:使用Move Method将这些方法移动到对应的Class中,以化解其“相思之苦”,让其牵手。
13:组合与继承,你有两种选择
这里无所谓说哪种好,哪种坏,看情况,以下是这两种的表现形式,你可以进行互转。
namespace LosTechies.DaysOfRefactoring.ReplaceInheritance.Before
{
public class Sanitation
{
public string WashHands()
{
return "Cleaned!";
}
}
public class Child : Sanitation
{
}
}
namespace LosTechies.DaysOfRefactoring.ReplaceInheritance.After
{
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();
}
}
}
14:分解复杂判断
复杂的判断基础总是要分解的,因为它太容易阅读了,写注释?注释一坨 Shit?
namespace LosTechies.DaysOfRefactoring.SampleCode.ArrowheadAntipattern.Before
{
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;
}
}
}
namespace LosTechies.DaysOfRefactoring.SampleCode.ArrowheadAntipattern.After
{
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);
}
}
}
15:尽快返回
实际上,该条是要求我们,可以在方法内部多使用 return,直到最后才 return,会使得最终结果看起来很复杂;
二:盖房子
1:寻找边界
架构的第一原则,是寻找边界,最直观的成果物就是建立几个解决方案,解决方案内有多少个项目。划边界的最终目的就是要告诉组员:什么样的代码应该编写到哪个解决方案中。
2:建立公共库
任何解决方案几乎都需要一个公共库,用于放置一些 Helper 类。
3:资源可以作为一个单独的项目
不同的资源可以独立成为不同的项目,图片、JS、Style字典、配置文件等,都可以作为资源。另外,第三方的 DLL 也需要作为资源独立出来,把它们注册到全局程序集中不如直接作为 Content 包含进项目来的舒爽。
4:客户端逻辑最小化
.NET 程序最让人诟病的是:混淆了也可窥测你的源码。除了这个原因,从解耦的角度看,UI 或者其它客户端项目,都应该知道更少的逻辑才好。
5:基于测试的与MVC、MVVM、MVP
如果一开始你并不知道什么是 MVC 或者 MVVM,那么没关系,先试着掌握单元测试,把代码写成基于测试的。我有一个激进的观点是,所有的架构模式,其实目的都是为了代码可测试。
6:AOP
权限认证是典型的面向切面编程。不是 Attribute 才能带来 AOP 思想,把要运行的代码交给一个 Action ,也能实现 AOP。
7:模版模式、继承与多态
继承不是多态,继承的另一个价值叫做:模版模式。如果一件 Case 有多个实现途径,它就应该是模版的,因为你总能找到一些方法放置到父类中去;
8:工厂模式与工厂
类不是被调用者 new 出来的,而是调用某个类的某个方法后被返回出来的,就叫做工厂模式。这类也叫做对象容器。对象容器也可以很复杂,复杂到叫做一个框架,比如 Unity。
9:观察者模式、事件通知
事件就是观察者模式。解耦也可以使用观察者模式来实现。
10:接口的存在都是有目的的
自从 面向接口编程 这个概念提出来后,接口就开始变得漫天飞。接口的出现不能基于某种假设,而是实际已经发生了作用。
11:避免二转手的代码
二转手的代码常常来自于所谓三层架构代码,UI-BLL-DAL,然后 BLL 中的大量方法实际就只有一句话 Dal.Update(model),老实说,我受够了这样的代码。
12:见到条件,就考虑是否使用策略模式
“使用策略类” 是指用设计模式中的策略模式来替换原来的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 LosTechies.DaysOfRefactoring.SwitchToStrategy.After_WithIoC { 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; } } }
总结:这种重构在设计模式当中把它单独取了一个名字——策略模式,这样做的好处就是可以隔开耦合,以注入的形式实现功能,这使增加功能变得更加容易和简便,同样也增强了整个系统的稳定性和健壮性。
13:分解依赖
无抽象、静态类、静态方法都是不可单元测试的。那么,如果我们要写出可测试的代码,又要用到这些静态类等,该怎么办,实际上我们需要两个步骤:
1:为它们写一个包装类,让这个包装类是抽象的(继承自接口,或者抽象类,或者方法本身是Virtual的);
2:通知客户端程序员,使用包装类来代替原先的静态类来写业务逻辑;
FCL 中的典型例子是:HttpResponseWrapper。