重构

技术是为了更好得解决变动的需求,要做到这一点,必须写出优雅的代码。这需要重构,

  1. 这是一个开发人员从初级到中级的必经之路
  2. 重构是目的,设计模式是到达的方式,这需要一直的训练

重构是什么

  1. 什么是重构,调整软件内部结构,提高理解性,降低修改成本;但不改变软件可观察行为(功能)
  2. 何时重构,不用专门拨时间进行重构,应该是随时随地进行的。当想为程序添加特性,但代码结构使你不是很方便达成目的,可考虑先重构。应该好程序应满足下面条件
    • 容易阅读
    • 所有逻辑都只在唯一地址指定
    • 新的改动不会危及现有行为
    • 尽可能简单表达条件逻辑
  3. 重构难题,数据库、修改接口
  4. 重构和性能,重构代码有时会为了改善结构而牺牲性能,尽管如此你也不必要求代码处处高性能。一视同仁优化所有代码,90%工作是白费的,你优化的代码很少被运行。所以,编写良好代码结构,不对性能投以特别的关注,直至进入性能优化阶段(后期阶段)。

代码的坏味道

  1. 重复代码,提取方法,把方法提取到父类
  2. 过长函数,提取方法
    • 难点:怎样解决参数过多的问题
  3. 过大的类,提取类或者子类
  4. 过长参数列
  5. 由于需求变动,导致代码块多处逻辑都修改(不符合逻辑在唯一处)。
  6. 依赖情结,类中某方法,太过依赖其他类数据,move it
  7. 数据泥团,提取出类
  8. 令人迷惑的暂时字段
  9. 过多的注释,好代码,注释是多余的,写注释前请先尝试重构。

提炼函数(Extract Method)

  1. extract Method
    • 难点:源函数参数,局部变量(个数越多,越难extract)
    • 临时变量
  2. Inline Method,不需要extract 函数,提取了之后感觉这一中间层很别扭。
    • 函数体来代替 函数名
  3. 查询函数代替临时变量:临时变量局限于某个函数,函数可以在对象整个生命周期中使用。
  4. 内联临时变量,引入解释临时变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # 内联临时变量
    let xx = aaa.b;
    if(xx >21){
    dosomething;
    }

    <!-- 之后 -->
    if(aaa.b >21){
    dosomething;
    }


    # 引入解释临时变量
    if(aaa.bb.c.indexof('xx') > -1 && ddd.ee.f > -1){
    dosomething;
    }

    <!-- 之后 -->
    const a = aaa.bb.c.indexof('xx') > -1;
    const b = ddd.ee.f > -1;
    if(a && b){
    dosomething;
    }
  5. 分解临时变量,当一个临时变量被赋值了两次,可以引入两个独立 临时变量

    • i = i + xx 形式不要拆解
  6. 拒绝对函数参数赋值
    • 引发很多,函数引用传参的问题,java 更多是提倡按值传递。
    • js 动态语言,很多都是对象,注意 深拷贝 的问题
  7. 类来代替函数(replace method with Object)
    • 当一个函数局部变量很多,逻辑复杂,extract 困难。就把函数升级为类
  8. 替换算法,简单逻辑替换复杂逻辑

引入解释临时变量,和抽取函数选择哪一种。

对象之间搬移特性

  1. 搬移函数、搬移字段,如果一类里面包含太多行为,或者里面的函数和其他类耦合高,被调用次数比驻类(宿主类)还多。可以考虑搬移函数,搬移之前,往往先进行搬移字段,如果字段被驻类很多函数调用,先进行自我封装(self-Encapsulation)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    class A ...
    private B b;
    private double xx;

    double xxx1(double x1, int x2){
    return xx * x1 * x2;
    }


    double xxx2(double x1, int x2){
    return xx * x1 * x2;
    }


    # 将 xx 般至B里面
    <!-- A 进行自我封装 -->
    class A ...
    private B b;
    private double xx;

    double xxx1(double x1, int x2){
    return xx * x1 * x2;
    }


    double xxx2(double x1, int x2){
    return xx * x1 * x2;
    }

    private void setxx(double arg){
    xx = arg;
    }

    private double getxx(){
    return xx
    }


    <!-- xx 移到B -->
    class B ...
    private double xx

    void setxx(double arg){
    xx = arg;
    }

    double getxx(){
    return xx;
    }

    <!-- 这样,就算A中有很多对xx 的引用,就只要修改-->
    private void setxx(double arg){
    // xx = arg;
    b.setxx(arg);
    }

    private double getxx(){
    // return xx
    return b.getxx();
    }

其实自我封装,也是多加了一层。一层函数调用

  1. 提炼类,类内联化(Inline Class),一个类太大,抽象得不合理,应该提炼。内联是指一个类没有承担足够多的责任,不应该单独存在,将这个类内联到其他类中。
  2. 隐藏委托关系,移除中间人,如下,B把责任全部都委托给了C,如果A 完全不知道C 的存在,则称为隐藏委托关系。有时候抽象的目的就在于此,减少耦合。但如果B 这个中间人(Middle Man)越来越大时,干脆就直接移出中间人B,直接调用C

    1
    # A调用 B,B再调用 C获得结果后,给 A返回
  3. 引入本地函数,或者本地扩展,当某语言的标注库函数提供的功能无法满足开发需求,可以引入本地函数,相当于java 函数重写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 比如微信小程序request 封装一下
    function request(option){
    // dosometing();
    wx.request(option);
    }

    # 调用自己写的request 函数
    requset({
    url: url,
    method: 'GET',
    success: function(res) {
    // xxx
    },
    fail: function(err) {
    // xxx
    }
    })

重新组织数据

  1. 自封装字段, 在类里面操作字段用函数,不直接访问。是否封装,一直以来存在争议。建议先不封装,如后面不满足需求时再封装,就像将字段搬移到其他类前,先进行字段自我封装一样。
  2. 对象取代数值、对象取代数组
  3. 值对象改为引用对象、引用改为值
  4. 两个类之间的双向关联,就像双向绑定一样,在js 动态语言实现更方便,无需提前说明字段,直接添加。用过
    • layaAir 游戏里面,物理引擎body 和现实画面物体的坐标进行帧循环同步
  5. 配置常量代替魔法数字
  6. 封装字段,类里面的字段应该设置为private,并提供set/get 方法给外部访问。否则,字段被外部操作,类却不知道(因为直接访问字段)。这样做把数据和行为分开了。
  7. 数据类取代记录,把数据归类,抽到类中,该类没有只有数据,没有行为
  8. 类型码的取代,如 switch/case
    • 类取代类型码
    • 子类取代类型码
    • state/strategy(设计模式) 取代类型码

重新组织数据的方法,其中4、5、6、7 比较常用,5、7对数据归类,抽取配置是必须要求的。对类型码的处理,等涉及了再熟悉。

简化条件表达式

  1. 分解、合并条件表达式,比如if 成立条件判断涉及多种情况,但是为一个目的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 分解条件表达式
    // 是否不是夏天
    if(date.before(SUMMER_START) || date.after(SUMMER_END)){
    ......
    }
    // 可以抽取条件判断为一个函数,签名为 notSummer(date)

    # 合并条件表达式
    double disabilityAmount(){
    if(xx1) return 0;
    if(xx2) return 0;
    if(xx3) return 0;
    }

    // 可以合并上面判断逻辑
  2. 合并重复的条件片段,比如if/else 内都含一段逻辑,可以提取至外面

  3. break/return 代替控制标记,结构化编程原则指明,每一个子程序只能有一个入口一个出口(单一入口原则),选择加入控制标记,降低可读性。因此编程语言提供,break/continue/return
  4. 卫语句取代嵌套条件表达式
  5. 多态取代条件表达式
  6. 引入null 对象

简化函数调用

  1. 修改函数名,可能一开始没法取一个适当的名字
  2. 添加、移除参数
  3. 将查询、修改函数分离,每个函数只做一件事
  4. 保持对象完整,将要传递的多个参数替换成一个对象,也是引入参数对象
  5. 以工厂函数代替构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    class Employee{
    private int _type;
    static final int ENGINEER = 0;
    static final int SALESMAN = 0;
    static final int MANAGER = 0;

    Employee(int type){
    _type = type;
    }
    }

    # 改为 工厂函数
    static Employee create(int type){
    return new Employee(type);
    }

    # 更近一步
    // 应该吧 engine/salesman/mannager 抽出成子类
    static Employee create(int type){
    switch(type){
    case ENGINEER:
    return new Engineer();
    case SALESMAN:
    return new Salesman();
    case MANAGER:
    return new Mannager();
    default:
    throw new IllegalArgumentException("xx");
    }
    }

    # 简化switch
    static Employee create(String name){
    try{
    return (Employee)Class.forName(name).newInstance();
    }catch(Exception e){
    throw new IllegalArgumentException("xxx");
    }
    }
  6. 封装向下转型,java 5 模板机制,模板类?

欢迎大家给我留言,提建议,指出错误,一起讨论学习技术的感受!

-------------本文结束感谢您的阅读-------------
Donate comment here