重构


重构是一个老生常谈的问题,并且也有大量的书籍介绍重构的方法。那如何重构代码?如何保证重构的代码不出问题?重构后的代码如何防止腐化?我先贴出来 ChatGPT 如何回答的,来感受下她的强大。

如何重构代码

  1. 提取方法:如果你有一段过长或过于复杂的代码,可以将其提取为独立的方法,使其更易读且可重复使用。
  2. 重命名变量和方法:使用描述性的名称命名变量和方法,使代码更容易理解。
  3. 移除重复代码:查找重复的代码块,并将其提取为可重复使用的方法。
  4. 简化条件语句:如果条件语句过于复杂,将其分解为更小、更易处理的部分。
  5. 使用更好的数据结构:如果使用的数据结构效率低下或不能满足需求,考虑使用更好的数据结构。
  6. 将代码分解成模块:将代码分解成具有特定职责的较小模块,每个模块都有自己的一组测试。
  7. 移除无用代码:查找不再使用的代码,并将其删除以简化代码库并使其更易维护。 这些只是重构代码的几个示例。关键在于不断寻找改进代码的方法,使其随着时间推移更易维护。

ChatGPT 上的回答和很多书籍和教程中讲的大同小异,其实有时候描述文字会很抽象。这里我拿自己经历的一个例子说明。当然不是 ChatGPT 给的例子😂。

简单功能的开发

这是很常见的成员列表页面展示功能。我们先看第一个版本的写法。


classDiagram
class MemberListViewController {
  -MemberListView view
  -loadView()
}

class MemberListView{ 
  -renderView(List<MemberData> data)
}

class MemberData {
  -String name
  -Int status
}

MemberListViewController *-- MemberListView: 1..1

MemberListView ..> MemberData: 1..*

下面用伪代码的方式展示下,调用的流程。

MemberListViewController::loadView(){
  RequestData(){ data in
    List<MemberData> dataList = List<MemberData>(data)
    view.render(dataList)
  }
}
MemberListViewController *vc = MemberListViewController()
vc.loadView()

其实这种 UI 开发的逻辑再简单不过了,获取数据然后把数据渲染到相应的 UI 组件上。我们下面来看下功能的发展过程。

臃肿类的形成

随着此功能设计了不同的应用场景,例如从场景A进去应该获取服务A的数据然后展现,从场景B进去获取服务B的数据… 先来看如果按照原来的逻辑,需要写类似下面的大量代码。


switch condition {
    case A:
      RequestAData(){ data in
        List<MemberData> dataList = List<MemberData>(data)
        view.render(dataList) 
      }
    case B:
      RequestBData(){ data in
        List<MemberData> dataList = List<MemberData>(data)
        view.render(dataList)
      }
  }

这时候你就会发现 Switch Case 中越来越多的数据请求和渲染代码。随着应用的场景越来越多,会发现 MemberListViewController 类变的越来越大。这时候如何重构?

这里就要用到重构的一个重要原则:职责单一。优化代码就是把 MemberListViewController 大的类拆分,此类负责渲染视图,不要再负责数据请求了,网络请求相似的功能内聚到另外一个类中MemberListDataInteractor ,专门处理数据获取,这样可以减少单个类的大小,方便阅读。

classDiagram

class MemberData {
  -String name
  -Int status
}

class MemberListDataInteractor { 
  - List<MemberData> dataList 
  - requestData(callback(List<MemberData> list))
}

class MemberListViewController {
  -MemberListView View
  -MemberListDataInteractor interactor
  -loadView()
}

class MemberListView{ 
  -View listView 
  -renderView(List<MemberData> data)
}

MemberListViewController *-- MemberListView: 1..1

MemberListViewController *-- MemberListDataInteractor: 1..1

MemberListDataInteractor *-- MemberData: 1..*

这个时候的调用过程可能是这样的。如下的伪代码:

MemberListViewController::loadView() {
  interactor.requestData() { List<MemberData> list in
    MemberListView.renderView(list)
  }
}
MemberListViewController::loadView() {
  interactor.requestData() { List<MemberData> list in
    MemberListView.renderView(list)
  }
}
MemberListViewController *vc = MemberListViewController()
vc.loadView()

这个其实就是典型的 MVC 的结构,视图渲染、数据模型、模型组装分开,核心就是内聚不同的功能。有时候想一想阅读代码就跟我们看文章一样,如果不分段落,直接一个1000字的段落,相信很多人都不愿意看。职责单一原则的目标就是让人更愿意阅读你的代码,第一眼不至于被吓到。

重复代码的优化

随着功能的演化,用户的界面越来越复杂,需要大量的数据频繁的渲染到视图上。就会发现有大量视图渲染组装的代码。这类代码的特点是相似度很高,只是渲染到不同的视图上而已。这时候重构的另一个重要原则DRY,不要写重复的代码,就发挥作用了,我们只需要把重复的代码再抽象出一层,就可以减少大量相似的代码。 这个类可以叫做 MemberListDataPresentor,专门用来组装视图。只要简单的增加 MemberViewModel 这种数据结构映射到 MemberListView 这种视图上,然后 MemberListDataPresentor 就负责数据组装。我们来看下这种结构。

classDiagram

class MemberData {
  -String name
  -Int status
}

class MemberViewModel {
  -List<MemberData> dataList
}

class MemberListDataPresentor { 
  -MemberListDataInteractor interactor 
  - bind(MemberViewModel model,MemberListView view)
}

class MemberListViewController {
  -MemberListView View
  -MemberListDataPresentor presentor
  - loadView()
}

class MemberListView{ 
  -View listView 
  -renderView(MemberViewModel data)
}

MemberListDataPresentor *-- MemberViewModel : 1..1

MemberViewModel *-- MemberData: 1..*

MemberListView *-- MemberViewModel: 1..1

MemberListViewController *-- MemberListDataPresentor: 1..1


这样使用的时候只要 MemberListDataPresentor 组装好数据,就不必再调用渲染了,可以直接通过 MemberViewModel 映射到 MemberListView 上了。可以看到下面伪代码的调用过程。


interactor.loadAllUserData() { List<MemberData> list in
  presentor.bindData(list,listView)
}

这其实就是 MVVM 的架构进化的过程。DRY 原则就是不要写重复的代码,就像写文章一样,千篇一律的文字没人愿意看一个道理。我们继续来看这个功能发展的情况。

大量的耦合

随着功能越来越复杂,会发现 MemberListDataPresentor 这个类调用的接口会越来越多,既需要调用 MemberListDataInteractor 大量的请求接口来获取数据,同时也需要组装各种 Model 的数据,势必会造成大量接口暴露。这种各种复杂关系的调用,使阅读起越来越难。这时候软件工程的一个最好有的原则:任何工程问题,都可以通过增加一个中间层来解决。我们这里就需要增加工具类解耦,解耦的本质通过工具类拆分。使得依赖关系变为如下结构。

classDiagram

MemberListDataPresentor ..> ColdObserval

MemberListDataInteractor ..> ColdObserval

MemberListViewController ..> ColdObserval

有大量接口依赖的类,互相直接调用就可以用类似的方式。

presentor.addObserval() { result in
    // bind data
}
interactor.postMessage();

其实这就是使用观察者模式来拆分。观察者模式的好处就是解耦,减少接口依赖,这样我们想要定义不同的 presentor 类时,也不必依赖各种具体的 interactor,只需要监听消息即可。例如 WebRTC 中重要的线程工具类 TaskQueue ,就是一个很好的解耦的拆分的工具。把编解码,采集,传输很好的解耦分离开。有了这个铺垫,我们最后来看下如何扩展功能。

扩展新功能

试想下我们需要在 MemberListViewController 视图上增加新功能,不仅仅是显示 MemberListView 这一种视图,还能插入各种其他业务视图。

正是因为有了工具类的拆分,这样所有的类都没有任何的依赖,不用暴露新接口,扩展就很容易了。具体可以通过代理模式来横向拆分。定义要扩展的代理类 Plugin,我们新功能只要实现 Plugin 定义的接口函数,就可以横向扩展所有的功能。我们看下类结构。

classDiagram

class MemberListViewPlugin { 
  -View subView 
  -loadView()
}

class MemberListDataInteractorPlugin { 
  -List<MemberData> dataList 
  -requestData()
}

class MemberListDataPresentorPlugin { 
  -List<MemberData> dataList 
  -bindData()
}

MemberListDataPresentor *-- MemberListDataPresentorPlugin : 1..*

MemberListDataInteractor *-- MemberListDataInteractorPlugin : 1..*

MemberListView *-- MemberListViewPlugin : 1..*

然后我们的插件调用过程就如下:

MemberListDataPresentorPlugin *presentorPlugin = MemberListDataPresentorPlugin()
presentor.registPlugin(presentorPlugin);
MemberListDataInteractorPlugin *interactorPlugin = MemberListDataInteractorPlugin()
interactor.registPlugin(interactorPlugin)

这样每次增加新功能时,不需要更改原来的 MemberListDataPresentorMemberListDataInteractor 任何代码,只需要添加插件的实现就可以了。这其实就是很多软件插件的架构模式。

我们从上面这个例子里可以看到代码的演变,如何从 MVC 到 MVVM 再到最后插件化,这些过程让代码结构更加清晰容易阅读,防止代码腐化。

重构的回顾

我们总结下上述的重构过程中几个关键的节点。

  1. 当你发现一个类越来越大。

    超过了1000行代码了,一定是需要拆分功能了。

  2. 当你开发功能时,发现需要原来的类大量更改接口才能实现,我们就需要用工具类来解耦。
  3. 当添加新功能时,需要频繁更改一个类时。

    这时候就需要用代理模式的插件来扩展你的类,这样就可以避免大量的修改逻辑,保证代码稳定性。例如我们经常看到的一些可插拔的插件系统,都是通过这种方式实现的。

  4. 性能优化的代码太多时尤其注意,尽量不要暴露出来,因为性能优化的代码往往可读性比较差。

    对于优化性能的代码,重构的时候我这里,尽量封装成内部的函数,而不要暴露给外部使用。比如定义了一个 Cache 资源的类,为了优化内存,这种最好不要把API暴露在外面,在内部消化最好。

  5. 发现无用的功能代码及时的删除,防止进一步防止腐化

    不及时删除的后果,会发现新功能调用以前移除的功能类的方法,这时候你想删除老功能代码时,你会发现欲哭无泪。

对比 ChatGPT 重构的总结,总体原则一样,但是会更加具体一点。最后我想讲下关于重构代码时,如何保证稳定性的一些原则。我总结起来就是小步快走,保证稳定。在重构的过程中允许一定的冗余代码,增加灰度能力,当发现问题时可以及时回滚,等待重构的代码测试没问题了再删除。

很多优秀的开源项目的代码不仅对代码的性能,也对代码的质量和可维护性要求很高,阅读起来就像欣赏优美的诗篇。屎山一样的代码从来不会有伟大的作品。我相信每个优秀的程序员,都不愿意把自己的代码变成屎山,但是罗马也不是一天能建成的,学会重构是必备的技能。

如果你喜欢这篇文章,谢谢你的赞赏

图3

如有疑问请联系我