该系列文章是2016年折腾的一个总结,对于这一年中思考和解决的一些问题做一些梳理和总结。
前两篇文章主要是说了业务逻辑接口还有模块化的事情。随着系统内部逻辑单元(可能是模块,也可能是为了解耦拆解出来用来承载职责的类等常见的实现)的增多。势必会引入另外的一个问题,就是逻辑单元之间的交互增加和逻辑单元之间通信成本的提高。在iOS架构设计系列之解耦的尝试之变异的MVVM,一文中我们在将整个业务逻辑层从MCV向MVVM演变的时候也遇到了这个问题,当时是本着作孽自己造轮子的心态,通过构建EventBus组件来解决。同样,针对于逻辑单元之间通信成本增加的问题,也需要寻找一个合适的解决方案。
在ios多模块管理一文中,描述一种进行系统模块拆解和管理的思路。将职能不同的业务,拆解成了独立的模块。而且每个模块通过代码隔离,做到了互相之间影响的最小化。但是他们之间怎么交互呢?换种说法就是业务模块应该暴漏什么样的外部接口,以方便其他业务模块来调用?
在终端上业务逻辑主要是围绕着界面展开的。在iOS中的表现就是各式各样的ViewController。而在以往的编码实践中,所谓业务模块间的交互就是VC之间的相互调用。我们常见的是这样的:
UIViewController* aVC = [UIViewController new]; //配置aVC需要参数 .... [self.navigationController pushViewController:aVC animated:YES];
或者这样的
UIViewController* aVC = [UIViewController new]; //配置aVC需要的参数 ... [self presentViewController:aVC animated:YES];
我们通过对于指定业务模块的代码级引用来调用对方的服务。而这种依赖属于接口依赖,稍微符合接口隔离的设计。但不是一个符合迪米特法则(最小知识法则)的设计。调用方由于对于服务提供方有接口依赖,因而就造成了以下的潜在问题:
链接过程中必须引入服务提供方所在的模块,无法提供打包过程中动态下掉某些业务模块的需求。
服务提供方类接口变动,将直接调用方。
调用方知道了服务提供方的太多有冗余信息。
这些问题,我们通过代码级别的模块隔离基本上解决了。正如前文所说,你要真正把模块之间的交互影响降低的最小,最好的解决方案就是建造『信息孤岛』,而信息孤岛就会造成模块之间『鸡犬之声相闻老死不相往来』,这也非我等所愿。他们之间还要保持一个最小的通信,来完成服务的调用。这样我们在代码隔离之后,就要解决两个问题:
模块发现,就是说我一个模块怎么被其他模块发现,或者说我一个模块做些什么事情,外部模块使用的时候,才能知道有我的存在。
服务调用,模块做为服务提供方,需要能够真实的提供所标称的服务。
解决这两个问题,我们首先要说一个观点就是机制与策略分离。我们希望设计的是一整套能够满足上述要求的协议,其次才是实现,最后才是在我们的APP中的具体应用。这也是我这一年来的一个非常重要的总结。并且在逐渐开源出来的一些库中也体现着这个设计。具体说一下,所谓机制即是抽象出来的规则,比如:
f(x)=x^2 x属于R
所谓策略即是在具体场景中的应用,比如当x=2的时候:
f(2)=4 x=2
很明显刚才说的三个层次中协议与实现做成了一个机制与策略分离。而实现与应用又组成了另外的一个机制与策略分离。我比较喜欢这种嵌套的解决方案,你解决了一个通用性的问题,然后嵌套使用,就能够解决更多的问题,只需要付出少量的思维成本。
协议是问题解决方案的描述,或者说要解决这个问题大家都应该遵守的规则。就像网络的tcp协议,你要基于tcp通信你就需要遵循这个协议。
实现是针对于某类环境的实施方案,比如linux上对于TCP的实现还有windows上对于TCP的实现。虽然都是一个协议,但是大家的实现方式不一样,有基于c写的,有基于c++写的.
而应用是真对具体的问题域提出的实施方案,比如我们做了一个哟呵校园的聊天软件使用了tcp进行socket通信。
解决方案设计
模块间通信协议URL
那我们首先要做的就是针对模块间通信问题构思一个协议。一个为了解决模块间通信问题大家都遵守的规则。其实关于这个问题在今年下半年,业界飘来一股router风。大家都在模块化之后的通信问题上作出了不同的尝试。而且甚至为此进行了一场博客间的辩论。仔细分析一下,就能发现大家虽各有意见,但是基本上都同意使用URL的方案来解决这个问题。所争执的不同在于实现方案上的差异。而此处的URL正是我们所谓的协议部分。
统一资源定位符(或称统一资源定位器/定位地址、URL地址等[1],英语:Uniform / Universal Resource Locator,常缩写为URL),有时也被俗称为网页地址(网址)。如同在网络上的门牌,是因特网上标准的资源的地址(Address)。它最初是由蒂姆·伯纳斯-李发明用来作为万维网的地址。现在它已经被万维网联盟编制为因特网标准RFC 1738。
为何如此?
回到最开始我们描述的问题中第一点: 模块发现。其实也就是模块这种资源的定位问题,这个和URL设计的初衷是不谋而合的。URL整套的设计思路就是在整体的互联网中解决信息孤岛,让各个信息孤岛之间能够进行资源发现和资源调用而设计。而我们目前所要处理的模块间通信问题,其实是这个宏大问题域的一个子集。因而选用URL协议,是一个非常顺理成章的事情。另外一点,这里真心没必要重新造一个类似于URL的协议的轮子出来的。URL协议中能够非常完美的解决这个问题。
在iOS中基于URL协议的模块间通信实现DZURLRoute
其实业界这个route的实现已经有千千万万了,为啥我还要再写一个?一个是因为原有的一些库的模型和我所想象的不吻合,一个是因为我实现不想削足适履去适配他们的模型。所以本着造轮子的作孽心态还是自己写。其实也不是非常复杂。
URL协议解决了模块发现的问题,但是是个静态的txt,并不具备exe的能力。我们可以通过定义一个类似于:
yoho://innerfuction/viewcontroller/showuserinfo?uid=22&xx=33
来让一个模块对外宣称支持显示用户详细信息的服务。但是我们要如何使用这个服务呢?很多Route的实现是通过URL直接将对应的ViewController返回,然后由调用方再去调用接口配置ViewController,而后调用方进行push或者present。而我认为这种方式不是很合理,做为调用发应该尽可能少的知道服务提供方的信息。服务怎么被弹起,应该是由服务方自己决定的,而不是调用方。最好只知道一个URL还有支持什么样的服务就好了,最好能把交互接口精简、精简、再精简。
而思考了一下很多route库之所以没能够做到模块只对外暴漏URL就可以的一个很重要的原因,就是在ViewController被弹出的时候,iOS需要一个调起的ViewController
UIViewController* aVC = [UIViewController new]; //配置aVC需要参数 .... [self.navigationController pushViewController:aVC animated:YES];
就是必须知道当前界面是在哪里,你才能去push下一个界面。只有知道这个self.navigationController上下文信息才行。所以很多事情只能在调用方来处理。我觉得这种方式制约着被调用方需要拿到服务提供方的一个实例才行。每一个问题背后都有一个解决方案。于是我在自己造的轮子中使用了全局UI堆栈的方式解决了这个问题。
通过构造了被调用的上下文信息类DZURLRequestContext,用于携带调用方的上下文信息来解决这个问题。上下文信息中携带了当前UI的堆栈信息,能够方便定位用哪个VC做为起点,来弹出下一个页面。当然使用这个context还有另外一个原因就是因为URL中能够传输的参数类型是受限的,只能传输NSString类型,对于一个实例则不能传输。为了传输实例参数也需要这样的一个context环境。
这样在调用一个页面的服务的时候,就能够做到如下所示极致简单:
[[DZURLRoute defaultRoute] routeURL:DZURLRouteQueryLink(kYHURLSacnQRCode, @{})];
当然该库首先是解决了通过URL调用的问题,而后才是上面说的这些优化问题。同时,也针对很多不同的应用场景提供了解决方案。更加具体的信息可以参考DZURLRoute。
而具体的应用问题,就是APP内部自己的事情了,不展开叙述。基本上都是调用库接口的事情,没有太多的表述价值。
Others
做个预告吧,也算是对自己的一个敦促,下一篇说一下在DZURLRoute中关于UI堆栈的问题是怎么解决的。