暴风雨前夜
我所在的XX金融从事iOS开发已有两年,这两年产品从无到有快速迭代,我一直从事关于App后台的搭建,架构上的整理,主要是负责用户侧/鉴权侧,尤其是在年底对App的账户相关模块进行了一次大的重构,也有了一些新的想法
先说说公司的架构,公司的后台服务相当于一个个独立的子服务系统,基本上各自为政,比如日志系统,交易系统,用户系统等等,他们的URL也不尽相同,但是基本上都遵循一个统一的Http头部信息,在语言,Cookies上面提供了不上的好处,在body这一侧,也统一的response的结构和错误码区间,很好的解决了出错的展示问题。
但是,公司的弊端也十分明显,由于各个服务之间数据耦合较少,有一些任务居然要App做中转,比如上传日志,当App上传日志的时候,要先向指定的位置上传(OSS服务),上传成功后,OSS服务返回文件的具体地址,然后把指定的具体地址发给用户服务器,用户服务器记录下后返回App,这个时候App才认为上传成功。从这个业务可见 ,App在各个服务之间承担了大量的逻辑运算和数据中转传递的功能,导致了业务线的拉长。并且由于业务出现串行,就导致一个事件不再是原子操作事件,导致了串行事件失败了一个就会导致整个业务失败,而整个业务失败后,中间用来缓存其中某个事件的数据也需要做清除操作。总之,App端将承受较大的逻辑压力。
为什么要这么做
先说一下改版之前的做法。改版重构之前,我所在的用户模块,是属于业务逻辑最为复杂的模块之一,承担金融账户侧的鉴权,数据展示,登录注册,各种账户相关的权限控制,还承担了用户侧的交易/登录密码,手机号设置等等。这里是整个APP的数据中心的管理单元,各个数据的设置,读取,归档,导致了耦合性十分的严重,这就导致了修改一个问题要反复核对,不仅仅影响开发效率,导致了很多问题的发生。
.
业务层拿数据,可以从网络层中获取,也可以从本地磁盘中获取,网络层的数据,也可以获取到就直接赋值到本地存储池中。光看这个图就十分的苦闷,这个架构至少存在几个问题:
层级划分不明,权责不明,分工不清。
作为业务层,其实只要关心业务就好了,获取数据只要做到向下层获取,不用管具体的获取来源是什么,数据流动流向应该是单向运动,即下层仅仅对上层提供数据支持,并整合本层所有数据,在一个类中集中管理。这该图中可以看到,当时的XX股票架构,业务层居然可以访问存储层,也可以访问网络层,所以在代码里面,可以看到数据是要从网络端获取还是本地存储中获取,其实是用很多判断逻辑的。导致了每个Controller异常复杂。
数据流动方向的一致性,数据源头单一性
数据应该是单方向运动,及从下至上的传递方式。如果说存在双向传递,则可以在同一层中双向传递(如本地持久层和本地热数据,他们都属于同一层,彼此之间双向数据传递是可以的),但是在上下层之间双向传递是十分可怕的。比如网络层收到了数据,XX股票的旧架构就会向业务层传递数据(代理方法模式),同时也向本地存储数据。这样在业务层就十分困惑:是读取网络层传递过来的数据,还是读取本地存储的数据。这里还可能存在多线程的同步问题,导致收到数据方也可能不能获取到最新的数据。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
block(data); //业务层回调
});
saveToLocal(data);//本地存储池
对于这里,到底是业务层先返回,还是本地存储的数据先更新,这就很难说了。这样的代码进一步导致了问题的复杂性。
要什么/怎么做
丢了这些杂七杂八的东西,重新开始思考架构。我们到底需要怎样的架构。
-
我认为应该是层次分明,层级划分十分清晰。
-
我认为要可扩展性强,即两两层级之间耦合。
.
所以,我将旧的框架改为这样:
-
将两层换位三层,将大量的业务逻辑下层到AccountManager,这样的好处是十分明显的,因为在重构前,我发现了大量的重复冗余代码,相同的代码分散到各个Controller中。而现在全部放到AccountManager里面做整合,重复代码迅速减少。
-
在Controller中不需要判断数据应该从哪里获取,所有数据只需要向AccountManager中获取就好了。AccountManager去决定,当前是需要发起网络请求,还是直接返回本地数据。
-
数据流向单一,即从下层向上层传递。同一层不传递数据,以上一层做枢纽,传递数据。这里,如果网络返回数据,先将数据返回至AccountManager,AccountManager决定是否要覆盖本地缓存,同时将数据传递至Controller层(UI)。这样,同一层次之间不相干的模块相互不耦合。
一步步来
架构是出来了,但是问题也随之而来,该如何改动代码?重构的朋友都知道,要在老的架构中,添加一个层级,是很麻烦的,尤其是他们彼此之间两两耦合。我的做法是,在老的架构上,先提供一个帮助类(helpModule),先将他放到UI层的位置,先当做为UI减轻负担的一个帮助类,这个类里面集合了大多数权限,账号,管控的判断。等UI层的Controller的冗余代码大量减少,逻辑比较清晰的时候,再将这个Help类改为AccountManager做管理类,将AccountManager与下层打通,AccountManager移动到下一层就好了。
AccountManager的信息爆炸。
不得不说,这是我之前并没有想到的,AccountManager中由于整合了App全局信息管理,导致了代码量剧增,之前为了避免代码复用,鼓励组内成员将业务层相关的逻辑代码规整到AccountManager中,但是放开权限后,大家将它作为为自己模块减负的逻辑帮助类在使用,导致了AccountManager管理十分困难。经过一个版本的整理,我将不能被复用的代码又整合回所属的Controller,另外,AccountManager自身增加帮助类,进一步瘦身。
是否提供set权限
关于数据传递时,AccountManager是否能够接受上层向下层的直接写入操作?这个问题事实上,和组内成员有过较多的讨论,我的看法是,AccountManager不能接受上层(UI层)的set方法,即所有提供的字段,严格意义上来说,都应该是readonly的。
这里分情况讨论:
- 当前数据来自于用户。由于当前层级关系,UI层访问下层数据,必须经过AccountManager,所以如果当前的数据是用户提供的,比如说用户上一次输入的手机号码,这里可以提供类方法(不是实例方法)用于该数据的读写。
- 当前数据来自服务器。这里我的看法是,如果你要修改该字段,就必须发起请求,让后台返回的数据刷新到本地。比如说我们有个逻辑,当用户输入错误密码达到五次,就锁定账户,我认为应该每次输入完密码后,让后台返回当前的账户状态,直接复写该锁定字段即可。这样,App永远信任一个输入源(即服务器),避免UI层的set导致数据和服务端的不一致。
保持数据来源的一致性可以很好的解决随机Bug的产生,当某一模块强行set一个数据到AccountManager里面去,同时请求网络来刷新数据,如果网络返回的数据和Set的数据不一致的时候,那么在UI上会出现自己Set的UI样式,然后闪现为后台的UI样式,而且当这个字段和其他字段关联,就可能存在组合异常的情况,如果这里出现了异常情况,数据恢复又成了难事。所以有个统一的数据源,对于逻辑精简大有裨益。
但是,这个策略在组内推行时受到了很大的阻力,其他模块的成员往往希望自己能够直接操作数据,而不是让数据操作自己模块。还是上面这个例子,其他模块的成员希望被锁后直接给给AccountManager调用set方法,让该字段立即处于这个状态,比如UI立即显示,而不是等网络回来再显示。他们的理由是不能接受太长时间的网络请求(而我后来了解到,实际上,这和网络发送的串行模式有很大关系,即如果他们的某个协议被复用,并且该协议也被用于串行网络请求,那么他们仅仅更新数据就可能引起错误的业务。这一点我下一篇文章着重说明)。
归类存储
为了避免AccountManager的数据爆炸,我将数据做归类处理,即数据为集中模式而非平铺模式。这样数据的聚合大大减少了代码接口量,并且提供的模型数据也便于存储。在需要复杂网络业务的时候,将不相关的模型做逻辑处理,返回有价值的信息,避免在上层中直接读取裸数据。