再论重构

  重构是一个热门的话题,很多人也许知道,但是并不会实践,如果尝试一下的话,你会觉得代码真的不一样了,现在决定改变一下吧?!

重构的动机

  重构不是无目的的,重构是一种不改变代码行为的前提下,改善代码可读性,可扩展性的过程。

  了解重构之前,我们需要了解一下代码为什么需要重构。

1. 软件代码是会腐烂的
  代码是会腐烂的,而且是逐渐的,不知不觉的就从好变烂了。

  毫无疑问,并不是所有的烂代码都是一次写成的,也许最初的代码设计的是很好的,但是一旦被多个人修改过以后,就变坏了,我想这个大家肯定是深有体会的。代码总是在所有人的共同“努力”下写烂的。

  从代码变烂的那个时刻开始,当我们需要在其中工作的时候,就会不自觉的添加上自己的一堆烂代码,觉得毫无羞愧,因为代码已经烂了。

  当我们对这种现象习以为常的时候,代码就越来越烂了。
2. 破窗效应
  对于已经开始腐烂的代码我们通常会不去关注和改善,而是继续选择让其更烂,就好像我们对于好多玻璃已经破损的窗户来说,继续打破一块玻璃丝毫不以为意的现象一样,这就是心理学上的“破窗效应”,说白了就是破罐子破摔。就像下面这幅图描述的这样:


3. 技术债务
  这种已经腐烂的代码对于后来的团队来说,就是上一代团队所欠的债务,因为当软件的规模渐渐稳定下来以后,收入增加就很慢了,而后来的团队维护或添加新功能的成本却存在,相对于渐渐减少的收入来说,现有的代码就是一种债务,后面团队的工作也就是在为前人留下的腐烂代码还债的过程。

  综上所述,为了自己,为了他人,让我们力所能及的重构吧!

重构的难题

  好了,看了代码变烂的过程,再回味一下修改烂代码的过程,你是不是想试试重构了?实际上,要实施重构,是会遇到很多难题的。

1. 技术上的难题
  技术上的难题其实是最简单的问题,因为所有技术上的问题,都可以通过学习和练习来解决,比如学习什么是好的代码,学习如何辨别不良代码,如何组织单元测试,如何设计和架构等。
2. 管理上的难题
  事实上,管理上的难题比技术上的难题更加难以处理。

  下面这幅图大家肯定非常熟悉了:


  这幅图显示的含义大概就是“冰山一角”了。对于我们的项目来说,其实存在相同的结构,那就是用户看的见的功能只是水面上面的那一点,而用于支持这些功能的代码却有那么一大坨。

  通常我们的Manager,特别是一线的Manager,关注的都是水面上那一点,他们从来不看代码,从来不关注代码质量,只关注项目进度。他们会说:“什么?拿出时间重构?免谈!”“你这个Sprint都忙什么了?我没看到功能有一点变化啊?”...每当大伙听到这种话的时候,是不是当场晕菜?

  长期这样下去,冰山下面那一坨慢慢就垮了,真的到了那个时候,难道上面那一点还会存在吗?

  所以,在管理上,所有人都应该重视代码的内在质量。
  软件工程并不只是程序员的事,参与软件研发的所有人员,包括管理人员,测试人员,开发人员等,都应该了解软件的质量不仅仅包括面上所表现出来的那些业务流程,而且包括面上没有表现出来的易读性,扩展性。这些非功能性需求有时候更能决定项目的成败。

3. 个人难题 - 程序员心理学
  程序员总是宽以律己,严于待人。
  自以为是,总喜欢给别人挑刺,给自己找借口,这是很多程序员的通病。这种情况下,再怎么培训也是不行的,我们总是需要先放下身段,谦虚学习,做好自己,守住自己的职业道德底线。
  守住程序员的底线,保持最起码的职业道德,这是一切的关键。如果自己做不到怎么办?那就通过行政手段来强制执行吧,哈哈,开个玩笑。

重构的目标

  当你们克服了上面的那些难题,准备大展身手的时候,且慢,让我们先来看一下重构的目标。重构的目标是改善代码,或者说是改出美的代码。

1. 什么是美的代码?
  1). 沟通
  易于读懂的代码就是好代码,比如白居易为什么能写出流传千古,妇孺皆知的好诗,那是因为他总是把写好的诗词念给老奶奶听,如果她听不懂,就“重构”直到她能听懂。美的代码必须是易于沟通的代码。
  2). 简单
  简单就是美,对称就是美嘛,这个就不用多说了。
  3). 灵活
  易于扩展,易于修改的代码当然就是好代码了。一个几千行的代码,你能轻易的扩展吗?是美的代码吗?
2. 什么是不美的代码?
  所有不简单,不直接,不易读懂,不易扩展的代码就是不美的代码,比如重复,杂糅,易混淆,强耦合,复杂表达式等。当然了很多人认为:写出别人看不懂,复杂诡异的代码才是高人。对此我认为:如果是个人玩的项目,或者是永远不会有其他人去维护的项目,那么写是无可厚非的;但是如果是多人合作的项目,那么写有点不太厚道。

重构的手段
  好了,知道了重构的目标,我们下面就可以去了解如何重构代码。重构的手段多种多样,并且通常某一种手段能达到多个目的。下面是我总结的几种手段:
1. 命名
  该手段常用于通过重新给目标一个新的易于理解,清晰的表达其用途或属性的名称来解决难于理解,难懂的问题。
  比如给某个表达式起个易于理解的名字表示表达式的意图。看下面的代码:

int salary = getSalary();
int level = getLevel();
bool authorized = isAuthorized();
if (salary > 5000 || level > 10 || (salary > 4000 && level > 8 && authorized))
{
    //...
}

这是一个判断条件并执行一定逻辑的代码,你觉得怎么样?

  再看重构以后的代码:

int salary = getSalary();
int level = getLevel();
bool authorized = isAuthorized();

bool isHightSalary = salary > 5000;
bool isHightLevel = level > 10;
bool isMiddleButAllowed = salary > 4000 && level > 8 && authorized;
if (isHightSalary || isHightLevel || isMiddleButAllowed)
{
    //...
}

大家觉得那种容易理解?

  有时也为了表达不同的目的,来起一个别名来达到易于理解的目的。看下面的例子:

static void Main(string[] args)
{
    // 1. scrubber move
    onScrubberMove();

    // 2. action move
    onActionMove();
}

static void extendTime() { }
static void onScrubberMove() { extendTime(); }
static void onActionMove() { extendTime(); }

这是一个简化的模型,在这个程序中,我们有两种对象移动的时候要去扩展时间线,同样的行为包装在含义更加明显的函数中更容易理解。

2. 拆分
  该手段常用于通过简化目标的逻辑结构来解决复杂的表述问题。
  比如将复杂的表达式拆开成多个子表达式分别求解后合成。还看上面的那个例子:

static void Main(string[] args)
{
    int salary = getSalary();
    int level = getLevel();
    bool authorized = isAuthorized();

    if (salary > 5000 || level > 10 || (salary > 4000 && level > 8 && authorized))
    {
        doWork();
    }
}

再看重构后的例子:

static void Main(string[] args)
{
    int salary = getSalary();
    int level = getLevel();
    bool authorized = isAuthorized();

    bool isHightSalary = salary > 5000;
    if (!isHightSalary)
    {
        return;
    }

    bool isHightLevel = level > 10;
    if (!isHightLevel)
    {
        return;
    }

    bool isMiddleButAllowed = salary > 4000 && level > 8 && authorized;
    if (!isMiddleButAllowed)
    {
        return;
    }

    doWork();
}

是否更加容易理解一点。

  比如将复杂的函数拆开成多个子函数来简化主函数的条理,使得主函数抽象层次一致。针对上面的例子,再重构一步如何?看代码:

static void Main(string[] args)
{
    if(isAllowed())
    {
        doWork();
    }
}

private static bool isAllowed()
{
    int salary = getSalary();
    int level = getLevel();
    bool authorized = isAuthorized();

    bool isHightSalary = salary > 5000;
    if (!isHightSalary)
    {
        return false;
    }

    bool isHightLevel = level > 10;
    if (!isHightLevel)
    {
        return false;
    }

    bool isMiddleButAllowed = salary > 4000 && level > 8 && authorized;
    if (!isMiddleButAllowed)
    {
        return false;
    }

    return true;
}

Main函数的逻辑是否变得更加容易理解了,这样的子函数是不是也更容易维护和扩展?

3. 归类
  该手段常用于通过将具有类似功能或逻辑的目标放到一起来解决代码凌乱,难于阅读,难于查找,难于修改的问题。

  如最简单的代码归类:

static void Main(string[] args)
{
    List<int> salaryList = new List<int>();
    List<int> levelList = new List<int>();
    List<int> scoreList = new List<int>();

    collectHighSalary(salaryList);
    collectHighLevel(levelList);
    collectHighScore(scoreList);

    collectMiddleSalary(salaryList);
    collectMiddleLevel(levelList);

    collectLowSalary(salaryList);
    collectLowlevel(levelList);
}

这段代码已经很好了,再看看下面这样归类如何:

static void Main(string[] args)
{
    List<int> salaryList = new List<int>();
    collectHighSalary(salaryList);
    collectMiddleSalary(salaryList);
    collectLowSalary(salaryList);

    List<int> levelList = new List<int>();
    collectHighLevel(levelList);
    collectMiddleLevel(levelList);
    collectLowlevel(levelList);

    List<int> scoreList = new List<int>();
    collectHighScore(scoreList);
}

每一段代码的内聚性是不是更高了?!修改后的代码是不是满足了变量最小作用域的原则?!

  归类最多的场景就是上面的每个collect方法都分散在多个类中,这个时候把这些方法收集放到一个辅助类中是不是更能满足高内聚的原则。

  归类对于方法太多,顺序太乱,包内类太繁杂仍然是行之有效的处理方法。

4. 转移
  该手段通常用于将目标转移到其他的目的地,比如基类,子类,外部类,内部类来解决类功能不单一的问题。该手段仍然是以构造职责单一的,高内聚的实体(如类,方法)为目标。

  看我实际项目中的一个场景的简化代码:

public class OuterBox
{
    public void doWork1()
    {
        operateMiddleBox();
        operateInnerBox();
    }
}
public class MiddleBox
{
    public void doWork2()
    {
        operateOuterBox();
        operateInnerBox();
    }
}
public class InnerBox
{
    public void doWork3()
    {
        operateMiddleBox();
    }
}

这个场景是关于界面的,我们的界面套了多层,这里简化成了3层:OuterBox套着MiddleBox,MiddleBox套着InnerBox。这3层之间项目联系,有时需要调用对方的方法。时间一长,这里的结构嵌套的越多,代码就月复杂的,修改起来相当麻烦。如何重构这个场景呢?

  首先,为了解耦合,我尝试了观察者模式,实现起来太麻烦了,事件和挂接的地方太多。

  然后,我尝试了中介者模式,把每个UI互相调用的部分都移到了中介者中,觉得轻松了不少,这是修改后的代码:

public class OuterBox
{
    public void doWork1()
    {
        Mediator.Instance.OperateMiddleBox();
        Mediator.Instance.OperateInnerBox();
    }
}
public class MiddleBox
{
    public void doWork2()
    {
        Mediator.Instance.OperateOuterBox();
        Mediator.Instance.OperateInnerBox();
    }
}
public class InnerBox
{
    public void doWork3()
    {
        Mediator.Instance.OperateMiddleBox();
    }
}
public class Mediator
{
    public void RegisterOuterBox() { }
    public void RegisterMiddleBox() { }
    public void RegisterInnerBox() { }

    public void OperateOuterBox() { }
    public void OperateMiddleBox() { }
    public void OperateInnerBox() { }

    public static Mediator Instance = new Mediator();
}

我个人觉得这次重构还是不错的,重构后各个Box之间解耦了,而且交互的逻辑集中转移到了Mediator中,方便处理。

5. 封装
  不要暴露不必要的细节是封装的目的,封装的结果通常是得到新的函数,类或者组件。封装细节可以体现为:
1). 封装复杂性
  该手段通过封装复杂的难以理解但无法修改,或者不用关注的细节问题,来降低变化的修改难度。

  你是不是遇到过这种代码:

static void Main(string[] args)
{
    // 此处是使用一坨丑陋不堪,复杂,但是永远也不用去修改的代码去与另外一个组件交互
    // 省略100行

    DoSometing();
}

  那100行无比复杂的代码是与别的组件交互的,不出意外从来不用去修改,这个时候把这个代码封装到函数中,是不是更好?

static void Main(string[] args)
{
    Communicate();
    DoSometing();
}

private static void Communicate()
{
    // 此处是使用一坨丑陋不堪,复杂,但是永远也不用去修改的代码去与另外一个组件交互
    // 省略100行
}

2). 封装变化点

  该手段通过封装目标中变化点来稳定目标的职责。这种情况其实是封装不同变化率的一个特例,就是代码一部分不变,一部分会改变。这个例子太多了,大部分的设计模式都是解决这种问题的。此处个人觉得不需要例子了,如果想看的同学搜一下“策略模式”来看一下即可。

6. 抽象
  该手段通过抽取出目标的可复用的部分(可能是逻辑,可能是数据,甚至可能是抽象的行为)转变为接口来重用部分的逻辑。根据重用粒度的大小依次可以分为:抽取成抽象类,抽取接口,抽取方法。看个简化后的例子:

static void Main(string[] args)
{
    A a = new A();
    B b = new B();
    Say(a);
    Say(b);
}

static void Say(A a) { a.Say(); }
static void Say(B b) { b.Say(); }

public class A
{
    public void Say() { }
}
public class B
{
    public void Say() { }
}

  下面是抽取接口后的实现:

static void Main(string[] args)
{
    A a = new A();
    B b = new B();
    Say(a);
    Say(b);
}
static void Say(ISay sayable) { sayable.Say(); }

public interface ISay
{
    public void Say();
}

public class A: ISay
{
    public void Say() { }
}
public class B: ISay
{
    public void Say() { }
}

这个简单的例子也许表达不出大量重复的行为这层意思,但是由于空间有限,也只好如此简化了。

重构的实施
  做好了一切的技术准备,下面就是实施重构了。通常来说,重构总是持续的,小步骤的改动。这些细微的改动,通过单元测试保证重构的正确性,是可控制的。不要总是盲目相信自己的能力,要靠数据说话。
1. 持续重构
  重构是持续进行的,对于代码中的坏味道,只要发现并可以立即处理(指的是有资源去处理,比如人力,时间等),就应该立即去重构。
2. 小步骤重构
  程序员大多数有时是盲目自信的,现实的代码常常是看上去很简单,但是下面的坑却很深,有这种感觉或者经历的同学请“点赞”,呵呵。

  大伙还记得这位为了赢1美元而跳浅水坑的美国青年吗:


  够坑爹吧?所以不要盲目自信,还是重构提倡小步骤进行,以防大步走扯着蛋。
3. 单元测试
  重构是在不改变软件表象的情况下改善代码可读性,扩展性的一项活动,所以需要保证重构前后代码的功能应该是一致的,这个通常是要通过单元测试保证的。
4. 重构的渐进过程
  重构的坏味道有很多,每个人的改法也不尽相同,这个并没有一个统一的标准,所以在实际的工作中,由于存在很多老的代码,所以很多组织实际进行重构的时候,都不会选择重构老的代码,除非这些代码出问题或有其它不得不改的理由。此外由于重构开始的时候培养这个习惯比较困难,所以很多的公司都是优先选择几个比较有代表性的坏味道来要求重构,等大家都习惯这个方式以后再加入其他的重构目标,这样大家有一个接受的过程。

重构的质量

  重构实施完了以后,如何去保证施工的质量是一个问题。通常来说,需要依靠下面几个手段。

1. 技术手段: 单元测试
  这个上面说过了,单元测试是保证重构质量的主要手段,这个对于不同的语言有不同的工具,需要的同学自己google一下吧。
2. 行政手段: 强制Review
  单元测试保证的只是代码功能没变,但是不能检查代码美不美,所以要保证重构质量,还需要一些行政手段和工具。
 1). Review的工具
  这个不用多少了,现代的代码管理工具都带版本对比功能,足够Rreview的时候用了。
 2). 代码检查工具
  这个现在也有不少的工具可以检查代码的风格,比如SourceMonitor,PMD等经典工具。
  很多注重代码质量的公司都会要求每个员工提交代码之前都要运行一下这些工具,保证代码符合开发规范。

时间: 2024-10-21 15:58:49

再论重构的相关文章

优化与重构的思考

看这篇文章:http://www.cnblogs.com/greyzeng/p/4077732.html 对评论引发我的思考. 网上有人说这句话我赞同: 优化和重构是两个概念啊,楼主还是没有搞清楚优化不宜过早主要指的是性能的优化不宜过早,因为很多性能优化其实没有对系统有明显的提升.而重构主要指的是修正代码中不好的味道,提高代码的可读性和可扩展性 优化的确不宜过早,但是重构是应该持续在整个开发过程中的当需求比较稳定的时候,就应该考虑通过重构来整理代码 另外一个人的观点: 我们的做法是,将重构这件事

代码的重构

时间:2015年12月25日15:32:02 1.什么是重构? 重构就是调整程序的代码改善程序的质量.性能,使程序的设计模式和架构更加合理,提高软件的扩展性和维护性. 2.为什么都开发完成了再去重构它?为什么不是开始的时候就设计合理一点? 一个完美的预见未来的设计和可以容纳所有扩展的设计是不存在的,在程序设计的时候编程人员只能从大局方面去设计一个软件,无法做到滴水不漏的设计,而且很多需求是在开完成之后再去更改添加,功能的变化导致设计的调整在所难免,所以"测试在先,持续重构"的习惯十分重

代码重构之谈

何谓重构 重构是: 为了是代码更易于维护和修改,在一系列小的.语义不变的代码转换(即是代码保持正常工作)中重组.重排代码. 重构不只是任意的调整 代码必须仍能正常工作 小步骤仅使语义被保留(即不是一个重大改写) 单元测试来证明代码仍然有效 代码是 更松散的耦合性 功能更聚集的模块 更容易理解的 有很多人所共知的重构技术 你至少应该在Do yourself之前,多多少少熟悉一些 设计重构"条款" 何时重构 你应该重构: 当你看到一个更好的方式来来做同一件事的任何时候 "更好&q

浅谈重构

1.重构概念 在不改变软件的外部行为的基础上,改变软件内部的结构,使其更加易于阅读.易于维护和易于变更.——<重构 改善既有代码的设计> 说白了重构就是一系列的“等量变换”! 2.重构的风险 当我们遇到公司前人留下的烂代码时(很多时候我们也是留下“烂代码”的人),一般都是先开骂,其次就捉摸着干脆重做算了,一般都不愿意修改和重构,我们通常给出的理由是“代码太烂了,还不如重做”,这也就骗骗产品狗和老大罢了,真实的原因只有一个:里边埋坑太多,业务复杂,文档缺失,改坏了要承担后果. 所以重构有风险,重

重构技巧

重构 重构指在不改变程序原有行为的基础上,对既有代码进行修改,以改进其内部结构. 何时应该重构 添加功能时重构,修复bug时重构,代码评审时重构: 何时不应重构 既有代码太过混乱或不能正常运作,项目已近最后期限. 重构的好处 1. 重构可以改进软件设计 项目结束后,后期的bug修复.需求增加会导致代码逐渐腐败变质.冗余.结构混乱.难以理解.难以维护,难以扩展.如要修改某个错误,可能涉及到要修改的代码点很多. 软件开发中唯一保持不变的就是变化.当软件因为需求变更而开始逐渐退化时,运用软件重构改善我

国际化资源管理模块重构总结

从17年末到18年初花了差不多三周的时间,将项目中最重要的模块之一--国际化资源管理,进行了彻底的重构.在掉了无数头发加了好多个晚上的班之后,终于改变了先前一个service解决所有逻辑的臃肿情况,代码的可读性,扩展性,模块功能的扩展性以及可用性等性能获得了很大的提升.我在这次重构中有着许许多多的思考和尝试, 对于一个工作经验仅有一年的人来说是一个不小的挑战.最终项目完成并上线之后,自己对于工作结果还挺满意的,从中也收获了很多很多,不写点总结就有点对不起自己过去三周的辛劳了. 先说说背景.在国际

小波分解和重构

小波变换能够很好地表征一大类以低频信息为主要成分的信号, 小波包变换可以对高频部分提供更精细的分解 详见(http://www.cnblogs.com/welen/articles/5667217.html) 小波分解函数和重构函数的应用和区别 (https://www.baidu.com/link?url=NsLWcGxYPabqB0JEFzkjHzeLmcvGkjDRccPoaD7K0gwo9mrHRDCUgTbV15zT8NKTm9PAuTJ2Hwb3n10PutFRpbOdQRac7XC

从零开始实现放置游戏(八)——实现挂机战斗(6)代码重构

前几张,我们主要实现了升级经验.人物等级属性.地图.地图怪物,这四种配置的增删查改以及Excel导入功能.我们主要以地图怪物为例,因此在文章末尾提供的源代码中只实现了地图怪物这部分的逻辑功能. 如果你照猫画虎,把4种配置功能的逻辑全部实现的话,就会发现,增删查改的代码基本相同,除了SQL语句和模型对象不同,其他地方变化不大. 本章我们利用泛型模板,对整个系统就行重构.在重构结束后,你就会发现写代码简直就是TMD艺术! 后端重构 idlewow-core 我们从最底层开始,首先重构位于core模块

不要做只是重构的架构师

程序员的一条升级路线时,转行做技术专家.架构师. 但要注意架构师并不适合所有人,尤其是下面场景的架构师,如果你的工作只是重构,那需要及早考虑转行了. 相对于架构师的开发工作.研发工作更有趣,更容易得到社会的承认,不论是图形学,还是人工智能,区块链,甚至黑客(网络安全),凭借你的智慧和努力,可以在短时间内取得成就,并达到一个很漂亮的高度.研发方面是拼年轻,智商和体力的工作,有众多的天才少年取得漂亮的成果,每年有大量新的技术突破和文献等着大家研究.你做的每一件事情,都能表现出漂亮的成果,全局光照,计