前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >代码重构:类重构的 8 个小技巧

代码重构:类重构的 8 个小技巧

作者头像
phoenix.xiao
发布2021-10-20 10:44:27
6740
发布2021-10-20 10:44:27
举报
文章被收录于专栏:小二十七小二十七

在大多数 OOP 类型的编程语言和面向对象程序设计中,根据业务建模主要有以下几个痛点 🤕:

  1. 对象不可能一开始就设计的合理,好用
  2. 起初就算设计精良,但随着版本迭代,对象的职责也在发生变化
  3. 在迭代中,对象的职责往往会因为承担过多职责,开始变的臃肿不堪(🙈闻到腐烂的味道了~)

那么怎么解决以上的问题?就要运用一些重构的技巧,来让代码结构保持整洁,从而让后续的需求扩展更加稳定

1:合理的分配函数

说明:从 OOP 的角度来考虑,如果函数之间频繁的调用,显然适合放在一个对象当中 使用场景:在 A 对象内,看到它经常调用 B 类的函数,那么你就有必要需要考虑把 B 类的函数搬过来了。

示例一

空说很难理解,我们先展示一段代码,来展示说项重构的手法:

代码语言:javascript
复制
public class Account {
    // 计算透支费用
    double overdraftCharge() {
        if (_type.isPremium()) {
            double result = 10;
            if (_daysOverdrawn > 7) {
                result += (_daysOverdrawn - 7) * 0.85;
            }
            return result;
        } else {
            return _daysOverdrawn * 1.75;
        }
    }

    double bankCharge() {
        double result = 4.5;
        if (_daysOverdrawn > 0) {
            result += overdraftCharge();
        }
        return result;
    }

    // 编码道德 758 条:私有变量应该放在类的底部
    private AccountType _type;
    private int _daysOverdrawn;
}

// 账户类型
class AccountType {
  //... do something 
}

在上面例子 🌰 中,我们看到 Account 显然承担一些不属于它本身的职责,从 _type.isPremium() 的调用方式来看,overdraftCharge 不论从调用,还是命名来看,都更像是 AccountType 的职责,所以我们尝试来搬迁它,最终代码如下:

代码语言:javascript
复制
class AccountType {
    // 从 Account 搬过来了
    double overdraftCharge(Account account) {
        if (isPremium()) {
            double result = 10;
            if (account.getDaysOverdrawn() > 7) {
                result += (account.getDaysOverdrawn() - 7) * 0.85;
            }
            return result;
        } else {
            return account.getDaysOverdrawn() * 1.75;
        }
    }
    // more ...
}

public class Account {
    double bankCharge() {
        double result = 4.5;
        if (_daysOverdrawn > 0) {
            // 还可以根据不同 Account 类型进行扩展
            result += _type.overdraftCharge(this);
        }
        return result;
    }
}

函数 overdraftCharge 搬家后,我们有几个可见的好处如下:

  1. 可以根据不同 Account 类型,计算不同结果,程序更灵活,调用方无需知道计算细节
  2. 避免类似 _type.isPremium() 的函数调用出现,看上去更合理
总结

通过 示例一 我们可以得出总结:

  1. 如果一个对象有太多行为和另一个对象耦合,那么就要考虑帮它搬家
  2. 只要是合理的分配函数,就可以使系统结构,对象本身的行为更加合理

2:合理分配字段

说明:这里的思路和 合理的分配函数 非常的相似,只是主体由 函数 替换为的 字段 使用场景:当 A 类的某一个字段频繁的被 B 类使用,那么就要考虑把它搬迁放到 B 类中

示例一

这里比较简单,能理解上面的函数分配,也就能理解这里,我们看一段简单的示例就好,还是以刚才的 Account 类为例子:

代码语言:javascript
复制
public class Account {
    // 结算日息
    double interestForAmountDays (double amount,int days){
        return _interestRate * amount * days / 365;
    }
    private AccountType _type;
    // 利率 %
    private int _interestRate;
}

// 账户类型
class AccountType {
    // do something....
}

从示例上看,_interestRate 字段显然更适合放在 AccountType,我们做一次字段搬迁,搬完后代码如下:

代码语言:javascript
复制
public class Account {
    double interestForAmountDays (double amount,int days){
        return _type.getInterestRate() * amount * days / 365;
    }
    private AccountType _type;
}

// 账户类型
class AccountType {
    // 利率 %
    private int _interestRate;

    public int getInterestRate() {
        return _interestRate;
    }
}

主要做有 2 个好处如下:

  1. 只需引入 AccountType 即可,无需再重复引入 _interestRate 字段
  2. AccountType 可以根据不同的类型,设置不同的 _interestRate 利率,代码更灵活
总结

不管是搬迁函数,还是搬迁字段也好,它们都是在不断重构类的职责和属性,程序会跟随需求不断变化,没有任何设计是可以保持一成不变的,所以这里的重构方法,不需要等到特定的时间和特定的规划再去进行,重构应该是融入在日常开发当中,随时随地都在进行的

3:拆解大类

说明:随着需求越来越多,原来设计的对象承担的职责也会不断的增多(方法,属性等……),如果不加以使用重构的手段来控制对象的边界(职责,功能),那么代码最终就会变得过于复杂,难以阅读和理解,最终演化成技术债,代码腐烂,从而导致项目最终的失败。 使用场景:当一个类变的过于庞大,并且承担很多不属于它的职责(通过类名来辨识)的时候,创建新类的分担它的工作

示例一

这里的 Person 承担的过多的职责,我们把不属于它职责范围的函数抽离出来,从而保证对象上下文的清晰,拆解过程如下:

实际代码如下:

代码语言:javascript
复制
public class Person {
    
    private String name;
    private String sex;
    private String age;
    private String officeAreaCode;
    private String officeNumber;

    //... 省略 get/set 代码...
}

Person 的类图看起来是这样的:

显然 Person 做了很多不属于自己的事情(现实情况往往要惨的多),想要分解的 Person 的话,我们可以这样做:

  1. 识别 Person 的职责,然后创建一个 TelePhoneNumber 对象进行分担
  2. 将关联字段和函数迁移到 TelePhoneNumber 类中
  3. 进行单元测试

当我们拆解后,新建的 TelePhoneNumber 类代码如下:

代码语言:javascript
复制
public class TelePhoneNumber {

    private String officeAreaCode;
    private String officeNumber;

    //... 省略 get/set 代码...
}

这时候 Person 对象的职责就简单和清晰很多了,对象结构如下:

TelePhoneNumber 对接结构图如下:

总结

拆解大类,是常见的重构技术手段,其最终目的都是保证每个对象的大小,职责都趋向合理。就像我们工作中如果有一个人太忙,那么就找一个人帮他分担就好了。

4:合并小类

说明:这里是和 拆解大类 逻辑完全相反的的技巧 说用场景:如果一个类没有做太多的事情,就要考虑把它和相似的类合并在一起,这样做的目的是:

  • 尽可能保证和控制每个类的职责在一个合理的范围之内
  • 类过大就使用 拆解大类 的手法
  • 类太小就使用 合并小类 的手法
示例一

我们还是用上面的 PersonTelePhoneNumber 类举例,合并过程如下:

上图可以看到 Person 在本身属性很少的情况下,又拆分了 TelePhoneNumber 类,这属于典型的过度拆分了。就需要使用合手法,将散乱在各地临散的类进行合并。代码如下:

代码语言:javascript
复制
class Person {

    // Person 职责很少,没必要拆解为 2 个类
    private String name;
    private String age;

    // ...
}

class TelePhoneNumber {

    private String phoneNumber;

    // ...
}

我们把 PersonTelePhoneNumber 进行合并,然后可以移除 TelePhoneNumberPerson 的最终代码如下:

代码语言:javascript
复制
public class Person {

    // Person 看上去更加合理了
    private String name;
    private String age;
    private String phoneNumber;

    // ... do some 
}
总结

如果类很小,那么就要考虑将它合并,从而让临近的类的职责更加合理

5:隐藏委托关系

说明:委托关系是指,必须通过 A 类才能调用另一个 B 类对象 使用场景:当只有个别函数需要通过关联方式获取的时候,使用隐藏委托模式,让调用关系更加简单

示例一

我们先看看委托模式的代码,我们使用一个 PersonDepartment 类来举例,代码如下:

代码语言:javascript
复制
public class Person {

    Department department;

    // 获取所属部门
    public Department getDepartment() {
        return department;
    }
}

class Department {

    private String chargeCode;
    private Person manage;

    public Department(Person manage) {
        this.manage = manage;
    }

    // 需要通过 Department 才能找到部门 Manage
    public Person getManage() {
        return manage;
    }
}

以上代码设计看上去没有问题,但是当我想要获取某一个 Person 对象的所属经理的时候,我就需要先获取 PersonDepartment 对象,然后在 Department 中才能调用 getManager() 函数,代码看起来就会很别扭,如下:

代码语言:javascript
复制
Person john = new Person();
// 委托模式:需要通过 Department 委托对象才能获取 Person 想要的数据
Person manage = john.getDepartment().getManage();

这样的类结构设计会存在以下几个问题:

  1. 违背 OOP 的封装原则,封装的原则意味类尽可能的少对外的暴露信息
  2. 调用方需要去理解 PersonDepartment 的依赖关系,才能拿到 getManage() 信息
  3. 如果委托关系发生变化,那么调用方也需要修改代码

我们可以在 Person 中隐藏这层委托关系,从而让 Person 可以直接获取 getManage(),我们在 Person 加入以下代码:

代码语言:javascript
复制
public class Person {

    Department department;

    public Person getManage() {
        return department.getManage();
    }
}

这里看到 Person 有两处修改:

  1. 隐藏 department.getManage() 委托关系
  2. 移除 getDepartment() 函数

最终获取 Person 的 getManage() 显示更加直接,代码如下:

代码语言:javascript
复制
// 然后改用新函数获取 Person 的 Manage 
Person manage = john.getManage();
总结

如果 只有少数函数 需要依赖委托关系获取的时候,可以使用 隐藏委托关系 的重构手法来让类关系和调用变的简单。

6:移除中间人

说明:这是 隐藏委托关系 这个技巧的反例 使用场景:当有过多函数需要委托的时候,不建议继续隐藏委托模式,直接让调用方调用目标类,代码反而更简洁

示例一

我们上面的代码通过在 Person 建立隐藏的委托模式,如下:

代码语言:javascript
复制
public Person getManage() {
    return department.getManage();
}

通过这种方式来简化对象关系的调用,但是再想想,在后续的需求迭代中,Department 也在不断增加特性,如果 Department 每个新增的特性,都需要通过 Person 来进行委托的话,那么代码看起来就像这样:

代码语言:javascript
复制
class Department {

    private String chargeCode;
    private Person manage;
    // 部门不断发展,新增不少角色
    private Person captain;
    private Person groupLeader;

    // 省略获取部门角色的方法....
}

所以每当 Department 增加角色,Person 都要修改代码来继续隐藏委托模式,Person 代码如下:

代码语言:javascript
复制
class Person {

    Department department;

    public Person getManage() {
        return department.getManage();
    }
    
    // 兼容 Department 新增的委托模式
    public Person getCaptain() {
        return department.getCaptain();
    }

    public Person getGroupLeader() {
        return department.getGroupLeader();
    }
}

所以 当有过多函数委托 的时候,倒不如移除 Person 这个简单的中间人,让对象直接调用 Department,区别如下:

代码语言:javascript
复制
// 通过委托模式获取
Person manage = john.getManage();
Person captain = john.getCaptain();
Person groupLeader = john.getGroupLeader();

// 移除中间人获取
Person manage = john.getDepartment().getManage();
Person captain = john.getDepartment().getCaptain();
Person groupLeader = john.getDepartment().getGroupLeader();

这样做的好处就是 Person 的代码量大大减少,移除中间人后的 Person 类:

代码语言:javascript
复制
public class Person {
    
    // 当委托工作变的非常重的时候,解除委托关系,可以让 Person 获得解放
    Department department;

    public Department getDepartment() {
        return department;
    }
}
总结
  • 当需要委托的特性越多,隐藏委托模式就显得没有必要,直接调用提供人代码会更简单
  • 如果只有简单的委托特性,建议使用隐藏委托关系

7:扩展工具类

使用场景:当系统工具库无法满足你需求的时候,但是你又无法修改它(例如 Date 类),那么你可以封装和扩展它,来让它具备你需要的新特性。

示例一

例如我们有一段处理时间的函数,Date 工具类似乎并没有提供这样的方法,我们自己实现了,代码如下:

代码语言:javascript
复制
Date previousEnd = new Date();
Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);

以上 newStart 实现的方式有以下几个问题:

  • 表达式难以阅读
  • 无法复用

我们使用 扩展工具类 的方式,可以把程序重构为以下这样:

代码语言:javascript
复制
Date previousEnd = new Date();
Date newStart = nextDay(previousEnd);

// 提炼一个函数,作为 Date 类的扩展函数方法
public static Date nextDay(Date arg) {
    return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
总结
  • 通过扩展工具类,为工具类增强更多的功能,从而满足业务的需求
  • 如果有可能(获取修改工具类的权限),那么可以考虑把扩展函数搬到工具类内部,让更多人复用

8:增强工具类

使用场景:当你无法修改工具类(通常都无法修改),并且只有个别函数需要扩展的时候,那么使用 扩展工具类 没有任何问题,只要少量的代码就可以满足功能需求,但是这种扩展是一次性的,例如扩展的 nextDay() 函数,无法被其他类复用。所以我们需要用增强工具类来解决这个问题

示例一

我们还是使用上面的 nextDay() 扩展函数来举例,假如这个函数会经常被用到,那么我们就需要增强它,做法如下:

  1. 新建一个扩展类,然后继承工具类(例如 Date
  2. 在扩展类内实现扩展函数,例如 nextDay()

代码如下:

代码语言:javascript
复制
public class StrongDate extends Date {
    // 提炼一个函数,作为 Date 类的扩展函数方法
    public static Date nextDay(Date arg) {
        return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
    }

    // ... 这里还可以做更多扩展
}

调用方使用方式:

代码语言:javascript
复制
Date previousEnd = new Date();
Date newStart = StrongDate.nextDay(previousEnd);
总结
  • 工具类的扩展函数会经常被复用,建议使用 增强工具类 的方式重构显然更加的合适
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小二十七 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1:合理的分配函数
    • 示例一
      • 总结
      • 2:合理分配字段
        • 示例一
          • 总结
          • 3:拆解大类
            • 示例一
              • 总结
              • 4:合并小类
                • 示例一
                  • 总结
                  • 5:隐藏委托关系
                    • 示例一
                      • 总结
                      • 6:移除中间人
                        • 示例一
                          • 总结
                          • 7:扩展工具类
                            • 示例一
                              • 总结
                              • 8:增强工具类
                                • 示例一
                                  • 总结
                                  领券
                                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档