《iOS应用架构谈 开篇》出来以后,不少人来催我赶忙出第二篇。这一篇文章出得至关艰难,由于公司里的破事儿特别多,我本身又有点私事儿,以致于能用来写博客的时间不够充分。html
如今好啦,第二篇出来了。java
当咱们开始设计View层的架构时,每每是这个App尚未开始开发,或者这个App已经发过几个版本了,而后此时须要作很是完全的重构。ios
通常也就是这两种时机会去作View层架构,基于这个时机的特殊性,咱们在这时候必须清楚认识到:View层的架构一旦实现或定型,在App发版后可修改的余地就已经很是之小了。由于它跟业务关联最为紧密,因此哪怕稍微动一点点,它所引起的蝴蝶效应都不见得是业务方可以hold住的。这样的状况,就要求咱们在实现这个架构时,代码必须得改得勤快,不能偷懒。也必须抱着充分的自我怀疑态度,作决策时要拿捏好尺度。git
View层的架构很是之重要,在我看来,这部分架构是这系列文章涉及4个方面最重要的一部分,没有之一。为何这么说?程序员
View层架构是影响业务方迭代周期的因素之一github
产品经理产生需求的速度会很是快,尤为是公司此时仍处于创业初期,在规模稍大的公司里面,产品经理也喜欢挖大坑来在leader面前刷存在感,好比阿里。这就致使业务工程师任务很是繁重。正常状况下让产品经理砍需求是不太可能的,所以做为架构师,在架构里有一些可作可不作的事情,最好仍是能作就作掉,不要偷懒。这能够帮业务方减负,编写代码的时候也能更加关注业务。web
我跟一些朋友交流的时候,他们都会或多或少地抱怨本身的团队迭代速度不够快,或者说,迭代速度不合理地慢。我认为迭代速度不是想提就能提的,迭代速度的影响因素有不少,一期PRD里的任务量和任务复杂度都会影响迭代周期能达到什么样的程度。抛开这些外在的不谈,从内在可能致使迭代周期达不到合理的速度的缘由来看,其中有一个缘由颇有可能就是View层架构没有作好,让业务工程师完成一个不算复杂的需求时,须要处理太多额外的事情。固然,开会多,工程师水平烂也属于迭代速度提不上去的内部缘由,但这个不属于本文讨论范围。还有,加班不是优化迭代周期的正确方式
,嗯。面试
通常来讲,一个不够好的View层架构,主要缘由有如下五种:数据库
这五个地方会影响业务工程师实现需求的效率,进而拖慢迭代周期。View架构的其余缺陷也会或多或少地产生影响,但在我看来这里五个是比较重要的影响因素。若是你们以为还有什么因素比这四个更高的,能够在评论区提出来我补上去。xcode
对于第五点我想作一下强调:架构的设计是必定须要有传承的,有传承的架构从总体上看会很是协调。但实际状况有多是一我的走了,另外一个顶上,即使任务交接得再完整,都不可避免不一样的人有不一样的架构思路,从而致使整个架构的流畅程度受到影响。要解决这个问题,一方面要尽可能避免单点问题,让架构师作架构的时候再带一我的。另外一方面,架构要设计得尽可能简单,平缓接手人的学习曲线。我离开安居客的时候,作过保证:凡是从我手里出来的代码,终身保修
。因此不要想着离职了就什么事儿都无论了,这不光是职业素养问题,还有一个是你对你的代码是否足够自信的问题。传承性对于View层架构很是重要,由于它距离业务最近,改动余地最小。
因此当各位CTO、技术总监、TeamLeader们以为迭代周期不够快时,你能够先不忙着急吼吼地去招新人,《人月神话》早就说过加人不能彻底解决问题。这时候若是你能够回过头来看一下是否是View层架构不合理,把这个弄好也是优化迭代周期的手段之一。
嗯,至于本系列其余三项的架构方案对于迭代周期的影响程度,我认为都不如View层架构方案对迭代周期的影响高,因此这是我认为View层架构是最重要的其中一个理由。
View层架构是最贴近业务的底层架构
View层架构虽然也算底层,但还没那么底层,它跟业务的对接面最广,影响业务层代码的程度也最深。在全部的底层都牵一发的时候,在View架构上牵一发致使业务层动全身的面积最大。
因此View架构在全部架构中一旦定型,可修改的空间就最小,咱们在一开始考虑View相关架构时,不光要实现功能,还要考虑更多规范上的东西。制定规范的目的一方面是防止业务工程师的代码腐蚀View架构,另外一方面也是为了可以有所传承。按照规范来,总仍是不那么容易出差池的。
还有就是,架构师一开始考虑的东西也会有不少,不可能在初版就把它们所有实现,对于一个还没有发版的App来讲,初版架构每每是最小完整功能集,那么在第二版第三版的发展过程当中,架构的迭代任务就颇有可能不仅是你一我的的事情了,相信你一我的也不见得能搞定所有。因此你要跟你的合做者们有所约定。另外,初版出去以后,业务工程师在使用过程当中也会产生不少修改意见,哪些意见是合理的,哪些意见是不合理的,也要经过事先约定的规范来进行筛选,最终决定如何采纳。
规范也不是一成不变的,何时枪毙意见,何时改规范,这就要靠各位的技术和经验了。
以上就是前言。
View代码结构的规定
关于view的布局
什么时候使用storyboard,什么时候使用nib,什么时候使用代码写View
是否有必要让业务方统一派生ViewController?
方便View布局的小工具
MVC、MVVM、MVCS、VIPER
本门心法
跨业务时View的处理
留给评论区各类补
总结
架构师不是写SDK出来交付业务方使用就没事儿了的,每家公司必定都有一套代码规范,架构师的职责也包括定义代码规范。按照道理来说,定代码规范应该是属于通识,放在这里讲的缘由只是由于我这边须要为View添加一个规范。
制定代码规范严格来说不属于View层架构的事情,但它对View层架构将来的影响会比较大,也是属于架构师在设计View层架构时须要考虑的事情。制定View层规范的重要性在于:
在这一节里面我不打算从头开始定义一套规范,苹果有一套Coding Guidelines,当咱们定代码结构或规范的时候,首先必定要符合这个规范。
而后,相信你们各自公司里面也都有一套本身的规范,具体怎么个规范法其实也是根据各位架构师的经验而定,我这边只是建议各位在各自规范的基础上再加上下面这一点。
viewController的代码应该差很少是这样:
要点以下:
全部的属性都使用getter和setter
不要在viewDidLoad里面初始化你的view而后再add,这样代码就很难看。在viewDidload里面只作addSubview的事情,而后在viewWillAppear里面作布局的事情(勘误1
),最后在viewDidAppear里面作Notification的监听之类的事情。至于属性的初始化,则交给getter去作。
好比这样:
#pragma mark - life cycle - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; [self.view addSubview:self.firstTableView]; [self.view addSubview:self.secondTableView]; [self.view addSubview:self.firstFilterLabel]; [self.view addSubview:self.secondFilterLabel]; [self.view addSubview:self.cleanButton]; [self.view addSubview:self.originImageView]; [self.view addSubview:self.processedImageView]; [self.view addSubview:self.activityIndicator]; [self.view addSubview:self.takeImageButton]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; CGFloat width = (self.view.width - 30) / 2.0f; self.originImageView.size = CGSizeMake(width, width); [self.originImageView topInContainer:70 shouldResize:NO]; [self.originImageView leftInContainer:10 shouldResize:NO]; self.processedImageView.size = CGSizeMake(width, width); [self.processedImageView right:10 FromView:self.originImageView]; [self.processedImageView topEqualToView:self.originImageView]; CGFloat labelWidth = self.view.width - 100; self.firstFilterLabel.size = CGSizeMake(labelWidth, 20); [self.firstFilterLabel leftInContainer:10 shouldResize:NO]; [self.firstFilterLabel top:10 FromView:self.originImageView]; ... ... }
这样即使在属性很是多的状况下,仍是可以保持代码整齐,view的初始化都交给getter去作了。总之就是尽可能不要出现如下的状况:
- (void)viewDidLoad { [super viewDidLoad]; self.textLabel = [[UILabel alloc] init]; self.textLabel.textColor = [UIColor blackColor]; self.textLabel ... ... self.textLabel ... ... self.textLabel ... ... [self.view addSubview:self.textLabel]; }
这种作法就不够干净,都扔到getter里面去就行了。关于这个作法,在唐巧的技术博客里面有一篇文章和我所提倡的作法不一样,这个我会放在后面详细论述。
getter和setter所有都放在最后
由于一个ViewController颇有可能会有很是多的view,就像上面给出的代码样例同样,若是getter和setter写在前面,就会把主要逻辑扯到后面去,其余人看的时候就要先划过一长串getter和setter,这样不太好。而后要求业务工程师写代码的时候按照顺序来分配代码块的位置,先是life cycle
,而后是Delegate方法实现
,而后是event response
,而后才是getters and setters
。这样后来者阅读代码时就能省力不少。
每个delegate都把对应的protocol名字带上,delegate方法不要处处乱写,写到一块区域里面去
好比UITableViewDelegate的方法集就老老实实写上#pragma mark - UITableViewDelegate
。这样有个好处就是,当其余人阅读一个他并不熟悉的Delegate实现方法时,他只要按住command而后去点这个protocol名字,Xcode就可以马上跳转到对应这个Delegate的protocol定义的那部分代码去,就免得他处处找了。
event response专门开一个代码区域
全部button、gestureRecognizer的响应事件都放在这个区域里面,不要处处乱放。
关于private methods,正常状况下ViewController里面不该该写
不是delegate方法的,不是event response方法的,不是life cycle方法的,就是private method了。对的,正常状况下ViewController里面通常是不会存在private methods的,这个private methods通常是用于日期换算、图片裁剪啥的这种小功能。这种小功能要么把它写成一个category,要么把他作成一个模块,哪怕这个模块只有一个函数也行。
ViewController基本上是大部分业务的载体,自己代码已经至关复杂,因此跟业务关联不大的东西能不放在ViewController里面就不要放。另一点,这个private method的功能这时候只是你用获得,可是未来说不定别的地方也会用到,一开始就独立出来,有利于未来的代码复用。
为何要这样要求?
我见过无数ViewController,代码布局乱得一塌糊涂,这里一个delegate那里一个getter,而后ViewController的代码通常都死长死长的,看了就让人头疼。
定义好这个规范,就能使得ViewController条理清晰,业务方程序员很可以区分哪些放在ViewController里面比较合适,哪些不合适。另外,也能够提升代码的可维护性和可读性。
业务工程师在写View的时候必定逃不掉的就是这个命题。用Frame也好用Autolayout也好,若是没有精心设计过,布局部分必定惨不忍睹。
直接使用CGRectMake的话可读性不好,光看那几个数字,也没法知道view和view之间的位置关系。用Autolayout可读性稍微好点儿,但生成Constraint的长度实在太长,代码观感不太好。
Autolayout这边能够考虑使用Masonry,代码的可读性就能好不少。若是还有使用Frame的,能够考虑一下使用这个项目。
这个项目里面提供了Frame相关的方便方法(UIView+LayoutMethods
),里面的方法也基本涵盖了全部布局的需求,可读性很是好,使用它以后基本能够和CGRectMake说再见了。由于天猫在最近才切换到支持iOS6,因此以前天猫都是用Frame布局的,在天猫App中,首页,范儿部分页面的布局就使用了这些方法。使用这些方便方法能起到事半功倍的效果。
这个项目也提供了Autolayout方案下生产Constraints的方便方法(UIView+AEBHandyAutoLayout
),可读性比原生好不少。我当时在写这系列方法的时候还不知道有Masonry。知道有Masonry以后我特意去看了一下,发现Masonry功能果真强大。不过这系列方法虽然没有Masonry那么强大,可是也够用了。当时安居客iPad版App所有都是Autolayout来作的View布局,就是使用的这个项目里面的方法。可读性很好。
让业务工程师使用良好的工具来作View的布局,能提升他们的工做效率,也能减小bug发生的概率。架构师不光要关心那些高大上的内容,也要多给业务工程师提供方便易用的小工具,才能发挥架构师的价值。
这个问题唐巧的博客里这篇文章也提到过,个人意见和他是基本一致的。
在这里我还想补充一些内容:
具备必定规模的团队化iOS开发(10人以上)有如下几个特色:
部分复用
的状况也比较多。
若是这三个特色你一看就明白了,下面的解释就能够不用看了。若是你针对个人倾向愿意进一步讨论的,能够先看我下面的解释,看完再说。
同一份代码文件的做者会有不少,不一样做者同时修改同一份代码的状况也很多见。所以,使用Git进行代码版本管理时出现Conflict的概率也比较大。
iOS开发过程当中,会遇到最蛋疼的两种Conflict一个是project.pbxproj
,另一个就是StoryBoard
或XIB
。由于这些文件的内容的可读性很是差,虽然苹果在XCode5(如今我有点不肯定是否是这个版本了)中对StoryBoard的文件描述方式作了必定的优化,但只是把可读性从很是差
提高为不好
。
然而在StoryBoard中每每包含了多个页面,这些页面基本上不太可能都由一我的去完成,若是另外一我的在作StoryBoard的操做的时候,出于某些目的动了一下不属于他的那个页面,好比为了美观调整了一下位置。而后另一我的也由于要添加一个页面,而在Storyboard中调整了一下某个其余页面的位置。那么针对这个状况我除了说个呵呵
之外,我就只能说:祝你好运。
看清楚哦,这还没动具体的页页面内容呢。
但若是使用代码绘制View,Conflict同样会发生,可是这种Conflict就好解不少了,你懂的。
需求变化很是频繁,产品经理一时一个主意,为了完成需求而针对现有代码进行微调的状况,以及针对现有代码的
部分复用
的状况也比较多。
我以为产品经理一时一个主意不是他的错,他说不定也是被逼的,好比谁都会来掺和一下产品的设计,公司里的全部人,上至CEO,下至基层员工都有可能对产品设计评头论足,只要他我的有个地方用得不爽(极大多是我的喜爱)而后又正好跟产品经理比较熟悉可以搭得上话,都会提出各类意见。产品经理躲不起也惹不起,有时也是没办法,嗯。
但落实到工程师这边来,这种状况就很蛋疼。由于这种改变有时候不光是UI,UI所对应的逻辑也有要改的可能,工程师就会两边文件都改,你原来link的那个view如今不link了,而后你的outlet对应也要删掉,这两部分只要有一个没作,编译经过以后跑一下App,一下子就crash了。看起来这不是什么大事儿,但很影响心情。
另外,若是出现部分的代码复用,好比说某页面下某个View也但愿放在另一个页面里,相关的操做就不是复制粘贴这么简单了,你还得从新link一遍。也很影响心情。
复杂界面元素,复杂动画交互场景的开发任务比较多。
要是想在基于StoryBoard的项目中作一个动画,很烦。作几个复杂界面元素,也很烦。有的时候咱们挂Custom View上去,其实在StoryBoard里面看来就是一个空白View。而后另一点就是,当你的layout出现问题须要调整的时候,仍是挺难找到问题所在的,尤为是在复杂界面元素的状况下。
因此在针对View层这边的要求时,我也是建议不要用StoryBoard。实现简单的东西,用Code同样简单,实现复杂的东西,Code比StoryBoard更简单。因此我更加提倡用code去画view而不是storyboard。
有的时候咱们出于记录用户操做行为数据的须要,或者统一配置页面的目的,会从UIViewController里面派生一个本身的ViewController,来执行一些通用逻辑。好比天猫客户端要求全部的ViewController都要继承自TMViewController。这个统一的父类里面针对一个ViewController的全部生命周期都作了一些设置,至于这里都有哪些设置对于本篇文章来讲并不重要。在这里我想讨论的是,在设计View架构时,若是为了可以达到统一设置或执行统一逻辑的目的,使用派生的手段是有必要的吗?
我以为没有必要,为何没有必要?
这两条缘由是我认为没有必要使用派生手段的理由,若是两条理由你都心照不宣,那么下面的就能够不用看了。若是你还有点疑惑,请看下面我来详细讲一下缘由。
为何使用了派生,业务方的使用成本会提高?
其实不光是业务方的使用成本,架构的维护成本也会上升。那么具体的成本都来自于哪里呢?
这里讲的集成成本是这样的:若是业务方本身开了一个独立demo,快速完成了某个独立流程,如今他想把这个现有流程集合进去。那么问题就来了,他须要把全部独立的UIViewController改变成TMViewController。那为何不是一开始就马上使用TMViewController呢?由于要想引入TMViewController,就要引入整个天猫App全部的业务线,全部的基础库,由于这个父类里面涉及数日猫环境才有的内容,所谓拔出萝卜带出泥,你要是想简单继承一下就能搞定的事情,搭环境就要搞半天,而后这个小Demo才能跑得起来。
对于业务层存在的全部父类来讲,它们是很容易跟项目中的其余代码纠缠不清的,这使得业务方开发时遇到一个两难问题:要么把全部依赖所有搞定,而后基于App环境(好比天猫)下开发Demo
,要么就是本身Demo写好以后,按照环境要求改代码
。这里的两难问题都会带来成本,都会影响业务方的迭代进度。
我不肯定各位所在公司是否会有这样的状况,但我能够在这里给你们举一个我在阿里的真实的例子:我最近在开发某滤镜Demo和相关页面流程,最终是要合并到天猫这个App里面去的。使用天猫环境进行开发的话,pod install完全部依赖差很少须要10分钟,而后打开workspace以后,差很少要再等待1分钟让xcode作好索引,而后才能正式开始工做。在这里要感谢一下则平,由于他在此基础上作了不少优化,使得这个1分钟已经比原来的时间短不少了。但若是天猫环境有更新,你就要再重复一次上面的流程,不然 就颇有可能编译不过。
拜托,我只是想作个Demo而已,不想搞那么复杂。
新来的业务工程师有的时候不见得都记得每个ViewController都必需要派生自TMViewController而不是直接的UIViewController。新来的工程师他不能直接按照苹果原生的作法去作事情,他须要额外学习,好比说:全部的ViewController都必须继承自TMViewController。
尽量少地使用继承能提升项目的可维护性,具体内容我在《跳出面向对象思想(一) 继承》里面说了,在这里我想偷懒不想把那篇文章里说过的东西再说一遍。
其实对于业务方来讲,主要仍是第一个集成成本比较蛋疼,由于这是长痛,每次要作点什么事情都会遇到。第二点倒还好,短痛。第三点跟业务工程师没啥关系。
那么若是不使用派生,咱们应该使用什么手段?
个人建议是使用AOP。
在架构师实现具体的方案以前,必需要想清楚几个问题,而后才能决定采用哪一种方案。是哪几个问题?
这三个问题按照顺序一一解答以后,具体方案就能出来了。
咱们先看第一个问题:
方案的效果,和最终要达到的目的是什么?
方案的效果应该是:
其实就是要实现不经过业务代码上对框架的主动迎合,使得业务可以被框架感知
这样的功能。细化下来就是两个问题,框架要可以拦截到ViewController的生命周期,另外一个问题就是,拦截的定义时机。
对于方法拦截,很容易想到Method Swizzling
,那么咱们能够写一个实例,在App启动的时候添加针对UIViewController的方法拦截,这是一种作法。还有另外一种作法就是,使用NSObject的load函数,在应用启动时自动监听。使用后者的好处在于,这个模块只要被项目包含,就可以发挥做用,不须要在项目里面添加任何代码。
而后另一个要考虑的事情就是,原有的TMViewController(所谓的父类)也是会提供额外方法方便子类使用的,Method Swizzling
只支持针对现有方法的操做,拓展方法的话,嗯,固然是用Category
啦。
我本人不同意Category的过分使用,但鉴于Category是最典型的化继承为组合的手段,在这个场景下仍是适合使用的。还有的就是,关于Method Swizzling
手段实现方法拦截,业界也已经有了现成的开源库:Aspects,咱们能够直接拿来使用。
我这边有个很是很是小的Demo能够放出来给你们,这个Demo只是一个点睛之笔,有一些话我也写在这个Demo里面了,各位架构师们大家能够基于各自公司App的需求去拓展。
这个Demo不包含Category,毕竟Category仍是得大家本身去写啊~而后这套方案可以完成原来经过派生手段全部能够完成的任务,但同时又容许业务方没必要添加任何代码,直接使用原生的UIViewController。
而后另外要提醒的是,这方案的目的是消除没必要要的继承,虽然不限定于UIViewController,但它也是有适用范围的,在适用继承的地方,仍是要老老实实使用继承。好比你有一个数据模型,是由基本模型派生出的一整套模型,那么这个时候仍是老老实实使用继承。至于拿捏什么时候使用继承,相信各位架构师必定可以处理好,或者你也能够参考我前面提到的那篇文章来控制拿捏的尺度。
其实这些都是相对通用的思想,万变不离其宗的仍是在开篇里面我提到的那三个角色:数据管理者
,数据加工者
,数据展现者
。这些五花八门的思想,不外乎就是制订了一个规范,规定了这三个角色应当如何进行数据交换。但同时这些也是争议最多的话题,因此我在这里来把几个主流思想作一个梳理,当你在作View层架构时,可以有个比较好的参考。
MVC(Model-View-Controller)是最老牌的的思想,老牌到4人帮的书里把它归成了一种模式,其中Model
就是做为数据管理者
,View
做为数据展现者
,Controller
做为数据加工者
,Model
和View
又都是由Controller
来根据业务需求调配,因此Controller
还负担了一个数据流调配的功能。正在我写这篇文章的时候,我看到InfoQ发了这篇文章,里面提到了一个移动开发中的痛点是:对MVC架构划分的理解
。我当时没可以去参加这个座谈会,也没办法发表我的意见,因此就只能在这里写写了。
在iOS开发领域,咱们应当如何进行MVC的划分?
这里面其实有两个问题:
为何咱们会纠结于iOS开发领域中MVC的划分问题?
关于这个,每一个人纠结的点可能不太同样,我也不知道当时座谈会上你们的观点。但请容许我猜一下:是否是由于UIViewController中自带了一个View,且控制了View的整个生命周期(viewDidLoad,viewWillAppear...),而在常识中咱们都知道Controller不该该和View有如此紧密的联系,因此才致使你们对划分产生困惑?
,下面我会针对这个猜想来给出个人意见。
在服务端开发领域,Controller和View的交互方式通常都是这样,好比Yii:
/*
... 数据库取数据 ... 处理数据 ... */ // 此处$this就是Controller $this->render("plan",array( 'planList' => $planList, 'plan_id' => $_GET['id'], ));
这里Controller和View之间区分得很是明显,Controller作完本身的事情以后,就把全部关于View的工做交给了页面渲染引擎去作,Controller不会去作任何关于View的事情,包括生成View,这些都由渲染引擎代劳了。这是一个区别,但其实服务端View的概念和Native应用View的概念,真正的区别在于:从概念上严格划分的话,服务端其实根本没有View,拜HTTP协议所赐,咱们平时所讨论的View只是用于描述View的字符串(更实质的应该称之为数据),真正的View是浏览器。
。
因此服务端只管生成对View的描述,至于对View的长相,UI事件监听和处理,都是浏览器负责生成和维护的。可是在Native这边来看,本来属于浏览器的任务也逃不掉要本身作。那么这件事情由谁来作最合适?苹果给出的答案是:UIViewController。
鉴于苹果在这一层作了不少坚苦卓绝的努力,让iOS工程师们没必要亲自去实现这些内容。并且,它把全部的功能都放在了UIView上,而且把UIView作成不光能够展现UI,还能够做为容器的一个对象。
看到这儿你明白了吗?UIView的另外一个身份实际上是容器!UIViewController中自带的那个view,它的主要任务就是做为一个容器。若是它全部的相关命名都改为ViewContainer
,那么代码就会变成这样:
- (void)viewContainerDidLoad { [self.viewContainer addSubview:self.label]; [self.viewContainer addSubview:self.tableView]; [self.viewContainer addSubview:self.button]; [self.viewContainer addSubview:self.textField]; } ... ...
仅仅改了个名字,如今是否是感受清晰了不少?若是再要说详细一点,咱们日常所认为的服务端MVC是这样划分的:
---------------------------
| C |
| Controller |
| |
---------------------------
/ \
/ \
/ \
------------ ---------------------
| M | | V |
| Model | | Render Engine |
| | | + |
------------ | HTML Files |
---------------------
但事实上,整套流程的MVC划分是这样:
---------------------------
| C |
| Controller |
| \ |
| Render Engine |
| + |
| HTML Files |
---------------------------
/ \
/ \ HTML String
/ \
------------ ---------------
| M | | V |
| Model | | Browser |
| | | |
------------ ---------------
由图中能够看出,咱们服务端开发在这个概念下,其实只涉及M和C的开发工做,浏览器做为View的容器,负责View的展现和事件的监听。那么对应到iOS客户端的MVC划分上面来,就是这样:
----------------------------
| C |
| Controller |
| \ |
| View Container |
----------------------------
/ \
/ \
/ \
------------ ----------------------
| M | | V |
| Model | | UITableView |
| | | YourCustomView |
------------ | ... |
----------------------
惟一区别在于,View的容器在服务端,是由Browser负责,在整个网站的流程中,这个容器放在Browser是很是合理的。在iOS客户端,View的容器是由UIViewController中的view负责,我也以为苹果作的这个选择是很是正确明智的。
由于浏览器和服务端之间的关系很是松散,并且他们分属于两个不一样阵营,服务端将对View的描述生成以后,交给浏览器去负责展现,然而一旦view上有什么事件产生,基本上是不多传递到服务器(也就是所谓的Controller)的(要传也能够:AJAX),都是在浏览器这边把事情都作掉,因此在这种状况下,View容器就适合放在浏览器(V)这边。
可是在iOS开发领域,虽然也有让View去监听事件的作法,但这种作法很是少,都是把事件回传给Controller,而后Controller再另行调度。因此这时候,View的容器放在Controller就很是合适。Controller能够由于不一样事件的产生去很方便地更改容器内容,好比加载失败时,把容器内容换成失败页面的View,无网络时,把容器页面换成无网络的View等等。
在iOS开发领域中,怎样才算是MVC划分的正确姿式?
这个问题其实在上面已经解答掉一部分了,那么这个问题的答案就当是对上面问题的一个总结吧。
M应该作的事:
C应该作的事:
V应该作的事:
我经过与服务端MVC划分的对比来回答了这两个问题,之因此这么作,是由于我知道有不少iOS工程师以前是从服务端转过来的。我也是这样,在进安居客以前,我也是作服务端开发的,在学习iOS的过程当中,我也曾经对iOS领域的MVC划分问题产生过疑惑,我疑惑的点就是前面开篇我猜想的点。若是有人问我iOS中应该怎么作MVC的划分,我就会像上面这么回答。
苹果自身就采用的是这种架构思路,从名字也能看出,也是基于MVC衍生出来的一套架构。从概念上来讲,它拆分的部分是Model部分,拆出来一个Store。这个Store专门负责数据存取。但从实际操做的角度上讲,它拆开的是Controller。
这算是瘦Model的一种方案,瘦Model只是专门用于表达数据,而后存储、数据处理都交给外面的来作。MVCS使用的前提是,它假设了你是瘦Model,同时数据的存储和处理都在Controller去作。因此对应到MVCS,它在一开始就是拆分的Controller。由于Controller作了数据存储的事情,就会变得很是庞大,那么就把Controller专门负责存取数据的那部分抽离出来,交给另外一个对象去作,这个对象就是Store。这么调整以后,整个结构也就变成了真正意义上的MVCS。
关于胖Model和瘦Model
我在面试和跟别人聊天时,发现知道胖Model和瘦Model的概念的人不是不少。大约两三年前国外业界曾经对此有过很是激烈的讨论,主题就是Fat model, skinny controller
。如今关于这方面的讨论已经很少了,然而直到今天胖Model和瘦Model哪一个更好,业界也尚未定论,因此这算是目前业界悬而未解的一个争议。我不多看到国内有讨论这个的资料,因此在这里我打算补充一下什么叫胖Model什么叫瘦Model。以及他们的争论来源于何处。
胖Model包含了部分弱业务逻辑
。胖Model要达到的目的是,Controller从胖Model这里拿到数据以后,不用额外作操做或者只要作很是少的操做,就可以将数据直接应用在View上
。举个例子:
Raw Data: timestamp:1234567 FatModel: @property (nonatomic, assign) CGFloat timestamp; - (NSString *)ymdDateString; // 2015-04-20 15:16 - (NSString *)gapString; // 3分钟前、1小时前、一天前、2015-3-13 12:34 Controller: self.dateLabel.text = [FatModel ymdDateString]; self.gapLabel.text = [FatModel gapString];
把timestamp转换成具体业务上所须要的字符串,这属于业务代码,算是弱业务。FatModel作了这些弱业务以后,Controller就能变得很是skinny,Controller只须要关注强业务代码就好了。众所周知,强业务变更的可能性要比弱业务大得多,弱业务相对稳定,因此弱业务塞进Model里面是没问题的。另外一方面,弱业务重复出现的频率要大于强业务,对复用性的要求更高,若是这部分业务写在Controller,相似的代码会洒获得处都是,一旦弱业务有修改(弱业务修改频率低不表明就没有修改),这个事情就是一个灾难。若是塞到Model里面去,改一处不少地方就能跟着改,就能避免这场灾难。
然而其缺点就在于,胖Model相对比较难移植,虽然只是包含弱业务,但好歹也是业务,迁移的时候很容易拔出萝卜带出泥。另一点,MVC的架构思想更加倾向于Model是一个Layer,而不是一个Object,不该该把一个Layer应该作的事情交给一个Object去作。最后一点,软件是会成长的,FatModel颇有可能随着软件的成长愈来愈Fat,最终难以维护。
瘦Model只负责业务数据的表达,全部业务不管强弱一概扔到Controller
。瘦Model要达到的目的是,尽一切可能去编写细粒度Model,而后配套各类helper类或方法来对弱业务作抽象,强业务依旧交给Controller
。举个例子:
Raw Data: { "name":"casa", "sex":"male", } SlimModel: @property (nonatomic, strong) NSString *name; @property (nonatomic, strong) NSString *sex; Helper: #define Male 1; #define Female 0; + (BOOL)sexWithString:(NSString *)sex; Controller: if ([Helper sexWithString:SlimModel.sex] == Male) { ... }
因为SlimModel跟业务彻底无关,它的数据能够交给任何一个能处理它数据的Helper或其余的对象,来完成业务。在代码迁移的时候独立性很强,不多会出现拔出萝卜带出泥的状况。另外,因为SlimModel只是数据表达,对它进行维护基本上是0成本,软件膨胀得再厉害,SlimModel也不会大到哪儿去。
缺点就在于,Helper这种作法也不见得很好,这里有一篇文章批判了这个事情。另外,因为Model的操做会出如今各类地方,SlimModel在必定程度上违背了DRY(Don't Repeat Yourself)的思路,Controller仍然不可避免在必定程度上出现代码膨胀。
个人态度?嗯,我会在本门心法
这一节里面说。
说回来,MVCS是基于瘦Model的一种架构思路,把本来Model要作的不少事情中的其中一部分关于数据存储的代码抽象成了Store,在必定程度上下降了Controller的压力。
VIPER(View,Interactor,Presenter,Entity,Routing)。VIPER我并无实际使用过,我是在objc.io上第13期看到的。
但凡出现一个新架构或者我以前并不熟悉的新架构,有一点我可以很是确定,这货必定又是把MVC的哪一个部分给拆开了(坏笑,作这种判断的理论依据在第一篇文章里面我已经讲过了)。事实状况是VIPER确实拆了不少不少,除了View没拆,其它的都拆了。
我提到的这两篇文章关于VIPER都讲得很详细,一看就懂。但具体在使用VIPER的时候会有什么坑或者会有哪些争议我不是很清楚,硬要写这一节的话我只能靠YY,因此我想一想仍是算了。若是各位读者有谁在实际App中采用VIPER架构的或者对VIPER颇有兴趣的,能够评论区里面提出来,咱们交流一下。
针对View层的架构不光是看重如何合理地拆分MVC来给UIViewController减负,另一点也要照顾到业务方的使用成本。最好的状况是业务方什么都不知道,而后他把代码放进去就能跑,同时还能得到框架提供的种种功能。
好比天安门广场上的观众看台,就是我以为最好的设计,由于没人会注意到它。
继承是罪恶,尽可能不要继承。就我目前了解到的状况看,除了安居客的Pad App没有在框架级针对UIViewController有继承的设计之外,其它公司或多或少都针对UIViewController有继承,包括安居客iPhone app(那时候我已经对此无能为力,可见View的架构在一开始就设计好有多么重要)。甚至有的还对UITableView有继承,这是一件多么使人发指,多么惨绝人寰,多么丧心病狂的事情啊。虽然不可避免的是有些状况咱们不得不从苹果原生对象中继承,好比UITableViewCell。但我仍是建议尽可能不要经过继承的方案来给原生对象添加功能,前面提到的Aspect方案和Category方案均可以使用。用Aspect+load来实现重载函数,用Category来实现添加函数,固然,耍点手段用Category来添加property也是没问题的。这些方案已经覆盖了继承的所有功能,并且很是好维护,对于业务方也更加透明,何乐而不为呢。
不用继承可能在思路上不会那么直观,可是对于不使用继承带来的好处是足够顶得上使用继承的坏处的。顺便在此我要给Category正一下名:业界对于Category的态度比较暧昧,在多种场合(讲座、资料文档)都宣扬过尽量不要使用Category。它们说的都有必定道理,但我认为Category是苹果提供的最好的使用集合代替继承的方案,但针对Category的设计对架构师的要求也很高,请合理使用。并且苹果也在不少场合使用Category,来把一个本来可能很大的对象,根据不一样场景拆分红不一样的Category,从而提升可维护性。
不使用继承的好处我在这里已经说了,放到iOS应用架构来看,还能再多额外两个好处:1. 在业务方作业务开发或者作Demo时,能够脱离App环境,或花更少的时间搭建环境。2. 对业务方来讲功能更加透明,也符合业务方在开发时的第一直觉。
这主要是为了提升可维护性。在一个文件很是大的对象中,尤为要限制好不一样类型的代码在文件中的布局。好比在写ViewController时,我以前给团队制定的规范就是前面一段所有是getter setter,而后接下来一段是life cycle,viewDidLoad之类的方法都在这里。而后下面一段是各类要实现的Delegate,再下面一段就是event response,Button的或者GestureRecognizer的都在这里。而后后面是private method。通常状况下,若是作好拆分,ViewController的private method那一段是没有方法的。后来随着时间的推移,我发现开头放getter和setter太影响阅读了,因此后面改为全放在ViewController的最后。
Controller会变得庞大的缘由,一方面是由于Controller承载了业务逻辑,MVC的总结者(在正式提出MVC以前,或多或少都有人这么设计,因此说MVC的设计者不太准确)对Controller下的定义也是承载业务逻辑,因此Controller就是用来干这事儿的,天经地义。另外一方面是由于在MVC中,关于Model和View的定义都很是明确,不多有人会把一个属于M或V的东西放到其余地方。而后除了Model和View之外,还会剩下不少模棱两可的东西,这些东西从概念上讲都算Controller,并且因为M和V定义得那么明确,因此直觉上看,这些东西放在M或V是不合适的,因而就往Controller里面塞咯。
正是因为上述两方面缘由致使了Controller的膨胀。咱们再细细思考一下,Model膨胀和View膨胀,要针对它们来作拆分其实都是相对容易的,Controller膨胀以后,拆分就显得艰难无比。因此若是可以在一开始就尽可能把能不放在Controller作的事情放到别的地方去作,这样在第一时间就可让你的那部分未来可能会被拆分的代码远离业务逻辑。因此咱们要稍微转变一下思路:模棱两可的模块,就不要塞到Controller去了,塞到V或者塞到M或者其余什么地方都比塞进Controller好,便于未来拆分
。
因此关于前面我按下不表的关于胖Model和瘦Model的选择,个人态度是更倾向于胖Model。客观地说,业务膨胀以后,代码规模确定少不了的,无论你技术再好,经验再丰富,代码量最多只能优化,该膨胀仍是要膨胀的,并且优化以后代码每每也比较难看,使用各类奇技淫巧也是有代价的。因此,针对代码量优化的结果,每每要么就是牺牲可读性,要么就是牺牲可移植性(通用性),Every magic always needs a pay, you have to make a trade-off.
。
那么既然膨胀出来的代码,或者未来有可能膨胀的代码,无论放在MVC中的哪个部分,最后都是要拆分的,既然早晚要拆分,那不如放Model里面,这样未来拆分胖Model也能比拆分胖Cotroller更加容易。在我还在安居客的时候,安居客Pad app承载最复杂业务的ViewController才不到600行,其余多数Controller都是在300-400行之间,这就为后面接手的人下降了很是多的上手难度和维护复杂度。拆分出来的东西都是能够直接迁移给iPhone app使用的。如今看天猫的ViewControler,动不动就几千行,看不了多久头就晕了,问了一下,你们都表示很习惯这样的代码长度,摊手。
架构师在公司里的职级和地位每每都是要高于业务工程师的,架构师的技术实力和经验每每也都是高于业务工程师的。因此你值得在公司里得到较高的地位,可是在公司里的地位高不表明在软件工程里面的角色地位也高
。架构师是要为业务工程师服务的,是他们使唤你而不是你使唤他们
。另外,制定规范一方面是起到约束业务工程师的代码,但更重要的一点是,这实际上是利用你的能力帮助业务工程师避免他没法预见的危机,因此地位高有必定的好处,毕竟夏虫不可语冰,有的时候不见得可以解释得通,所以高地位随之而来的就是说服力会比较强。但在软件工程里,必定要保持谦卑,必定要多为业务工程师考虑。
一个不懂这个道理的架构师,设计出来的东西每每复杂难用,由于他只愿意作核心的东西,周边不肯意作的都指望交给业务工程师去作,甚至有的时候就只作了个Demo,而后就交给业务工程师了,业务工程师变成给他打工的了。可是一个懂得这个道理的架构师,设计出来的东西会很是好用,业务方只须要扔不多的参数而后拿结果就行了,这样的架构才叫好的架构。
举一个保存图片到本地的例子,一种作法是提供这样的接口:- (NSString *)saveImageWithData:(NSData *)imageData
,另外一种是- (NSString *)saveImage:(UIImage *)image
。后者更好,缘由本身想。
你的态度越谦卑,就越能设计出好的架构,这是我设计心法里的最后一条,也是最重要的一条。即便你如今技术实力不是业界大牛级别的,但只要保持这个心态去作架构,去作设计,就已是合格的架构师了,要成为业界大牛也会很是快。
其实针对View层的架构设计,仍是要作好三点:代码规范
,架构模式
,工具集
。
代码规范对于View层来讲意义重大,毕竟View层很是重业务,若是代码布局混乱,后来者很难接手,也很难维护。
架构模式具体如何选择,彻底取决于业务复杂度。若是业务至关至关复杂,那就能够使用VIPER,若是相对简单,那就直接MVC稍微改改就行了。每一种已经成为定式的架构模式不见得都适合各自公司对应的业务,因此须要各位架构师根据状况去作一些拆分或者改变。拆分通常都不会出现问题,改变的时候,只要别把MVC三个角色搞混就行了,M该作啥作啥,C该作啥作啥,V该作啥作啥,不要乱来。关于大部分的架构模式应该是什么样子,这篇文章里都已经说过了,不过我认为最重要的仍是后面的心法,模式只是招术,熟悉了心法才能大巧不工
。
View层的工具集主要仍是集中在如何对View进行布局,以及一些特定的View,好比带搜索提示的搜索框这种。这篇文章只提到了View布局的工具集,其它的工具集相对而言是更加取决于各自公司的业务的,各自实现或者使用CocoaPods里现成的都不是很难。
对于小规模或者中等规模iOS开发团队来讲,作好以上三点就足够了。在大规模团队中,有一个额外问题要考虑,就是跨业务页面调用方案的设计。
跨业务页面调用是指,当一个App中存在A业务,B业务等多个业务时,B业务有可能会须要展现A业务的某个页面,A业务也有可能会调用其余业务的某个页面。在小规模的App中,咱们直接import其余业务的某个ViewController而后或者push或者present,是不会产生特别大的问题的。可是若是App的规模很是大,涉及业务数量很是多,再这么直接import就会出现问题。
-------------- -------------- --------------
| | page call | | page call | |
| Buisness A | <---------> | Buisness B | <---------> | Buisness C |
| | | | | |
-------------- -------------- --------------
\ | /
\ | /
\ | /
\ | /
\ | /
--------------------------------
| |
| App |
| |
--------------------------------
能够看出,跨业务的页面调用在多业务组成的App中会致使横向依赖。那么像这样的横向依赖,若是不去设法解决,会致使什么样的结果?
当一个需求须要多业务合做开发时,若是直接依赖,会致使某些依赖层上端的业务工程师在前期空转,依赖层下端的工程师任务繁重,而整个需求完成的速度会变慢,影响的是团队开发迭代速度。
当要开辟一个新业务时,若是已有各业务间直接依赖,新业务又依赖某个旧业务,就致使新业务的开发环境搭建困难,由于必需要把全部相关业务都塞入开发环境,新业务才能进行开发。影响的是新业务的响应速度。
当某一个被其余业务依赖的页面有所修改时,好比更名,涉及到的修改面就会特别大。影响的是形成任务量和维护成本都上升的结果。
固然,若是App规模特别小,这三点带来的影响也会特别小,可是在阿里这样大规模的团队中,像天猫/淘宝这样大规模的App,一旦赶上这里面哪怕其中一件事情,就特么很坑爹。
让依赖关系下沉。
怎么让依赖关系下沉?引入Mediator模式。
所谓引入Mediator模式来让依赖关系下沉,实质上就是每次呼唤页面的时候,经过一个中间人来召唤另一个页面,这样只要每一个业务依赖这个中间人就能够了,中间人的角色就能够放在业务层的下面一层,这就是依赖关系下沉。
-------------- -------------- --------------
| | | | | |
| Buisness A | | Buisness B | | Buisness C |
| | | | | |
-------------- -------------- --------------
\ | /
\ | /
\ | / 经过Mediater来召唤页面
\ | /
\ | /
--------------------------------
| |
| Mediater |
| |
--------------------------------
|
|
|
|
|
--------------------------------
| |
| App |
| |
--------------------------------
当A业务须要调用B业务的某个页面的时候,将请求交给Mediater,而后由Mediater经过某种手段获取到B业务页面的实例,交还给A就好了。在具体实现这个机制的过程当中,有如下几个问题须要解决:
这个看起来就很是像咱们web开发时候的URL机制,发送一个Get或Post请求,CGI调用脚本把请求分发给某个Controller下的某个Action,而后返回HTML字符串到浏览器去解析。苹果自己也实现了一套跨App调用机制,它也是基于URL机制来运转的,只不过它想要解决的问题是跨App的数据交流和页面调用,咱们想要解决的问题是下降各业务的耦合度。
不过咱们还不能直接使用苹果原生的这套机制,由于这套机制不可以返回对象实例。而咱们但愿可以拿到对象实例,这样不光能够作跨业务页面调用,也能够作跨业务的功能调用。另外,咱们又但愿咱们的Mediater也可以跟苹果原生的跨App调用兼容,这样就又能帮业务方省掉一部分开发量。
就我目前所知道的状况,AutoCad旗下某款iOS应用(时间有点久我不记得是哪款应用了,若是你是AutoCad的iOS开发,能够在评论区补充一下。)就采用了这种页面调用方式。天猫里面目前也在使用这套机制,只是这一块因为历史缘由存在新老版本混用的状况,所以暂时还没可以很好地发挥应有的做用。
嗯,想问我要Demo的同窗,我能够很大方地告诉你,没有。不过我打算抽时间写一个出来,如今除了已经想好名字叫Summon之外,其它什么都没作,哈哈。
我比较习惯一个对象的"私有"属性写在extension里面,而后这些属性的初始化所有放在getter里面作,在init和dealloc以外,是不会出现任何相似_property
这样的写法的。就是这样:
@interface CustomObject() @property (nonatomic, strong) UILabel *label; @end @implement #pragma mark - life cycle - (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.label]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.label.frame = CGRectMake(1, 2, 3, 4); } #pragma mark - getters and setters - (UILabel *)label { if (_label == nil) { _label = [[UILabel alloc] init]; _label.text = @"1234"; _label.font = [UIFont systemFontOfSize:12]; ... ... } return _label; } @end
唐巧说他喜欢的作法是用_property
这种,而后关于_property
的初始化经过[self setupProperty]
这种作法去作。从刚才上面的代码来看,就是要在viewDidLoad里面多调用一个setup方法而已,而后我推荐的方法就是不用多调一个setup方法,直接走getter。
嗯,怎么说呢,其实两种作法都能完成需求。可是从另外一个角度看,苹果之因此选择让[self getProperty]
和self.property
能够互相通用,这种作法已经很明显地表达了苹果的倾向:但愿每一个property都是经过getter方法来得到
。
早在2003年,Allen Holub就发了篇文章《Why getter and setter methods are evil》,自此以后,业界就对此产生了各类争议,虽然是从Java开始说的,可是发展到后面各类语言也参与了进来。而后虽然如今关于这个问题讨论得少了,可是依旧属于没有定论的状态。setter的状况比较复杂,也不是我这一节的重点,我这边仍是主要说getter。咱们从objc的设计来看,苹果的设计者更加倾向于getter is not evil
。
认为getter is evil
的缘由有很是之多,或大或小,随着争论的进行,你们慢慢就聚焦到这样的一个缘由:Getter和Setter提供了一个能让外部修改对象内部数据的方式,这是evil的,正常状况下,一个对象本身私有的变量应该是只有本身关心
。
而后咱们回到iOS领域来,objc也一样面临了这样的问题,甚至更加严重:objc并无像Java那么严格的私有概念
。但在实际工做中,咱们不太会去操做头文件里面没有的变量,这是从规范上就被禁止的。
认为getter is not evil
的缘由也能够聚焦到一个:高度的封装性
。getter事实上是工厂方法,有了getter以后,业务逻辑能够更加专一于调用,而没必要担忧当前变量是否可用。咱们能够想一下,假设一个ViewController有20个subview要加入view中,这20个subview的初始化代码是确定逃不掉的,放在哪里比较好?放在哪里都比放在addsubview的地方好,我我的认为最好的地方仍是放在getter里面,结合单例模式以后,代码会很是整齐,生产的地方和使用的地方获得了很好的区分。
因此放到iOS来讲,我仍是以为使用getter会比较好,由于evil的地方在iOS这边基本都避免了,not evil的地方都能享受到,仍是不错的。
要作一个View层架构,主要就是从如下三方面入手:
固然,你还会遇到其余的不少问题,这时候你能够参考这篇文章里提出的心法,在后面提到的跨业务页面调用方案的设计中,你也可以看到个人一些心法的影子。
对于iOS客户端来讲,它并不像其余语言诸如Python、PHP他们有那么多的非官方通用框架。客观缘由在于,苹果已经为咱们作了很是多的事情,作了不少的努力。在苹果已经作了这么多事情的基础上,架构师要作针对View层的方案时,最好仍是尽可能遵照苹果已有的规范和设计思想,而后根据本身过去开发iOS时的经验,尽量给业务方在开发业务时减负,提升业务代码的可维护性,就是View层架构方案的最大目标。