阅读《重构》的一些思考

  终于在断断续续的情况下把这本经典巨作看完了。

  这本书的全名叫做《重构-改善既有代码的设计》,原有的代码设计存在不足的地方让人感到不好维护,所以才需要去改善既有代码的设计,其实听起来会不会有点亡羊补牢的感觉?这里也提醒了我们一点:从设计代码的初期就要深思熟虑,虽然后续的改动基本无法避免,但良好的初期设计将对后续维护提供帮助。重构不是最终目的,我们的目的是让代码变得更好。

关于书的内容组织

  不得不称赞的一点是作者的写作思路是非常清晰的。整本书的开始是由一个小型案例讲起,让读者了解重构是一项什么样的工作,作者的案例设计也是挺巧妙的,值得不熟悉面向对象编程的读者反复揣摩。之后便开始讲诉重构的一些概念,包括什么是重构(在不改变软件可观察行为的前提下,调整其内部结构,整理代码的一个过程。注意与开发新功能的一个区分,开发新功能是在改变软件可观察行为)、为什么要重构(重构可以提高代码的可维护性)以及什么时候重构(添加功能、修复错误、复审代码的时候)。

  接下来就列举了一系列要点讲解不好的代码设计的一些症状。中间携带了关于测试体系的介绍,不过这一部分更多的是稍微提点一下,并没有做比较深入的分析。之后就迎来了整本书的重头戏,关于重构手法的介绍,包括如何组织函数、在对象之间搬移特性(字段、函数)、重新组织数据、简化条件表达式、简化函数调用和处理继承关系。随后也顺带介绍了大型重构的四个手法,包括梳理并分解继承体系、将过程化设计转化为对象设计、将领域与表述分离和提炼继承体系。

  本书的最后是对于重构的一个概括,作者认为真正懂得重构的一个衡量标准是”你可以自信地停止重构“,学会重构手法只是一个起点,把握何时使用、何时开始、何时停止的节奏才是使重构走向成功的关键。

阅读重构手法的心得

  书中介绍了多种重构手法,这里也不打算将其一一列举出来,而是挑选了部分让我醍醐灌顶的重构手法,以及阅读中自己的一些思考。说实在话,自己在阅读本书的时候并没有做到非常细致,特别是对于具体重构的步骤几乎是一扫而过,读得非常”粗“,可能跟我自己没有真正地处理起大型的遗留代码有关联,思维里总是回响着一股这样的声音”这个重构手法是处理这样的问题,恩,在最开始进行开发的时候就要留意这一点“,从而让我觉得这些重构的具体步骤很繁琐无趣,对于现阶段也没有较大的实用价值。或许等我真正接触到非常多的遗留代码问题,在来重读本书会有新的体会。

  1. 把关好命名原则

  良好的命名是可读代码的基础,书中对于这个概念也是非常重视的,甚至鼓励开发者在没有找到一个合适的命名前不要继续往下开发。虽然书中并没有讨论如何把握命名方式,但是这里我就班门弄斧一回,简单地谈一下自己的一些看法。

  首先要注意的第一个环节是要把握C#开发的基本命名规范,例如私有字段用‘_’开头、函数名称的首个单词最好能是一个动词、不要将变量名称命名为“temp”等,如果我们能做好这最基本的一点,让别人来看你的代码也是那么地清晰,这是不是一件美好的事情呢?所以,假如对于C#开发的基本命名规范还没有概念的话,是时候需要去恶补一下了。

  多数情况下开发都离不开业务,所以良好的命名也要结合具体业务。在分析业务的同时也要积累业务相关的词汇,例如Order表示订单,Sku表示库存量单位等,这些词汇在命名的时候可以提供支持。

  最后提及的一点策略是借鉴设计模式的词汇。虽然有时候我们不一定会在代码中使用设计模式,但是借鉴某些词汇来表达意图也是可以的,例如Factory、Adapter、Template、Bulider、Singleton、Proxy等,前人已经将经验汇集提供给了我们,我们需要做的就是好好利用它们。

  总体而言,就连作者本人也承认的一个观点是“经验会带来更多的帮助”,所以在开发中不仅要重视名称的选择,也要注重经验的积累。

  2. 使用return跳出多条件表达式

  说实在话,这是阅读本书最让我眼前一亮的重构手法了。不知道大家有没有跟我有这样的习惯:

public bool IsEnable()
{
    bool result = false;
    if (条件1)
    {
        result = true;
    }
    else
    {
        if (条件2)
        {
            result = true;
        }
        else
        {
            for (int i = 0; i < length; i++)
            {
                if (条件3)
                {
                    result = true;
                    break;
                }
            }
        }
    }
    return result;
}
多条件判断代码

  这里用伪代码模拟了一下场景,其实有可能实际的函数比这个还复杂,在函数的开头就声明一个result变量,接下来就是各种条件判断对result赋值,最后在统一返回这个result,这就是开发人员对于“统一出口”的思想。其实有时候会不会觉得这样的代码看起来很绕?来看下使用这个手法重构后的代码吧:

public bool IsEnable()
{
    if (条件1) return true;

    if (条件2) return true;

    for (int i = 0; i < length; i++)
    {
        if (条件3) return true;
    }
    return false;
}
重构后的代码

  重构后带来的最大变化是减少了else逻辑,及时地return避免引导读者去看另外一个没有用的else区域,代码变得更加清晰了。

  3. 没有一成不变的规则,具体情况具体分析

  重构手法更多地像一个工具箱,里面放满了十字螺丝刀,钳子,扳手等,处理代码遗留问题就像修理东西一样,遇到不同的问题就要使用不同的工具。其实书中有时候作者介绍的重构手法是一个对立面,例如将提炼函数和内联函数,一个主张提炼出一个独立的函数,一个提倡不使用独立的函数,所以涉及到具体情况就要具体分析,要从理解重构手法的角度出发,没有绝对的使用定律。

我所认为的书中的不足之处

  有些讨论的话题比较泛化,例如6.9节替换算法,“将函数本体替换为另一个算法”,内容描述起来比较空洞,有点类似于告诉我们“嗯,这个函数这样写不好,换一种实现方式更好”,并不能合格地称为一种重构手法。

  对于某些重构手法我是保持着中立态度(意思是这种重构手法我不太喜欢这样做),例如6.4节的“以查询取代临时变量”,来看一段书中给出的伪代码:

//重构前的代码
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
    return basePrice * 0.95;
else
    return basePrice * 0.85;

//重构后的代码
if (CaculateBasePrice() > 1000)
    return CaculateBasePrice() * 0.95;
else
    return CaculateBasePrice() * 0.85;

double CaculateBasePrice()
{
    return _quantity * _itemPrice;
}
6.4节例子

  有时候为了重用代码,将表达式提炼到一个函数中,可以更好地提高代码复用率。但这个重构手法还有一个着重点:用提炼的函数去替代原来的变量。其实我觉得这里用一个临时变量也并没有什么问题,替换为函数反而可能会出现一些问题:如果表达式是一个复杂的计算,每个使用的地方都调用一遍会降低性能;在阅读代码的时候,如果遇到函数调用的情况,都会将关注点调到函数中去,反而会降低了代码可读的流畅性。基于前面诉说的理由,我个人是不太喜欢实践这个重构手法。

  不得不承认的一点,就是这本书的写作时间是比较早的,以至于书中介绍的重构手法的实现步骤是建立于当时开发环境下的,部分做法可以说是已经“过时”了。例如10.1节函数改名,书中介绍了较为复杂的做法,但放在当今的开发环境下,IDE基本已经集成了函数改名这一重构功能,只要输入新的函数名称就能自动替换所有引用到旧函数的地方,已经大大提高了重构效率;6.1节提炼函数也是类似的情况。

  不过,这并不阻碍这本书的历史地位。推荐给各位读者,读完后也许你也会对代码重构有新的感悟。

原文地址:https://www.cnblogs.com/teroy/p/4194346.html