深度重构UIViewController

UIViewController是iOS应用的基础业务单位,每个iOS程序员都写过无数的Controller。今天和大家一起来深度解剖Controller,看看怎么来做一次深度的重构。

重构的前提

我们应该谨慎的去重构我们的代码。iOS系统提供的UIViewController一定程度上可以很好的应付简单的页面单位,对于复杂的页面,我们也可以采用市面上主流的MV(X)系列模式,比如MVP,MVVM等。但随着单个Controller内业务进一步增长,我们需要更细粒度的重构,或者说对MV(X)做进一步的定制。

以下图映客App两个页面为例。

左边页面元素少且静态,一个TableView基本上就能应付,右边的直播页面则元素多且动态,传统的MV(X)也会显得粒度太粗,这类复杂页面虽然不常遇到,但往往体现一个App的核心功能,合理的搭建或者重构这类Controller十分重要。

重构的本质

如何去定义重构,以我的理解可以归纳为两个关键词:分解,连接。

重构的前提是复杂,臃肿,不直观,重构的手段是分解之后再连接。以映客的直播界面为例,UI元素,用户事件,服务器交互等基础元素都非常之多,以一个简单的MVP去归类代码犹嫌不足,我们还需要进一步的分解成view1,view2...viewN,presenter1,presenter2...presenterN,model1,model2...modelN,第二个问题是如何把这一个个的类文件或者说功能单位合理组织连接起来。完成上述两步我们就完成了一次重构,每一次将代码打散再串联就是一次重构。

分解UIViewController

写了那么多Controller,让你来说下一个Controller都细分为哪些更小的功能单位,你能随口说出来么?只有做过足够多的业务,才能慢慢对Controller的构成有自己的理解。

当然可以回答说MVC或者MVP,但这个答案粒度太粗,一个Controller内部会发生哪些事可以说的更细,我们看下VIPER的答案:

  • View: displays what it is told to by the Presenter and relays user input back to the Presenter.
  • Interactor: contains the business logic as specified by a use case.
  • Presenter: contains view logic for preparing content for display (as received from the Interactor) and for reacting to user inputs (by requesting new data from the Interactor).
  • Entity: contains basic model objects used by the Interactor.
  • Routing: contains navigation logic for describing which screens are shown in which order

View不用多说,可以分解成更多的子View,最后合成一个树形结构。

Entity自然是代表Model。

MVC当中的C,MVP当中的P,被细分成了Interactor,Presenter,和Routing。这三个角色各自负责什么职责呢?

Routing比较清楚,处理页面之间的跳转。我见过的项目代码里,很少有把这一部分单独拎出来的,但其实很有意义,这部分代表的是不同Controller之间耦合依赖的方式,无论是从类关系描述的角度还是Debug的角度,都能帮助我们快速定位代码。

Interactor和Presenter初看起来很类似,似乎都是在处理业务逻辑。但业务逻辑其实是个大的归类,可以描述任何一种业务场景和行为。Interactor当中有个很重要的术语:use case,这个术语很多技术文章中都会遇见,它代表的是一个完整的,独立的,细分过后的业务流程,比如我们App当中的登录模块,它是一个业务单位,但它其实可以进一步的细分为很多的use case:

use case 1: 验证邮箱长度

use case 2: 密码强度检验

use case 3: 从Server查询user name是否可用

...

user case N

定义use case有什么好处呢?

好处当然是分门别类,结构清晰。你把100本书堆一堆,或者放书架上按类别摆放,下次找书的时候那种方式你更舒服?独立出一个个的use case还有一个好处是方便unit test,如果项目对每一个use case都有写对应的unit test,每次遇到“前一发动全身“的业务更改,可以边杯茶边写代码。

我见过不少代码都体现不出use case的分类,可以回头看下自己当前项目的登录模块,上面我提到的这些case有没有在类文件当中合理摆放,还是都搅在一起?

所以VIPER当中interactor的说法是强化大家写单独的use case的意识,打开interactor.m,看到一个函数代表一个use case,同一类的use case再用#pragma mark 归在一块,别人看你代码时能不赏心悦目吗?

再说到Presenter,Presenter可以看做是上面一个个use case的使用者和响应者。使用者将各个use case串联起来描述一个完整详细的业务流程,比如我们的登录模块,每次用户点击按钮注册的时候,会触发一系列的use case,从检验用户输入合法性,设备网络状态,服务器资源是否可用,到最后处理结果并展示,这就是一个完整的业务流程,这个流程由Presenter来描述。响应者表示Presenter在接收到服务器反馈之后进一步改变本地的状态,比如view的展示,新的数据修改等,甚至会调用Routing发生页面跳转。

说到这里就比较明了了,interactor和routing都是服务的提供方,presenter是服务的使用和集成方。VIPER说白了不过是对传统的MVC当中的C做了进一步细分。

能不能分的更细呢?

当然可以,VIPER的分法是一种通用的做法,我们还可以从业务的角度去做细分。拿映客的直播界面做例子,比如Presenter当中包含了很多完整的业务流程:

  • 收到用户消息并展示
  • 收到礼品消息并展示
  • 收到弹幕消息并展示
  • 收到用户进出房间的事件,处理并展示
  • 收到XXX,处理并展示

以Objective C语言的特性,我们可以生成更多的Presenter Category来安置这些流程,比如LivePresenter+Message, LivePresenter+Gift, LivePresenter+Danmu, LivePresenter+Room, LivePresenter+XXX。

不要觉得上面几个业务流程很简单,一个presenter处理绰绰有余,我前段时间刚好做过一个直播项目,Presenter类超过1000行代码很轻松。

还可以进一步细分,一个功能复杂繁多的页面基本上离不开UITableView,而tableview的代码量主要在于delegate和datasource。这两个职责当然可以放在presenter当中,或者我们向Android学习,把它们也独立出来放到单独的类文件中去处理,比如叫做Adapter,用代码来说就是:

_tableView.delegate = self.adapter;
_tableView.dataSource = self.adapter;

和tableView相关的这些代码都搬到了adapter当中:

@protocol UITableViewDelegate

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;

@end

@protocol UITableViewDataSource<NSObject>

@required
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

@end

我们的Presenter就变得更加干净了,看起来和刚大扫除过的房间一样令人愉悦。

好了,到这里我们盘子里的牛排已经被切成很多小块了,可以开始享用这些美味的代码了,继续我们的第二步工作:连接。

连接

先看下我们分解之后有哪些元素:

view(1…N), model(1…N), interactor, presenter(1…N), routing, adapter。看着应该粒度够细了,对于复杂的Controller,我个人习惯的做法和VIPER相近,但略有不同,Interactor当中的use case通过分层的架构被我放到server layer,分层的架构是另一个话题,这里不做细述。其他元素基本一致。

至于怎么连接,手段无非就是OC的几种类交互机制:

Delegate, Target-Action, Block, Notification, KVO

这几者之间的差异可以参考objc.io的一篇经典文章。选择不同对耦合度,开发便捷性,调试是否方便等都会产生影响,如何应用不同的机制将各个单位串联起来就看架构师自己的积累和理解了,任何一个选择都有其优势和局限性。

如果拿捏不准选哪个好的时候,我个人建议就使用delegate,朴素可靠且直观。delegate需要在不同的元素之间传递,代码量会偏多一些,但优点在protocol定义清晰,耦合在哪里一目了然,记得要注意循环引用的问题。

我早些时候其他几种机制都在实际项目中做过尝试,最后综合比较还是倾向于选择delegate,再后来经过一番脑洞(主要是为了解决传递delegate所带来的额外代码量),利用runtime特性,做了一个CDD机制来自动串联各个功能单位。CDD的详细介绍在之前的博客中有,这里也不细述了,其本质或者说最终目的还是在于连接。

说完了分解和连接,Controller的重构完成了大半,还剩下一个至关重要的概念:状态分享。

尽量避免跨类,跨模块或跨层共享状态

我之前在一篇博客中谈到过对于程序状态的维护。状态是否维护得好对于程序的整体稳定性很有影响,对于Controller当中的状态维护我有一个简单的建议:

传递状态的时候尽可能Copy

之前流行的函数式编程其实就很强调无状态性,无状态不是让大家不定义状态变量,而是避免函数之间的状态共享,具体到OC当中,就是不要在不同的功能单位里使用指向同一块内存拷贝的地址,为什么共享状态是一件危险的事,我在之前的文章中也介绍过。

一般来说,我们从Model Layer或者说数据层拿到的model实例,扔给Controller使用的时候应该是一份新的拷贝,在不同的类单位里共享NSMutableString或者NSMutableArray,NSMutableDictionary很容易让你的代码变得不稳定,而且这类不稳定性一般很难调试,debug填坑的时候经常按下葫芦浮起瓢。

在controller内部传递model或者state的时候,我们应该也尽量使用copy行为,任何state你一旦暴露出去就不再安全,自己创建,自己修改,自己销毁才是正途。说到

我之前介绍Facebook架构的时候就提到过,Facebook当中的model layer是由一个单独开发团队维护的,应用层开发人员(Controller开发人员)获取到的都是新的拷贝,要修改某个属性不一定有接口,甚至要向model的维护团队提交增加接口的申请,对于state维护的谨慎度可见一斑。

使用脚本生成原型代码

说了这么多,Controller重构的关键点都说完了。最后再提个小Tip,一旦Controller做深度细分之后,团队成员需要对Controller的分法和构成有一致的认识,写出来的代码应该保持一致,我的做法是通过脚本的方式生成Controller各个相关的类文件,比如我的Controller是如下结构:

通过脚本将文件名和文件内容当中的Template全部替换成目标Controller的名字,就省去了很多重复代码的体力劳动,也达到了代码风格一致的目的。

欢迎关注公众号:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容