1

重构

 1 year ago
source link: https://mengtnt.com/2023/03/20/refactor.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

重构

03月 20日, 2023 3 minute read

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

如何重构代码

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

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

简单功能的开发

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

MemberListViewController
-MemberListView view
-loadView()
MemberListView
-renderView(List data)
MemberData
-String name
-Int status

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

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 ,专门处理数据获取,这样可以减少单个类的大小,方便阅读。

MemberData
-String name
-Int status
MemberListDataInteractor
- List dataList
-requestData(callback(List list)
MemberListViewController
-MemberListView View
-MemberListDataInteractor interactor
-loadView()
MemberListView
-View listView
-renderView(List data)

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

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 就负责数据组装。我们来看下这种结构。

MemberData
-String name
-Int status
MemberViewModel
-List dataList
MemberListDataPresentor
-MemberListDataInteractor interactor
-bind(MemberViewModel model,MemberListView view)
MemberListViewController
-MemberListView View
-MemberListDataPresentor presentor
-loadView()
MemberListView
-View listView
-renderView(MemberViewModel data)

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


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

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

大量的耦合

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

MemberListDataPresentor
ColdObserval
MemberListDataInteractor
MemberListViewController

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

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

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

扩展新功能

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

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

MemberListViewPlugin
-View subView
-loadView()
MemberListDataInteractorPlugin
-List dataList
-requestData()
MemberListDataPresentorPlugin
-List dataList
-bindData()
MemberListDataPresentor
MemberListDataInteractor
MemberListView

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

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

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

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

重构的回顾

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

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

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

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

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

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

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

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

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK