正文
前言:
每个开发心中都有一个架构的梦,虽然不能像大佬们一样直接直接给出系统级的架构,但是我们在日常的编码过程中,也可以慢慢积累一些自己的架构的见解,慢慢提高~
因为在学校自己一个人在写整个App,加之需求也不明确,时常需求变更(在学校的组织写项目的通病了),所以编写过程真的是越写越糟心,所以,不得已对已经开发的一小部分做了重构,以下是本小白在重构过程中总结的一些见解(不得不说,本科阶段讲的那些设计模式什么的,是真的很有用,只是当时根本理解不了这些精髓,等到重构时才发现都可以套原型)。
架构的几个方向:
-
view层的组织和调用设计
-
本地持久化
-
网络层设计(网络层会说的比较笼统)
-
动态部署(Web App/Hybrid App/React-Native,这块也没咋说,因为目前没有涉猎)
架构设计的步骤:
-
问题分类,分模块(这个很重要)
-
搞清楚各个模块之间的依赖关系,设计好一套模块的交流规范并设计模块
-
为架构保持一定量的超前性(血的教训)
-
先实现基础模块,再组合基础模块形成初期架构
主要就是:自顶向下设计,自底向上实现,先量化数据再优化
敏捷原则:对扩展开放-对修改封闭
什么样app的架构叫好架构?
-
代码整齐,分类明确:每个模块只负责模块内的事务
-
不用文档,或很少文档,就能让业务方上手
-
思路和方法要统一,尽量不要多元
-
没有横向依赖,万不得已不出现跨层访问:(大概就是拓扑排序的原理)
1,当一个需求需要多业务合作开发时,如果直接依赖,会导致某些依赖层上端的业务工程师在前期空转,依赖层下端的工程师任务繁重,导致延期(就会挤压QA老铁的时间,然后再找PM撕*。。。。。别问我是怎么知道的)
2,当要开辟一个新业务时,如果已有各业务间直接依赖,新业务又依赖某个旧业务,就导致新业务的开发环境搭建困难,因为必须要把所有相关业务都塞入开发环境,新业务才能进行开发。
3,当某一个被其他业务依赖的页面有所修改时,比如改名,涉及到的修改面就会特别大。影响的是造成任务量和维护成本都上升的结果。
对应解决方法:依赖下沉,假如A、B、C三个模块存在横向依赖,这样的话引入新节点D,对A、B、C实现依赖下沉,当A调用B的某个页面的时候,将请求交给Mediater,然后由Mediater通过某种手段获取到B业务页面的实例,交还给A就行了。
-
对业务方该限制的地方有限制,该灵活的地方要给业务方创造灵活- - 实现的条件
-
易测试,易拓展
-
保持一定量的超前性
-
接口少,接口参数少
-
高性能
关于不跨层访问说下:
跨层访问是指数据流向了跟自己没有对接关系的模块。有的时候跨层访问是不可避免的,比如网络底层里面信号从2G变成了3G变成了4G,这是有可能需要跨层通知到View的。但这种情况不多,一旦出现就要想尽一切办法在本层搞定或者交给上层或者下层搞定,尽量不要出现跨层的情况。跨层访问同样也会增加耦合度,当某一层需要整体替换的时候,牵涉面就会很大。
易测试性:
尽可能减少依赖关系,便于mock。另外,如果是高度模块化的架构,拓展起来将会是一件非常容易的事情。
架构分层:
梗概:
经常有‘三层架构MVC’这样的说法,以至于很多人就会认为三层架构就是MVC,MVC就是三层架构。其实不是的。三层架构里面其实没有Controller的概念,而且三层架构描述的侧重点是模块之间的逻辑关系。MVC有Controller的概念,它描述的侧重点在于数据流动方向。
三层架构
所有的模块角色只会有三种:
-
数据管理者
-
数据加工者
-
数据展示者
意思也就是,笼统说来,软件只会有三层,每一层扮演一个角色。其他的第四层第五层,一般都是这三层里面的其中之一分出来的,最后都能归纳进这三层的某一层中去,所以用三层架构来描述就比较普遍。
View层设计:
View层的架构一旦实现或定型,在App发版后可修改的余地就已经非常之小了。因为它跟业务关联最为紧密,做决策时要拿捏好尺度。
View层架构是影响业务方迭代周期的因素之一:
因为View层架构是最贴近业务的底层架构
view层架构知识点主要包括:
-
良好的编码/实现规范
-
合适的设计模式(MVC、MVCS、MVVM、VIPER)
-
根据业务情况针对ViewController做好拆分(瘦身),提供一些小工具方便开发
view层代码规范:(第4点不一定)
1 viewDidload:做addSubview的事情
2 viewWillAppear:严格来说这里通常不做视图位置的修改,而用来更新Form数据。原因见下一点:
3 布局(添加约束)时机:首先,Autolayout发生在viewWillAppear之后,所以我一般选择放到 - viewWilllayoutSubview或者- viewDidLayoutSubviews中。因为viewWillAppear在每次页面即将显示都会调用,viewWillLayoutSubviews虽然在lifeCycle里调用顺序在viewWillAppear之后,但是只有在页面元素需要调整时才会调用,避免了Constraints的重复添加
4 viewDidAppear里面做添加监听之类的事情
5 属性的初始化,则交给getter(懒加载)去做,这也就要求:所有的属性都使用getter和setter,并且getter,setter方法放到.m文件的最后写,这样可以提高开发效率。另外一种思路是将所有属性都放到 setUpPropertyConfig方法中,然后setUpPropertyConfig放到viewDidLoad中,两者均可,没有什么区别。
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view addSubview:self.label];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.label.frame = CGRectMake(1, 2, 3, 4);
}
- (UILabel *)label
{
if (_label == nil) {
_label = [[UILabel alloc] init];
_label.text = @"1234";
_label.font = [UIFont systemFontOfSize:12];
... ...
}
return _label;
}
@end
6 每个delegate方法写到一块区域里面去,使用#pragma mark - UITableViewDelegate进行分割(这个是猪场搬砖看到黄师傅的代码学到的。。。之前一直没有这个意识,感谢)
7 VC里面尽量不要有私有方法
不是delegate方法的,不是event response(相应用户操作)方法的,不是life cycle(view didload这些方法)方法的,就是private method了,这些private methods一般是用于日期换算、图片裁剪啥的这种辅助的小功能。这些小功能一般都是单独抽出来写成模块的tool类或者系统Util类。
8 关于View的布局方法:
无外乎就是 storyboard+xib+代码撸的组合
借鉴一下@唐巧的分析脚本:
传送门:
https://gist.github.com/tangqiaoboy/b149d03cfd0cd0c2f7a1
可见这个本来就是有争议的。
其实,实现简单的东西,用Code一样简单,实现复杂的东西,Code比StoryBoard更简单。
所以本渣一般采用:
1,复杂页面主体手撸代码(用的是masonry)
2,简单、静态的Cell以及封装的一些自定义小控件使用xib。
还有几点本人目前能力不够,不能够给出正确的见解:
A.是否需要让业务方统一派生ViewController。
B.
#MVC
MVC架构基础请看象印笔记。
各个模块需要负责的事物:
-
M应该做的事:
1,给ViewController提供数据(网络获取API+本地的缓存获取API)
2,给ViewController存储数据提供接口(本地缓存的存储/更新新API)
3,提供经过抽象的业务基本组件(一般我会抽一个Manager(包括1,2)出来专门负责),供Controller调度
-
C应该做的事:(其中VC自带的View相当于C所管理的View的一个容器)
1,管理View Container的生命周期
2,负责生成所有的View实例,并放入View Container(就是C.view)
3,监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务。
-
V应该做的事:
1,响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。
2,界面元素表达
下面是MVCS、MVVM两个MVC设计模式的变种
可能还有一些别的设计模式,但是本人能力有限啊啊啊,所以只先介绍这俩
先说下:胖Model&瘦Model:
-
胖Model:(MVVM的基本思想)
包含了部分弱业务逻辑。胖Model要达到的目的是,Controller从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上。
-
瘦Model:(MVCS的基本思想)
瘦Model只负责业务数据的表达,所有业务无论强弱一律扔到Controller。瘦Model要达到的目的是,尽一切可能去编写细粒度Model,然后配套各种helper类或方法来对弱业务做抽象,强业务依旧交给Controller。
前言:首先不管MVVM也好,MVCS也好,他们的共识都是Controller会随着软件的成长,变很大很难维护很难测试。只不过两种架构思路的前提不同,MVCS是认为Controller做了一部分Model的事情,要把它拆出来变成Store,MVVM是认为Controller做了太多数据加工的事情,所以MVVM把数据加工的任务从Controller中解放了出来,使得Controller只需要专注于数据调配的工作,ViewModel则去负责数据加工并通过通知机制让View响应ViewModel的改变。
MVCS:
从概念上来说,它拆分的部分是Model部分,拆出来一个Store。这个Store专门负责数据存取。但从实际操作的角度上讲,它拆开的是Controller。
MVCS使用的前提是,它假设了你是瘦Model,同时数据的存储和处理都在Controller去做。所以对应到MVCS,它在一开始就是拆分的Controller。因为Controller做了数据存储的事情,就会变得非常庞大,那么就把Controller专门负责存取数据的那部分抽离出来,交给另一个对象去做,这个对象就是Store。这么调整之后,整个结构也就变成了真正意义上的MVCS。
MVVM:
ReactiveCocoa成熟之后,ViewModel和View的信号机制在iOS下终于有了一个相对优雅的实现(MVC中View和Model是不能直接通信的,需要Controller做一个协调者的身份)。MVVM本质上也是从MVC中派生出来的思想,MVVM着重想要解决的问题是尽可能地减少Controller的任务。
MVVM是基于胖Model的架构思路建立的,然后在胖Model中拆出两部分:Model和ViewModel。关于这个观点我要做一个额外解释:胖Model做的事情是先为Controller减负,然后由于Model变胖,再在此基础上拆出ViewModel,跟业界普遍认知的MVVM本质上是为Controller减负这个说法并不矛盾,因为胖Model做的事情也是为Controller减负。
另外,MVVM把数据加工的任务从Controller中解放出来,跟MVVM拆分的是胖Model也不矛盾。要做到解放Controller,首先你得有个胖Model,然后再把这个胖Model拆成Model和ViewModel。
在MVVM中,Controller扮演的角色:
-
MVVM的名称里没有C造成了MVVM不需要Controller的错觉,其实MVVM是一定需要Controller的参与的,虽然MVVM在一定程度上弱化了Controller的存在感,并且给Controller做了减负瘦身(这也是MVVM的主要目的)。其实MVVM应该是Model-ViewModel-Controller-View这样的架构,并不是不需要Controller。
-
Controller夹在View和ViewModel之间做事情:
1,最主要事情就是将View和ViewModel进行绑定。在逻辑上,Controller知道应当展示哪个View,Controller也知道应当使用哪个ViewModel,然而View和ViewModel它们之间是互相不知道的,所以Controller就负责控制他们的绑定关系。
2,常规的UI逻辑处理
一句话总结:
在MVC的基础上,把Controller拆出一个ViewModel专门负责数据处理的事情,就是MVVM。
-
关于MVVM是否必须要使用ReactiveCocoa?
当然不是,只是因为苹果本身并没有提供一个比较适合这种情况的绑定方法。虽然有KVO,Notification,block,delegate用来做数据通信,从而来实现绑定,但都不如ReactiveCocoa提供的RACSignal来的优雅简单,如果不用ReactiveCocoa,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM。
再深层次的我就不能很好解释了:如果需要了解,可以细看:
https://www.teehanlax.com/blog/model-view-viewmodel-for-ios/
关于项目究竟使用哪种设计模式:
MVC其实是非常高Level的抽象,意思也就是,在MVC体系下还可以再衍生无数的架构方式,但万变不离其宗的是,它一定符合MVC的规范。
所以我的建议是:
-
只要不是Controller的核心逻辑,都可以考虑拆出去,然后在架构的时候作为一个独立模块去定义,以及设计实现,但是不要为了拆分而拆分。
-
拆分出的模块尽量提高复用性,降低强业务相关性。
-
拆分的粒度要尽可能大一点,封装得要透明一些。
网络层:
首先先说下跨层访问:
关于跨层数据流通:
当存在A<-B<-C这样的结构时。当C有事件,通过某种方式告知B,然后B执行相应的逻辑。一旦告知方式不合理,让A有了跨层知道C的事件的可能,你 就很难保证A层业务工程师在将来不会对这个细节作处理。一旦业务工程师在A层产生处理操作,有可能是补充逻辑,也有可能是执行业务,那么这个细节的相关处理代码就会有一部分散落在A层。然而前者是不应该散落在A层的,后者有可能是需求。另外,因为B层是对A层抽象的,执行补充逻辑的时候,有可能和B层针对这个事件的处理逻辑产生冲突,这是我们很不希望看到的。
但有时跨层数据流通也是不可避免的:
比如,信号从2G变成3G变成4G变成Wi-Fi,这个就是需要跨层数据交流的。
再考虑下文:
数据以什么方式交付给业务层:
大多数App在网络层所采用的方案主要集中于这三种:Delegate,Notification,Block。
一般都是组合使用,这里我只能说下个人的选择,毕竟个人涉猎有限,所以只能结合自身所采用的模式说下好处:
之前在猪场某部门实习,网络层采用的是block为主进行数据交付。
当回调之后要做的任务在每次回调时都是一致的情况下,选择delegate,在回调之后要做的任务在每次回调时无法保证一致,选择block。
-
Delegate为主,Notification为辅(苹果的原生网络请求就是delegate。。但是AFN采用block做回调)。
所以我一般都是采用AFN做网络(毕竟方便省事),所以一般采用如下形式进行请求:
//请求发起采用AFN的Block,回调使用delegate方式,这样在业务方这边回调函数就能够比较统一,便于维护。
[AFNetworkingAPI callApiWithParam:self.param successed:^(Response *response){
if ([self.delegate respondsToSelector:@selector(successWithResponse:)]) {
[self.delegate successedWithResponse:response];
}
} failed:^(Request *request, NSError *error){
if ([self.delegate respondsToSelector:@selector(failedWithResponse:)]) {
[self failedWithRequest:request error:error];
}
}];
原因:使用Delegate能够很好地避免跨层访问,同时限制了响应代码的形式,相比Notification而言有更好的可维护性,而Notification则解决了跨层数据流通的相应需求。
但是使用Notification一定要约定好命名规范,不然会引发后期维护的灾难。
集约型API调用方式和离散型API调用方式的选择:
-
集约型API调用其实就是所有API的调用只有一个类,然后这个类接收API名字,API参数,以及回调着陆点(block,或者delegate等各种模式的着陆点)作为参数。然后执行类似startRequest这样的方法,它就会去根据这些参数起飞去调用API了,然后获得API数据之后再根据指定的着陆点去着陆。
-
离散型API调用是这样的,一个API对应于一个APIManager,然后这个APIManager只需要提供参数就能起飞,API名字、着陆方式都已经集成入APIManager中。