Design Pattern and Refactoring

总结下过去这段时间在重构过程中看过书中的知识点,以及一些经验教训。
PS: 本文没有写完,部分内容待完善,不定期更新

重构

重构的原因

最近负责的项目已经运行了六年,一套代码支持了几十个项目。时常出现一改动代码就引发新 Bug,功能或者项目之间很容易互相影响。这套结构混乱、耦合度高、复用性不强,逻辑混乱的代码给现今的工作带来种种不便。再加上缺乏需求和设计文档,新入职的员工来了半年也不能接手新业务。解决方案就是重构拆分,模块独立,降低系统耦合性。

重构和重写

大多数人都喜欢看自己写的代码,觉得别人的代码不容易理解,维护以前的代码浪费时间,所以一说到重构他们首先想到的是重写。不说重写会造成多少资源的浪费,但是我相信重写引入的 Bug 肯定会比合理重构造成的 Bug 只多不少。《Refactoring》书中给了我们很好的说明什么时候应该重写

现有代码根本不能正常运作。你可能只是试着做点测试,然后就发现代码中满是错误,根本无法稳定运作。

重构的目标

  • 解耦,减少 Bug
  • 组件化,将可以共用的部分抽离,避免重复代码散落多处维护困难
  • 模块化,按照业务功能拆分,做到高内聚低耦合
  • 逻辑结构要清晰。变量、方法放合适的地方
  • 接口合理化,不好用的接口的表现是,调用者又写了一些逻辑去弥补接口应该完成的工作
  • 梳理混乱的目录结构
  • 使用 CocoaPods 发布,每个模块打包成单独的 Pod

另外,即使使用 MVC 结构,大家都清楚 C 层不应该放 V 和 M 层的逻辑,但是一些初级的开发人员很容易就会把 V 或者 M 层的逻辑写到 C 层。

如上图橙黄色层为业务模块,绿色为中间层,红色为第三方开源库和底层 API,绿色是对红色层的封装,橙黄色只能调用绿色层和底层,红色部分是不可以修改的部分。调用关系如白线所示,模块间不能通过硬编码互相调用,模块间的重复部分可以下沉到组件。

重构的目标有了,框架也有了,但是如果每个类内部实现的方式很糟糕, 再完美的框架也只是个摆设。模块的高内聚低耦合还是需要利用好的模式来实现。

框架

采用大家最熟悉的 MVC,未采用 MVVM / MVP / VIPER 等模式是由于公司现有的业务就需要一段时间,再使用一些框架会加长适应时间,再者 RAC 可能会导致不方便查找 Bug,所以新开发的小模块有兴趣的话可以采用 MVP/ VIPER,整体框架沿用 MVC。

为了实现组件化,引入 BeeHive。

代码的坏味道

再次看 《Refactoring》 时候突然意识到作者总结的真是全面,很多问题都是自己遇到过的但是让我突然说一下肯定想不全面,所以平时还是应该多总结。

  • 重复代码、修改一处代码相关逻辑也要修改(抽出公共函数调用)
  • 过长函数、参数列、过大的类 (分解长内容)
  • 继承关系混乱,父类中的方法子类不愿意支持(《设计模式》中提到,“优先使用对象组合,而不是类继承”)
  • 过多的注释

关于注释,我个人也不喜欢写注释,好的代码注释就是把变量函数名用连贯的词汇表述出来,只有有问题的代码才需要注释,那么有问题的代码就不应该出现,所以我喜欢下面这句

当你感觉需要撰写注释时,请先尝试重构,试着让所有注释变得多余。

以上问题的重构方案步骤参见 《Refactoring》 一书即可,书中很详细的介绍了如何一小步一小步的修改,最终达成目的。

设计模式六大原则

除了以上,违反设计模式的六大原则也对目前项目造成一万点伤害

  • 单一职责原则,一个类就负责一件事
  • 里氏替换原则,子类可以扩展父类的功能,父类的功能应该是所有子类都需要包含的
  • 依赖倒置原则,高层模块不应该依赖低层模块,二者都应该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象
  • 接口隔离原则,适当的进行接口拆分
  • 迪米特原则,一个对象应该对其他对象保持最少的了解
  • 开闭原则,类、模块、函数应该对扩展开放,对修改关闭

继承的滥用

项目中有很多层次很深的继承关系,以前的同事已经习惯,只要一说复用,就用继承解决,再加上一些新手不太理解继承的用法,当需求变化,子类和父类需要有差异,就复制父类的代码覆盖父类方法,继承又违反里氏替换和接口隔离原则等等,导致继承滥用,代码混乱。

可以使用对象组合、利用 protocol 接口化、Category 等方式实现。

对象组合

即 is-a 改为 has-a,例如一个小控件 Widget,包含左侧显示标题 TitlePanel,右侧显示内容的控件 TextPanel 或者 PhotoPanel,原有的继承关系如下

|- Widget
~~~|- BasePanel
~~~~~~|- TitlePanel
~~~~~~~~~|- TextFieldPanel
~~~~~~~~~|- PhotoPanel

BasePanel 应该是包含了一个 title,TextPanel 和 PhonePanel 也不是 TitlePanel,他们之间的关系应该是平行的,不是从属关系,修改之后原本的 4 层继承关系,变为了 3 层,结构为

|- Widget
~~~|- TitlePanel
~~~|- BasePanel
~~~~~~|- TextFieldPanel
~~~~~~|- PhotoPanel

@interface BasePanel : Widget
@property (nonatomic, strong) TitlePanel *titlePanel;
@end

protocol

如果两个类的实现没有重复逻辑,但是需要公共函数统一调用,可以使用 protocol 定义公共接口

再如上述 TextFieldPanel 中获取文本内容的数据可能来自两个对象模型,一个是字典项 DictionaryModel, 一个是产品数据 ProductModel,这两个 Model 如果都继承自同一个父类,父类暴露 getContent 方法,基类不实现该方法,子类覆盖该方法即可实现,但是由于两个模型没有任何可复用代码,所以使用 protocol 定义接口的方式实现更加合适。

@protocol WidgetContent <NSObject>
- (void)getContent;
@end

@interface DictionaryModel : NSObject <WidgetContent>
@end
@interface ProductModel : NSObject <WidgetContent>
@end

优化

重构代码的同时,还对原有项目进行了优化,主要从以下几个方面进行

页面加载速度

  • 缓存,包括文件缓存,内存缓存(NSCache,自定义缓存池…)
  • 懒加载
  • 不要阻塞主线程
  • 使用算法或者合适的数据类型 (Map / Set 替换 Array 等)

服务器和客户端通信

  • 数据压缩,例如开启 GZip,使用 PorotoBuf
  • 减少/合并网络请求

PorotoBuf

Protocol Buffers 简称 (PorotoBuf / PB ) 是 Google 推出的一种高效的序列化文件格式,最大的特征是支持自定义的数据类型。可以将本地序列化文件 UserDefaults 等,或者服务器返回的 JSON 替换成 ProtoBuf。

优势
~ 比 XML,JSON 文件小,且解析更快
~ 像 Swift 一样,是类型安全的,可以给每个属性指定类型
~ 自动反序列化
~ 跨平台的

局限
~ 需要额外的开发工作
~ 没有 .proto 文件的情况下,不能做到自描述

数据库优化

  • 字段使用合适的类型,尽量减少 TEXT 类型等
  • 查询语句优化,不要在循环里调用数据库,将循环改成一次 SQL 查询
  • 使用更好用的轮子,例如 FMDB不支持 ORM,数据表有变动的时候需要改动的代码很多,修改不全则会引发 Bug,可以改用 GYDataCenter,并且该库增加了缓存机制。后期可能会使用 WCDB。

架构原则

重构开关

重构的同时要保证之前的功能还能正常运转,重写的功能可以通过开关来控制。测试通过重写的代码后,并上线一段时间后则可删除开关和原有代码。

监控处理

诊断日志可以帮助开发人员定位问题,之前项目中有诊断日志的上传功能,系统崩溃的时候会记录调用堆栈,下次应用启动的时候自动上传到后台。如果数据出错,用户还可以手动上传诊断日志到服务器。

优化后添加以下功能:

  • 截屏监测,截屏后弹出意见反馈的提示,将截图和诊断日志手动上传。
  • 后台可以根据配置,收集指定用户的诊断日志信息。

此外可以对性能、用户行为等数据进行监控。

单元测试

增加单元测试代码,详情参见(Test Driven iOS Development)[http://alicialy.github.io/2018/05/11/Test-Driven-iOS-Development/]

依靠工具提高代码质量

静态分析工具
OCLint
Infer

重复代码查询
PMD

代码格式化工具
Clang Format

请我喝汽水儿