重构,改变既有代码的设计

158 阅读6分钟

一. 重构的原则

最重要:

1.1 测试

重构最最最重要的是测试,重构的本意是让代码更加的优雅和让人理解,如果引发了更多的bug,那么将得不偿失。

1.2 何为重构

使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

重构必须是小步骤的,并且不能影响系统的功能。有人说他们的代码重构导致几天时间不能使用,那么他们在做的事情不是重构。 重构和优化是不一样的: 1. 重构是为了让代码更容易理解,更易于修改,这可能是代码运行的更慢。 2. 在性能优化时,只会关系怎么让程序运行的更快,可能会使代码更难以理解和维护。

1.3 两顶帽子

Kent Beck 提出了两顶帽子的比喻。即:添加新功能和重构。 1. 添加新功能的时候,不应该修改既有的代码,而是只管添加新功能。 2. 重构时就不应该添加新的东西,只管修改代码结构。 开发过程中需要时刻记住自己头上戴的时什么帽子,避免在重构的时候添加新功能,导致无法测试。

1.4 为何重构

  1. 改进代码的设计
  2. 使软件更容易被人理解。 傻瓜都能写出计算机可以理解的代码,唯有能写出让人类容易理解的代码,才是优秀的程序员。
  3. 帮助找bug
  4. 提高编程速度 所谓磨刀不误砍柴工,代码容易理解了,新增功能总是更快的。

1.5 何时重构

事不过三,三则重构。

1.6 何时不应该重构

  1. 重写比重构还容易,就别重构了,直接重写吧。
  2. 代码隐藏在一个api下面,那么搞不懂的话,就让他继续丑陋吧。

1.7 重构与性能

重构过程的的性能损耗问题,大部分可以忽略不急,如果真的引入了性能问题,那么可以先完成重构,在解决性能问题。 换个思路说,如果重构引发了性能问题,那么重构之后的代码应该更有利于性能调优,毕竟更好理解。 关于性能:实际上只会出现在一小部分代码上,如果你一视同仁的优化所有的代码,在开发的时候因为性能而放弃的代码的可读性,那么90%的优化工作都是白费劲的。 所有在开发的过程中,不对性能做太多的关注,最后进入性能调优的阶段,根据工具(接口监控)来找到需要优化的地方进行调优。不需要做过多的臆测。

1.8 自动化工具

这种就太多了,比如IDEA的提取函数,自动生成变量。最基础的比如:查找并替换。

二. 代码的坏味道

命名

这个太重要了,千万不要使用有歧义的命名,就怕命名是created,功能是delete。这遭大罪。

2.1 重复代码

一旦发现了重复代码或者重复变量,那么提取一下肯定是没错的。

2.2 过长的函数,过长的参数列表

提取。封装。

2.3 全局函数,可变数据。

在js存在很多全局函数,相互污染的问题。 全局的函数,变量千万不要修改,你不知道哪个地方调用了。你修改别人背锅。

2.4 发散式变化

你发现你想要修改一个函数,却必须修改很多不想关的函数。 比如:想要添加一个产品类型,你需要修改产品的增,删,改,查,甚至于排序的函数。

问题原因

这种发散式变化是由于编程的结构不合理,或者"CV"太多了导致的,类的职责过多。

解决办法

提炼类,拆分类的行为。

2.5 霰弹式修改

一旦有业务变化,需要修改多处。很容易造成修改上的遗漏。

解决办法

搬移函数,搬移字段。将相同的业务代码放进同一个类。

依恋情结

一个类多次调用另一个类的方法来获取结果。

public class OrderService{
    public List<Object> findAll(){};
    
    public Object findFirstObject(){};
    
    public void addObject(){};
}


public class CartService{
    public void addObject(){
        ...
        List<Objcet> objects = orderService..findAll();
        Object object = orderService.findFirstObject(objects);
        orderService.addObject(object);
        ....
        
    }
}


像上诉代码,实际上就是为了执行addObject 方法而多次调用了OrderService的方法。这就是一种依恋情结。

问题

  1. 使得代码的职责不再单一。
  2. 如果OrderService的任何一个方法被修改,也会影响到CartService的方法。

解决办法

原则:将总是一起变化的东西放在一块。

将多次调用的代码使用提炼函数提炼成一个新的函数,并且命名来表达这几行代码的意思。 交给调用方调用。

2.6 数据泥团

多个类/方法参数中都有相同的属性,而且这些属性的业务意义也是相同的。

问题

  1. 重复数据。
  2. 涉及到属性的调整,容易遗漏。
  3. 降低代码的阅读效率。
  4. 随着代码的增多容易导致长函数,大类。

解决

提炼类 将关联的属性放在一个新的业务类里面。

2.7 基本类型偏执

对于具有意义的业务概念,不愿意进行建模,而是使用基本类型来表示。 听着有些玄乎,我也懵了下。实际上就是比如坐标:x,y,z 不新建一个类,而是使用基础数据类型进行操作。

public void addPoint(int x,int y,int z){}
public void deletePoint(int x,int y,int z){};

问题

  1. 暴露了较多的细节。
  2. 业务内聚太差,可读性差。
  3. 修改麻烦,万一要加个a....

修改

  1. 对象取代基础类型。
  2. 子类取代类型。
  3. 多态取代表达式
  4. 提炼类、
  5. 引入参数对象。
public Class point{
    int x;
    int y;
    int z;
}


public void addPoint(Point point){}
public void deletePoint(Point point){};

是不是看起来舒服了很多。

2.8 重复的switch

在不同的地方使用了相同的switch 逻辑。

问题

影响可维护性,每当需要增加一个选择分支时,必须找到所有的switch 分支,并且逐一修改。

修改

消除重复的switch

  1. 多态加工厂
  2. 多态加不同的实现类

2.9 循环语句

针对于集合或者数组进行简单的分组,过滤等,采用传统的for循环进行遍历。(这个笔者感觉还好吧) 但是基本功能可以使用stream 更加的简洁。

2.10 冗赘的元素,夸夸其谈通用性

过于追求简洁,追求其通用性了。 一行代码提取一个函数。 在开发初期就各种设计模式全用上,为了炫技各种继承封装。属实没必要。

2.11 临时字段

临时字段必须在函数里面,千万别写在类里。如果两个函数都需要这个临时字段,那么就两个函数都加上。 写在类里面,万一修改了只后被别人使用。。。。