从重构到吐血 - 我是如何删掉 6 万行代码并且不删减原有功能的

9,531 阅读7分钟

原文发表在 近期重构工作的一点收获

以前做个人项目的时候,简历上写过重构了三次,后来在扇贝面试的时候,面试官问三次分别重构了什么,仔细想想那时候的重构并不算重构,第一次是 UI 改版,但是项目结构没什么大的变化,第二次是整体迁移到了 CocoaPods,这次勉强能算重构,第三次仅仅是变量名方法名空行这些地方的风格统一而已。

在现在工作的地方,接手这些项目之后,主要工作做的是重构,而重构工作,本来想写成一行,结果发现挺多,我列个列表吧:

  • 删除没用到的第三方库
  • 删除不合理的第三方库,使用系统自带的或者自己造轮子
  • 删除定义好但是没有用到的变量
  • 删除 import 进来但是没有用到的头文件
  • 删除更旧项目留下来的用不到的逻辑
  • Controller 层不合理的层级结构重构,无用代码清理
  • View 层不合理的结构重构
  • Service 层冗余的写法重构
  • Model 层不合理的写法重构
  • 拆开不合理的耦合
  • 耦合一个类别的模块
  • 修复了多处内存泄露
  • 修复了多处循环引用
  • 优化编译速度
  • 消除项目中的 warning

关于删除代码,在某个项目里,Pods 文件夹那些第三方库的代码删了 9 万多行(那个目录没有被 git ignore 掉),项目里面删除了大约 4 万行,其中大量代码是该项目之前的项目里面留下来的东西,只不过没人清理。在删了 4 万行之后,程序仍然能完整的跑。

接下来是做了部分重构,把一些第三方库删掉,自己造轮子,在这个过程中,累计删除了 1.2 万行代码,增加了 1100 行左右。

整个重构工作下来,编译速度从 2-3 分钟减小到了 40 多秒,warning 从 70 多减少到了 0,第三方库的数量从 51 个减少到了 13 个,安装包从 22.1M 减小到了 3.7M,功能反而比之前还要多。

内存泄露方面,因为没人在意这件事,有一个功能使用一次,就会增加好几百 kb 内存,那部分代码是用 C 写的,所以及时释放内存,并且优化下调用方式,内存泄露的问题就完美解决。

循环引用方面,是因为有人把 Xcode 的 warning 关了,后来打开的时候,发现了四个循环引用 + 几十个 warning,并且测试过程中发现那个页面不断打开退出,程序会 crash。

笼统的就这么多,我再来分享几个具体的点。

避免滥用单例

单例用着确实爽,但是程序退出之前是不会被回收的,如果是整个生命周期基本用不到的模块做成单例,那么只会浪费内存而已。

避免无用的层级

具体是什么意思呢,用网络层举例子,封装 AFN 是一层,API 的后缀字符串放一层,构造请求放一层,OAuth 授权放一层,发普通请求又是一层。增加一个 API,至少要修改 6 个文件。写着也很痛苦,看着也很痛苦啊。

网络层就只设计一层,封装 AFN,发请求的函数也在里面,API 地址直接用字符串写进去,搞那么多层没实际意义,在这么小的一个项目里面。

除此之外,关于项目文件结构,一两个文件的建议不要新建文件夹放进去,这个主要是个人习惯,其实无大碍。

合理设计方法名

不留隐患

- (void)requestAtPathForRouteNamed:(NSString *)routeName object:(id)object parameters:(NSDictionary *)parameters

- (void)requestWithMethod:(XXHTTPMethod)method path:(NSString *)path params:(id)params paramsType:(XXParamType)paramsType

之前这样设计的目的是 param 放 form data 类型数据,object 放 json 格式,显然不合理,同一个 API 不应该允许同时存在 form data 和 json,如果采用第一种,新来的同事可能会认为这两个都可以填数据,这是不符合我们期望的。

甚至再极端一点,某天我们需要传文件过去,是不是还得再扩充字段。

如果采用第二种,param 是 id 类型,如果是 json,type 传入 json 枚举类型,如果是二进制,type 传入二进制枚举类型,只留一个字段暴露给开发者更合理。

避免耦合

我们的项目中有一条渐变颜色的线要到处用到,这条线我们放在了 UIImage+XXUtil.h 里面,之前的设计是这样的:

+ (instancetype)xx_navigationBarShadowImage

在 .m 的实现中,还把 UIColor+XXTheme 耦合进去了,并且这个方法已经脱离了类名 Util 的实质,他已经不是一个通用的工具了,重构之后的命名是这样的:

+ (instancetype)xx_gradientImageWithStartColor:(UIColor *)aColor endColor:(UIColor *)bColor andWidth:(CGFloat)width

这样就很符合 Util 这个 category 名字。

避免滥用继承

继承确实很好用,带来的后果就是子类会把父类的方法挨个执行一遍,乍一看没什么,但是如果这个方法很消耗性能呢。

我们这个项目就遇到了,app 经常卡死,用着用着,就 freeze 了,点哪里都没反应。因为所有页面都继承自基类的一个设计,恰好基类里面有一个比较耗时的操作,每个页面都会执行至少三次,就导致了页面假死。

重构后的做法是设计成一个 category,只是给 UIViewController 添加了几个方法,按需调用,不需要在每个页面都调用,于是解决了这个诡异的 bug。

合理选择第三方库

如果有一个功能,迫于各种原因,不得不采用第三方库,至少也要选一个 GitHub 上 star 比较多的吧,其次是看看 issue 列表有没有什么很严重的 bug 没修好,以及兼容性问题,多养成好习惯,慢慢就能筛选出来最合适的库了。

避免滥用第三方库

我们的项目之前有用到 YYText 这个库,就为了一段文字里面加一张图片,活动当天 iOS9 设备出现好几百次 crash,实际上这段代码用 NSAttributedString attributedStringWithAttachment 写一下,七行就够了,七行替代掉一个不稳定的第三方库,还是很划算的。

不知道因为什么原因,可能是更旧的项目里面用了 PSCollectionView,能跑就没去重构,这类库也是属于完全没必要的,系统自带的足够好用,并且更安全。

各司其职

数据的处理,比如字符串进行 UTF8 编码,时间戳转成 YYYY-MM-DD 字符串,这些都放在 Model 层来处理,各司其职,Model 层就是做数据处理的。

结合实际需求

后端把各种 ID 用 long 型来记录,是因为他们要做索引,为了索引速度。而客户端完全没这个需求,直接用 string 就好,还不用担心长度不够的溢出,做展示的时候还不用转类型。

同样的,金额按理说应该用双精度浮点型,因为 float 的精度不够,结合我的开发经验看,金额很少要客户端做加减,直接用 string 即可,需要计算的时候再转换,只转换一次,避免丢失精度。

关于命名规范

苹果的 UIKit 就是最好的例子,写什么组件不知道名字怎么起的时候,就想想苹果有没有类似的组件,去找找灵感。

避免无意义的注释

OC 的方法名本身就很长很清晰了,只是给方法名中间加几个空格,然后作为注释,跟没写一样吧。

不用的代码删掉

Git 的作用就是随时回溯以前版本,代码都是能找到的,把代码注释掉,再写一行类似的,除了增加阅读成本,容易引起歧义,应该没什么用了。一个文件一共一两百行,打开之后发现七八十行代码被注释了,这种感觉相当操蛋,影响阅读。

重构祖传代码真的会有一种活久见的感觉,上面提到的那些是印象比较深刻的,还有一些小问题已经悄悄解决了,希望我写的代码不会让后面的同学也这样认为吧。