近期重构工作的一点收获

Blog Dec 6, 2017

更新:关于重构后增加的 bug 数量大家就不用多心了😂 我在团队是担任移动端 Lead 的,如果有重大失误,应该早就被拉出去祭天了吧。重构工作是两个月以前了的,结合这两个月的 issue 列表来看,引入的 bug 不多,最近在统计自己都做了什么工作,所以才把这篇文章分享出来。

以前做个人项目的时候,简历上写过重构了三次,后来在扇贝面试的时候,面试官问三次分别重构了什么,仔细想想那时候的重构并不算重构,第一次是 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 的作用就是随时回溯以前版本,代码都是能找到的,把代码注释掉,再写一行类似的,除了增加阅读成本,容易引起歧义,应该没什么用了。一个文件一共一两百行,打开之后发现七八十行代码被注释了,这种感觉相当操蛋,影响阅读。

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

Tags

Jie Li

🚘 On-road / 📉 US Stock / 💻 Full Stack Developer / 🎓 Grad Student / ®️ ENTJ

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.