最常用的重构指导

  1. 云栖社区>
  2. 博客>
  3. 正文

最常用的重构指导

luminji 2013-10-06 19:51:00 浏览292
展开阅读全文

参考: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。

Creative Commons License本文基于Creative Commons Attribution 2.5 China Mainland License发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名http://www.cnblogs.com/luminji(包含链接)。如您有任何疑问或者授权方面的协商,请给我留言。

网友评论

登录后评论
0/500
评论
luminji
+ 关注