iOS应用架构谈 开篇
iOS应用架构谈 view层的组织和调用方案
iOS应用架构谈 网络层设计方案
iOS应用架构谈 本地持久化方案及动态部署
iOS应用架构谈 组件化方案html
前几天的一个晚上在infoQ的微信群里,来自蘑菇街的Limboy作了一个分享,讲了蘑菇街的组件化之路。我不认为这条组件化之路蘑菇街走对了。分享后我私聊了Limboy,Limboy彷佛也明白了问题所在,我答应他我会把个人方案写成文章,因而这篇文章就出来了。ios
另外,按道理说组件化方案也属于iOS应用架构谈的一部分,可是当初构思架构谈时,我没打算写组件化方案,由于我忘了还有这回事儿。。。后来写到view的时候才想起来,因此在view的那篇文章最后补了一点内容。并且以为这个组件化方案太简单,包括实现组件化方案的组件也很简单,代码算上注释也才100行,我就偷懒放过了,毕竟写一篇文章好累的啊。git
本文的组件化方案demo在这里
https://github.com/casatwy/CTMediator 拉下来后记得pod install 拉下来后记得pod install 拉下来后记得pod install
,这个Demo对业务敏感的边界状况处理比较简单,这须要根据不一样App的特性和不一样产品的需求才能作,因此只是为了说明组件化架构用的。若是要应用在实际场景中的话,能够根据代码里给出的注释稍加修改,就能用了。github
蘑菇街的原文地址在这里:《蘑菇街 App 的组件化之路》,没有耐心看完原文的朋友,我在这里简要介绍一下蘑菇街的组件化是怎么作的:golang
这里的两步中,每一步都存在问题。正则表达式
第一步的问题在于,在组件化的过程当中,注册URL并非充分必要条件,组件是不须要向组件管理器注册Url的。并且注册了Url以后,会形成没必要要的内存常驻,若是只是注册Class,内存常驻量就小一点,若是是注册实例,内存常驻量就大了。至于蘑菇街注册的是Class仍是实例,Limboy分享时没有说,文章里我也没看出来,也有多是我看漏了。不过这还并不能算是致命错误,只能算是小缺陷。sql
真正的致命错误在第二步。在iOS领域里,必定是组件化的中间件为openUrl提供服务,而不是openUrl方式为组件化提供服务。数据库
什么意思呢?json
也就是说,一个App的组件化方案必定不是创建在URL上的,openURL的跨App调用是能够创建在组件化方案上的。固然,若是App尚未组件化,openURL方式也是能够创建的,就是丑陋一点而已。swift
为何这么说?
由于组件化方案的实施过程当中,须要处理的问题的复杂度,以及拆解、调度业务的过程的复杂度比较大,单纯以openURL的方式是没法胜任让一个App去实施组件化架构的。若是在给App实施组件化方案的过程当中是基于openURL的方案的话,有一个致命缺陷:很是规对象没法参与本地组件间调度。关于很是规对象
我会在详细讲解组件化方案时有一个辨析。
实际App场景下,若是本地组件间采用GET方式的URL调用,就会产生两个问题:
根本没法表达很是规对象
好比你要调用一个图片编辑模块,不能传递UIImage到对应的模块上去的话,这是一个很悲催的事情。 固然,这能够经过给方法新开一个参数,而后传递过去来解决。好比原来是:
[a openUrl:"http://casa.com/detail?id=123&type=0"];
同时就也要提供这样的方法:
[a openUrl:"http://casa.com/detail" params:@{ @"id":"123", @"type":"0", @"image":[UIImage imageNamed:@"test"] }]
若是不像上面这么作,复杂参数和很是规参数就没法传递。若是这么作了,那么事实上这就是拆分远程调用和本地调用的入口了,这就变成了我文章中提倡的作法,也是蘑菇街方案没有作到的地方。
另外,在本地调用中使用URL的方式实际上是没必要要的,若是业务工程师在本地间调度时须要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。。。在文章下半部分给出的demo代码样例已经说明了业务工程师在本地间调用时,是不须要知道URL的,并且demo代码样例也阐释了如何解决业务工程师遇到传params容易懵逼的问题。
URL注册对于实施组件化方案是彻底没必要要的,且经过URL注册的方式造成的组件化方案,拓展性和可维护性都会被打折
注册URL的目的实际上是一个服务发现的过程,在iOS领域中,服务发现的方式是不须要经过主动注册的,使用runtime就能够了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。若是有调用被弃用了,是常常会忘记删项目的。runtime因为不存在注册过程,那就也不会产生维护的操做,维护成本就下降了。
因为经过runtime作到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都没必要去主工程作操做,十分透明。
蘑菇街采用了openURL的方式来进行App的组件化是一个错误的作法,使用注册的方式发现服务是一个没必要要的作法。并且这方案还有其它问题,随着下文对组件化方案介绍的展开,相信各位天然内心有数。
先来看一下方案的架构图
--------------------------------------
| [CTMediator sharedInstance] | | | | openUrl: <<<<<<<<< (AppDelegate) <<<< Call From Other App With URL | | | | | | | | | |/ | | | | parseUrl | | | | | | | | | .................................|............................... | | | | | | | |/ | | | | performTarget:action:params: <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Call From Native Module | | | | | | | | | | | | |/ | | | | ------------- | | | | | | | runtime | | | | | | | ------------- | | . . | ---------------.---------.------------ . . . . . . . . . . . . . . . . -------------------.----------- ----------.--------------------- | . | | . | | . | | . | | . | | . | | . | | . | | | | | | Target | | Target | | | | | | / | \ | | / | \ | | / | \ | | / | \ | | | | | | Action Action Action ... | | Action Action Action ... | | | | | | | | | | | | | |Business A | | Business B | ------------------------------- --------------------------------
这幅图是组件化方案的一个简化版架构描述,主要是基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用作了拆分,并且是由本地应用调用为远程应用调用提供服务,与蘑菇街方案正好相反。
调用方式
先说本地应用调用,本地组件A在某处调用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]
向CTMediator
发起跨组件调用,CTMediator
根据得到的target和action信息,经过objective-C的runtime转化生成target实例以及对应的action选择子,而后最终调用到目标业务提供的逻辑,完成需求。
在远程应用调用中,远程应用经过openURL的方式,由iOS系统根据info.plist里的scheme配置找到能够响应URL的应用(在当前咱们讨论的上下文中,这就是你本身的应用),应用经过AppDelegate
接收到URL以后,调用CTMediator
的openUrl:
方法将接收到的URL信息传入。固然,CTMediator
也能够用openUrl:options:
的方式顺便把随之而来的option也接收,这取决于你本地业务执行逻辑时的充要条件是否包含option数据。传入URL以后,CTMediator
经过解析URL,将请求路由到对应的target和action,随后的过程就变成了上面说过的本地应用调用的过程了,最终完成响应。
针对请求的路由操做不多会采用本地文件记录路由表的方式,服务端常常处理这种业务,在服务端领域基本上都是经过正则表达式来作路由解析。App中作路由解析能够作得简单点,制定URL规范就也能完成,最简单的方式就是scheme://target/action
这种,简单作个字符串处理就能把target和action信息从URL中提取出来了。
组件仅经过Action暴露可调用接口
全部组件都经过组件自带的Target-Action来响应,也就是说,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程当中,对Business的侵入,同时也提升了组件化接口的可维护性。
--------------------------------
| |
| Business A |
| |
--- ---------- ---------- ---
| | | | | |
| | | | | |
...........| |........| |........| |...........
. | | | | | | .
. | | | | | | .
. --- --- --- --- --- --- .
. | | | | | | .
. |action| |action| |action| .
. | | | | | | .
. ---|---- -----|-- --|----- .
. | | | .
. | | | .
. ----|------ --|--------|-- .
. | | | | .
. |Target_A1| | Target_A2 | .
. | | | | .
. ----------- -------------- .
. .
. .
..................................................
你们能够看到,虚线圈起来的地方就是用于跨组件调用的target和action,这种方式避免了由BusinessA直接提供组件间调用会增长的复杂度,并且任何组件若是想要对外提供调用服务,直接挂上target和action就能够了,业务自己在大多数场景下去进行组件化改造时,是基本不用动的。
复杂参数和很是规参数,以及组件化相关设计思路
这里咱们须要针对术语作一个理解上的统一:
复杂参数
是指由普通类型
的数据组成的多层级参数。在本文中,咱们定义只要是可以被json解析的类型就都是普通类型
,包括NSNumber, NSString, NSArray, NSDictionary,以及相关衍生类型,好比来自系统的NSMutableArray或者你本身定义的都算。
总结一下就是:在本文讨论的场景中,复杂参数的定义是由普通类型组成的具备复杂结构的参数。普通类型的定义就是指可以被json解析的类型。
很是规参数
是指由普通类型
之外的类型组成的参数,例如UIImage等这些不可以被json解析的类型。而后这些类型组成的参数在文中就被定义为很是规参数
。
总结一下就是:很是规参数
是包含很是规类型的参数。很是规类型
的定义就是不能被json解析的类型都叫很是规类型。
边界状况:
举个例子就是经过json描述的自定义view。若是这个view可以经过某个组件被转化成json,那么即便这个view自己并非普通类型,在具备转化器的上下文场景中,咱们依旧认为它是普通类型。
而后我来解释一下为何应该由本地组件间调用来支持远程应用调用:
在远程App调用时,远程App是不可能经过URL来提供很是规参数的,最多只能以json string的方式通过URLEncode以后再经过GET来提供复杂参数,而后再在本地组件中解析json,最终完成调用。在组件间调用时,经过performTarget:action:params:
是可以提供很是规参数的,因而咱们能够知道,远程App调用
时的上下文环境以及功能是本地组件间调用
时上下文环境以及功能的子集
。
所以这个逻辑注定了必须由本地组件间调用来为远程App调用来提供服务,只有符合这个逻辑的设计思路才是正确的组件化方案的设计思路,其余跟这个不一致的思路必定就是错的。由于逻辑上子集为父集提供服务说不通,因此强行这么作的话,用一个成语来总结就叫作倒行逆施。
另外,远程App调用和本地组件间调用必需要拆分开,远程App调用只能走CTMediator
提供的专用远程的方法,本地组件间调用只能走CTMediator
提供的专用本地的方法,二者不能经过同一个接口来调用。
这里有两个缘由:
远程App调用处理入参的过程比本地多了一个URL解析的过程,这是远程App调用特有的过程。这一点我前面说过,这里我就不细说了。
架构师没有充要条件条件能够认为远程App调用对于无响应请求的处理方式和本地组件间调用无响应请求的处理方式在将来产品的演进过程当中是一致的
在远程App调用中,用户经过url进入app,当app没法为这个url提供服务时,常见的办法是展现一个所谓的404界面,告诉用户"当前没有相对应的内容,不过你能够在app里别的地方再逛逛"。这个场景多见于用户使用的App版本不一致。好比有一个URL只有1.1版本的app能完整响应,1.0版本的app虽然能被唤起,可是没法完成整个响应过程,那么1.0的app就要展现一个404了。
在组件间调用中,若是遇到了没法响应的请求,就要分两种场景考虑了。
场景1
若是这种没法响应的请求发生场景是在开发过程当中,好比两个组件同时在开发,组件A调用组件B时,组件B还处于旧版本没有发布新版本,所以响应不了,那么这时候的处理方式能够相对随意,只要能体现B模块是旧版本就好了,最后在RC阶段统测时是必定可以发现的,只要App没发版,怎么处理都来得及。
场景2
若是这种没法响应的请求发生场景是在已发布的App中,有可能展现个404就结束了,那这就跟远程App调用时的404处理场景同样。但也有可能须要为此作一些额外的事情,有可能由于作了额外的事情,就不展现404了,展现别的页面了,这一切取决于产品经理。
那么这种场景是如何发生的呢?
我举一个例子:当用户在1.0版本时收藏了一个东西,而后用户升级App到1.1版本。1.0版本的收藏项目在本地持久层存入的数据有多是会跟1.1版本收藏时存入的数据是不一致的。此时用户在1.1版本的app中对1.0版本收藏的东西作了一些操做,触发了本地组件间调用,这个本地间调用又与收藏项目自己的数据相关,那么这时这个调用就是有可能变成无响应调用,此时的处理方式就不见得跟之前同样展现个404页面就结束了,由于用户已经看到了收藏了的东西,结果你还告诉他找不到,用户马上懵逼。。。这时候的处理方式就会用不少种,至于产品经理会选择哪一种,你做为架构师是没有办法预测的。若是产品经理提的需求落实到架构上,对调用入口产生要求然而你的架构又没有拆分调用入口,对于你的选择就只有两个:要么打回产品需求,要么加个班去拆分调用入口。
固然,架构师能够选择打回产品经理的需求,最终挑选一个本身的架构可以承载的需求。可是,若是这种是由于你早期设计架构时挖的坑而打回的产品需求,你不以为丢脸么?
鉴于远程app调用和本地组件间调用下的无响应请求处理方式不一样,以及将来不可知的产品演进,拆分远程app调用入口和本地组件间调用入口是功在当代利在千秋的事情。
组件化方案中的去model设计
组件间调用时,是须要针对参数作去model化的。若是组件间调用不对参数作去model化的设计,就会致使业务形式上被组件化了,实质上依然没有被独立
。
假设模块A和模块B之间采用model化的方案去调用,那么调用方法时传递的参数就会是一个对象。
若是对象不是一个面向接口的通用对象,那么mediator的参数处理就会很是复杂,由于要区分不一样的对象类型。若是mediator不处理参数,直接将对象以范型的方式转交给模块B,那么模块B必然要包含对象类型的声明。假设对象声明放在模块A,那么B和A之间的组件化只是个形式主义。若是对象类型声明放在mediator,那么对于B而言,就不得不依赖mediator。可是,你们能够从上面的架构图中看到,对于响应请求的模块而言,依赖mediator并非必要条件,所以这种依赖是彻底不须要的,这种依赖的存在对于架构总体而言,是一种污染。
若是参数是一个面向接口的对象,那么mediator对于这种参数的处理其实就不必了,更多的是直接转给响应方的模块。并且接口的定义就不可能放在发起方的模块中了,只能放在mediator中。响应方若是要完成响应,就也必需要依赖mediator,然而前面我已经说过,响应方对于mediator的依赖是没必要要的,所以参数其实也并不适合以面向接口的对象的方式去传递。
所以,使用对象化的参数不管是否面向接口,带来的结果就是业务模块形式上是被组件化了,但实质上依然没有被独立。
在这种跨模块场景中,参数最好仍是以去model化的方式去传递,在iOS的开发中,就是以字典的方式去传递。这样就可以作到只有调用方依赖mediator,而响应方不须要依赖mediator。然而在去model化的实践中,因为这种方式自由度太大,咱们至少须要保证调用方生成的参数可以被响应方理解,然而在组件化场景中,限制去model化方案的自由度的手段,相比于网络层和持久层更加容易得多。
由于组件化自然具有了限制手段:参数不对就没法调用!没法调用时直接debug就能很快找到缘由。因此接下来要解决的去model化方案的另外一个问题就是:如何提升开发效率。
在去model的组件化方案中,影响效率的点有两个:调用方如何知道接收方须要哪些key的参数?调用方如何知道有哪些target能够被调用?其实后面的那个问题无论是否是去model的方案,都会遇到。为何放在一块儿说,由于我接下来要说的解决方案能够把这两个问题一块儿解决。
解决方案就是使用category
mediator这个repo维护了若干个针对mediator的category,每个对应一个target,每一个category里的方法对应了这个target下全部可能的调用场景,这样调用者在包含mediator的时候,自动得到了全部可用的target-action,不管是调用仍是参数传递,都很是方便。接下来我要解释一下为何是category而不是其余:
category自己就是一种组合模式,根据不一样的分类提供不一样的方法,此时每个组件就是一个分类,所以把每一个组件能够支持的调用用category封装是很合理的。
在category的方法中能够作到参数的验证,在架构中对于保证参数安全是颇有必要的。当参数不对时,category就提供了补救的入口。
category能够很轻松地作请求转发,若是不采用category,请求转发逻辑就很是难作了。
category统一了全部的组件间调用入口,所以不管是在调试仍是源码阅读上,都为工程师提供了极大的方便。
因为category统一了全部的调用入口,使得在跨模块调用时,对于param的hardcode在整个App中的做用域仅存在于category中,在这种场景下的hardcode就已经变成和调用宏或者调用声明没有任何区别了,所以是能够接受的。
这里是业务方使用category调用时的场景,你们能够看到很是方便,不用去记URL也不用纠结到底应该传哪些参数。
if (indexPath.row == 0) { UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail]; // 得到view controller以后,在这种场景下,到底push仍是present,实际上是要由使用者决定的,mediator只要给出view controller的实例就行了 [self presentViewController:viewController animated:YES completion:nil]; } if (indexPath.row == 1) { UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail]; [self.navigationController pushViewController:viewController animated:YES]; } if (indexPath.row == 2) { // 这种场景下,很明显是须要被present的,因此没必要返回实例,mediator直接present了 [[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]]; } if (indexPath.row == 3) { // 这种场景下,参数有问题,所以须要在流程中作好处理 [[CTMediator sharedInstance] CTMediator_presentImage:nil]; } if (indexPath.row == 4) { [[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) { // 作你想作的事 NSLog(@"%@", info); }]; }
本文对应的demo展现了如何使用category来实现去model的组件调用。上面的代码片断也是摘自这个demo。
基于安全考虑
咱们须要防止黑客经过URL的方式调用本属于native的组件,好比支付宝的我的财产页面。若是在调用层级上没有区分好,没有作好安全措施,黑客就有经过safari查看任何人的我的财产的可能。
安全措施其实有不少,大部分取决于App自己以及产品的要求。在架构层面要作的最基础的一点就是区分调用是来自于远程App仍是本地组件,我在demo中的安全措施是采用给action添加native
前缀去作的,凡是带有native前缀的就都只容许本地组件调用,若是在url阶段发现调用了前缀为native的方法,那就能够采起响应措施了。这也是将远程app调用入口和本地组件调用入口区分开来的重要缘由之一。
固然,为了确保安全的作法有不少,但只要拆出远程调用和本地调用,各类作法就都有施展的空间了。
基于动态调度考虑
动态调度的意思就是,今天我可能这个跳转是要展现A页面,可是明天可能一样的跳转就要去展现B页面了。这个跳转有多是来自于本地组件间跳转也有多是来自于远程app。
作这个事情的切点在本文架构中,有不少个:
若是以url parse为切点
的话,那么这个动态调度就只可以对远程App跳转产生影响,失去了动态调度本地跳转的能力,所以是不适合的。
若是以实例化target时为切点
的话,就须要在代码中针对全部target都作一次审查,看是否要被调度,这是不必的。假设10个调用请求中,只有1个要被动态调度,那么就必需要审查10次,只有那1次审查经过了,才走动态调度,这是一种相对比较粗暴的方法。
若是以category调度方法为切点
的话,那动态调度就只能影响到本地件组件的跳转,由于category是只有本地才用的,因此也不适合。
以target下的action为切点
是最适合的,由于动态调度在通常场景下都是有范围的,大多数是活动页须要动态调度,今天这个活动明天那个活动,或者今天活动正在进行明天活动就结束了,因此产生动态调度的需求。咱们在可能产生动态调度的action中审查当前action是否须要被动态调度,在常规调度中就不必审查了,例如我的主页的跳转,商品详情的跳转等,这样效率就能比较高。
你们会发现,若是要作相似这种效率更高的动态调度,target-action层被抽象出来就是必不可少的,然而蘑菇街并无抽象出target-action层,这也是其中的一个问题。
固然,若是你的产品要求全部页面都是存在动态调度需求的,那就仍是以实例化target时为切点
去调度了,这样能作到审查每一次调度请求,从而实现动态调度。
说完了调度切点,接下来要说的就是如何完成审查流程。完整的审查流程有几种,我每一个都列举一下:
这两种作法其实均可以,若是产品对即时性的要求比较高,那么采用第二种方案,若是产品对即时性要求不那么高,第一种方案就能够了。因为本文的方案是没有URL注册列表的,所以服务器只要给出原始target-action和对应跳转的target-action就能够了,整个流程不是只有注册URL列表才能达成的,并且这种方案比注册URL列表要更易于维护一些。
另外,说采用url rewrite的手段来进行动态调度,也不是不能够。可是这里我须要辨析的是,URL的必要性仅仅体如今远程App调度中,是不必蔓延到本地组件间调用的。这样,当咱们作远程App的URL路由时(目前的demo没有提供URL路由功能,可是提供了URL路由操做的接入点,能够根据业务需求插入这个功能),要关心的事情就能少不少,能够比较干净。在这种场景下,单纯以URL rewrite的方式其实就与上文提到的以url parse为切点
没有区别了。
不拆分远程调用和本地间调用,就使得后续不少手段难以实施,这个我在前文中都已经有论述了。另外再补充一下,这里的拆分不是针对来源作拆分。好比经过URL来区分是远程App调用仍是本地调用,这只是区分了调用者的来源。
这里说的区分是指:远程调用走远程调用路径,也就是openUrl
->urlParse
->perform
->target-action
。本地组件间调用就走本地组件间调用路径:perform
->target-action
。这两个是必定要做区分的,蘑菇街方案并无对此作好区分。
这是本末倒置的作法,倒行逆施致使的是将来架构难觉得业务发展提供支撑。由于前面已经论述过,在iOS场景下,远程调用的实现是本地调用实现的子集,只有大的为小提供服务,也就是本地调用为远程调用提供服务,若是反过来就是倒行逆施了。
注意这里复杂参数
和很是规参数
的辨析。
因为采用远程调用的方式执行本地调用,在前面已经论述过二者功能集的关系,所以这种作法没法知足传递很是规参数的需求。并且若是基于这种方式不变的话,复杂参数的传递也只能依靠通过urlencode的json string进行,这种方式很是丑陋,并且也不便于调试。
这个条件在组件化方案中是没必要要条件,demo也已经证明了这一点。这个没必要要的操做会致使没必要要的维护成本,若是单纯从只要完成业务就好
的角度出发,这倒不是什么大问题。这就看架构师对本身是否是要求严格了。
在本文给出的组件化方案中,响应者惟一要作的事情就是提供Target和Action,并不须要再作其它的事情。蘑菇街除此以外还要再作不少额外没必要要措施,才能保证调用成功。
这种作法使得全部的跨组件调用请求直接hit到业务模块,业务模块必然所以变得臃肿难以维护,属于侵入式架构。应该将本来属于调用相应的部分拿出来放在target-action中,才能尽量保证不将无关代码侵入到原有业务组件中,才能保证业务组件将来的迁移和修改不受组件调用的影响,以及下降为项目的组件化实施而带来的时间成本。
本文提供的组件化方案是采用Mediator模式和苹果体系下的Target-Action模式设计的。
然而这款方案有一个很小的缺陷在于对param的key的hardcode,这是为了达到最大限度的解耦和灵活度而作的权衡。在个人网络层架构和持久层架构中,都没有hardcode的场景,这也从另外一个侧面说明了组件化架构的特殊性。
权衡时,考虑到这部分hardcode的影响域仅仅存在于mediator的category中
。在这种状况下,hardcode对于调用者的调用是彻底透明的。对于响应者而言,处理方式等价于对API返回的参数的处理方式,且响应者的处理方式也被限制在了Action中
。
所以这部分的hardcode的存在虽然确实有点不干净,可是相比于这些不干净而带来的其余好处而言,在权衡时是能够接受的,若是不采用hardcode,那势必就会致使请求响应方也须要依赖mediator
,然而这在逻辑上是没必要要
的。另外,在个人各个项目的实际使用过程当中,这部分hardcode是没有影响的。
另外要谈的是,之因此会在组件化方案中出现harcode,而网络层和持久层的去model化都没有发生hardcode状况,是由于组件化调用的全部接受者和调用者都在同一片上下文里。网络层有一方在服务端,持久层有一方在数据库。再加上设计时针对hardcode部分的改进手段其实已经超出了语言自己的限制。也就是说,harcode受限于语言自己。objective-C也好,swift也好,它们的接口设计哲学是存在缺陷的。若是咱们假设在golang的背景下,是彻底能够用golang的接口体系去作一个最优美的架构方案出来的。不过这已经不属于本文的讨论范围了,有兴趣的同窗能够去了解一下相关知识。架构设计有时就是这么无奈。
组件化方案在App业务稳定,且规模(业务规模和开发团队规模)增加初期去实施很是重要,它助于将复杂App分而治之,也有助于多人大型团队的协同开发。但组件化方案
不适合在业务不稳定的状况下过早实施,至少要等产品已经通过MVP阶段时才适合实施组件化。由于业务不稳定意味着链路不稳定,在不稳定的链路上实施组件化会致使未来主业务产生变化时,全局性模块调度和重构会变得相对复杂。
当决定要实施组件化方案时,对于组件化方案的架构设计优劣直接影响到架构体系可否长远地支持将来业务的发展,对App的组件化不仅是仅仅的拆代码和跨业务调页面
,还要考虑复杂和很是规业务参数参与的调度,非页面的跨组件功能调度,组件调度安全保障,组件间解耦,新旧业务的调用接口修改等问题。
蘑菇街的组件化方案只实现了跨业务页面调用的需求,本质上只实现了我在view层架构的文章中跨业务页面调用的内容,这尚未到成为组件化方案
的程度,且蘑菇街的组件化方案距离真正的App组件化的要求仍是差了一段距离的,且存在设计逻辑缺陷,但愿蘑菇街可以加紧重构,打造真正的组件化方案。
没想到limboy如此迅速地发文回应了。文章地址在这里:蘑菇街 App 的组件化之路 续。而后我花了一些时间从新看了limboy的第一篇文章。我以为在本文开头我对蘑菇街的组件化方案描述过于简略了,并且我还忽略了原来是有ModuleManager
的,因此在这里我从新描述一番。
第一种是经过
MGJRouter
的registerURLPattern:toHandler:
进行注册,将URL和block绑定。这个方法前面一个参数传递的是URL,例如mgj://detail?id=:id
这种,后面的toHandler:
传递的是一个^(NSDictionary *routerParameters){// 此处能够作任何事}
的block。
当组件执行[MGJRouter openURL:@"mgj://detail?id=404"]
时,根据以前registerURLPattern:toHandler:
的信息,找到以前经过toHandler:
收集的block,而后将URL中带的GET参数,此处是id=404
,传入block中执行。若是在block中执行NSLog(routerParameters)
的话,就会看到@{@"id":@"404"}
,所以block中的业务就可以获得执行。
而后为了业务方可以不生写URL,蘑菇街列出了一系列宏或者字符串常量(具体是宏仍是字符串我就不是很肯定,没看过源码,但limboy文章中有提到经过一个后台系统生成一个装满URL的源码文件)来表征URL。在openURL
时,不管是远程应用调用仍是本地组件间调用,只要传递的参数不复杂,就都会采用openURL
的方式去唤起页面,由于复杂的参数和很是规参数这种调用方式就没法支持了。
缺陷在于:这种注册的方式实际上是没必要要的,并且还白白使用URL
和block
占用了内存。另外还有一个问题就是,即使是简单参数的传递,若是参数比较多,业务工程师不看原始URL字符串是没法知道要传递哪些参数的。
蘑菇街之因此采用id=:id
的方式,我猜是为了怕业务工程师传递多个参数顺序不一样会致使问题,而使用的占位符。这种作法在持久层生成sql字符串时比较常见。不过这个功能我没在limboy的文章中看到有写,不知道实现了没有。
在本文提供的组件化方案中,由于没有注册,因此就没有内存的问题。由于经过category提供接口调用,就没有参数的问题。对于蘑菇街来讲,这种作法其实并无作到拆分远程应用调用和本地组件间调用的目的,而不拆分会致使的问题我在文章中已经论述过了,这里就很少说了。
因为前面openURL的方式不可以传递很是规参数,所以有了第二种注册方式:新开了一个对象叫作
ModuleManager
,提供了一个registerClass:forProtocol:
的方法,在应用启动时,各组件都会有一个专门的ModuleEntry
被唤起,而后ModuleEntry
将@protocol
和Class
进行配对。所以ModuleManager
中就有了一个字典来记录这个配对。
当有涉及很是规参数的调用时,业务方就不会去使用[MGJRouter openURL:@"mgj://detail?id=404"]
的方案了,转而采用ModuleManager
的classForProtocol:
方法。业务传入一个@protocol
给ModuleManager
,而后ModuleManager
经过以前注册过的字典查找到对应的Class
返回给业务方,而后业务方再本身执行alloc
和init
方法获得一个符合刚才传入@protocol
的对象,而后再执行相应的逻辑。
这里的ModuleManager
其实跟以前的MGJRouter
同样,是没有任何须要去注册协议和类名的。并且不管是服务提供者调用registerClass:forProtocol:
也好,服务的调用者调用classForProtocol:
,都必须依赖于同一个protocol。蘑菇街把全部的protocol放入了一个publicProtocol.h的文件中,所以调用方和响应方都必须依赖于同一个文件。这个我在文章中也论述过:响应方在提供服务的时候,是不须要依赖任何人的。
普通参数调用
和很是规参数调用
。不去区分远程应用调用和本地组件间调用的缺陷我在文中已经论述过了,这里很少说。
openURL
方式,还提供了ModuleManager
方式,然而所谓的咱们实际上是分为「组件间调用」和「页面间跳转」两个维度,只要 app 响应某个 URL,不管是 app 内仍是 app 外均可以,而「组件间」调用走的彻底是另外一条路,因此也不会有安全上的问题。
其实也是不成立的,由于openURL
方式也出如今了本地组件间调用中,这在他第一篇文章里的组件间通讯
小节中就已经说了采用openURL
方式调用了,这是有可能产生安全问题的。并且这段话也认可了openURL
方式被用于本地组件间调用,又印证了我刚才说的第一点。
openURL
场景下,仍是出现了以远程调用的方式为本地间调用提供服务
的问题,这个问题我也已经在文中论述过了。
openURL
方案和protocol - class
方案,因此其实以前我指出蘑菇街本地间调用不能传递很是规参数和复杂参数是不对的
,应该是蘑菇街在本地间调用时若是是普通参数,那就采用openURL
,若是是很是规参数,那就采用protocol - class
了,这个作法对于本地间调用的管理和维护,显而易见是不利的。。。
必需要在 app 启动时注册 URL 响应者
这步不可避免,但没有说缘由。个人demo已经证明了注册是没必要要的,因此我想听听limboy如何解释缘由。
按照你的方案来看,红圈的地方是不可能没有依赖的。。。
认为
category
在某种意义上也是一个注册过程。
蘑菇街的注册和我这里的category实际上是两回事,并且我不管如何也没法理解把category和注册URL等价联系的逻辑😂
一个很简单的事实就能够证实二者彻底不等价了:个人方案若是没有category,照样能够跑,就是业务方调用丑陋一点。蘑菇街若是不注册URL,整个流程就跑不起来了~
认为openURL的好处是
能够更少地关心业务逻辑
,本文方案的好处是能够很方便地完成参数传递。
我没以为本文方案关心的业务逻辑比openURL
更多,由于二者比较起来,都是传参数发调用请求,在关心业务逻辑
的条件下,二者彻底同样。惟一的不一样就是,我能传很是规参数而openURL
不能。本文方案的整个过程当中,在调用者这一方是彻底没有涉及到任何属于响应者的业务逻辑的。
认为
protocol/URL注册
和将target-action抽象出调用接口
是等价的
这其实只是效果等价了,二者真正的区别在于:protocol对业务产生了侵入,且不符合黑盒模型。
因为业务中的某个对象须要被调用,所以必需要符合某个可被调用的protocol,然而这个protocol又不存在于当前业务领域,因而当前业务就不得不依赖publicProtocol。这对于未来的业务迁移是有很是大的影响的。
蘑菇街的protocol方式使对象要在调用者处使用,因为调用者并不包含对象本来所处的业务领域,当完成任务须要多个这样的对象的时候,就须要屡次经过protocol得到class来实例化多个对象,最终才能完成需求。
可是target-action
模式保证了在执行组件间调用的响应时,执行的上下文处于响应者环境中,这跟蘑菇街的protocol方案相比就是最大的差异。由于从黑盒理论上讲,调用者只管发起请求,请求的执行应该由响应者来负责,所以执行逻辑必须存在于响应者的上下文内,而不能存在于调用者的上下文内。
举个具体一点的例子就是,当你发起了一个网页请求,后端取好数据渲染好页面,不管获取数据涉及多少渠道,获取数据的逻辑都在服务端完成,而后再返回给浏览器展现。这个是正确的作法,target-action模式也是这么作的。
可是蘑菇街的方案就变成了这样:你发起了一个网络请求,后端返回的不是数据,返回的居然是一个数据获取对象(DAO),而后你再经过DAO去取数据,去渲染页面,若是渲染页面的过程涉及多个DAO,那么你还要再发起更多请求,拿到的仍是DAO,而后再拿这个DAO去获取数据,而后渲染页面。这是一种很是诡异的作法。。。
若是说这么作是为了应对执行业务的过程当中,须要根据中间阶段的返回值来决定接下来的逻辑走向的话,那也应该是屡次调用得到数据,而后决定接下来的业务走向,而不是每次拿到的都是DAO啊。。。使用target-action方式来应对这种场景其实也很天然啊~