虽然搜刮库在短期内拆分为独立的工程,并实现了绝大部门的两端代码复用,但是好景不长,仅仅更新过几个版本后,由于需求和版本发布周期的差异,搜刮库开始变为两个分支,并且两个分支的差异越来越大,末了代码无法合并而不得不永久维护两个搜刮库。搜刮库毕竟上是一次失败的拆分,其中的题目总结起来有三个:
- 在两端底层差异巨大的情况下自上而下的强行拆分,导致大量实现和适配留在了两端主工程实现,如许的设计层级紊乱,界限含糊,并且极大的增加了业务开发的复杂性;
- 寄希望于两端需求和发版周期完全同等这个想法不切实际,如果在架构上不为两端的差异性预留可伸缩的空间,复用最终是难以一连的;
- 约定或规范,受限于组织架构和具体执行的个人,不确定性太高。
页面组件化实践
在履历过搜刮库的失败拆分后,大家认为现在还不具备实现模块团体拆分和复用的条件,因此我们走向了另一个方向,即实现页面的组件化以告竣部门组件复用的目标。页面组件化的设计思绪是:
- 将页面拆分为粒度更小的组件,组件内部除了包罗UI实现,还包罗数据层和逻辑层;
- 组件提供个性化设置满足两端差异需求,如果无法满足再通过代理抛到上层处理。
页面组件化是一个良好的设计,但它主要适用于解决Activity巨大化的题目。由于底层差异巨大的情况,使得页面组件化很难实现大规模的复用,复用效率低。另一方面,页面组件化也没有为2端差异性预留可伸缩的空间。
MVP分层复用实践
我们还尝试过运用设计模式解决两端代码复用的题目。想法是将代码分为易变的和稳定的两部门,易变部门在两端上层实现差异化处理,稳定部门可以在下层实现复用。方案的主要设计思绪是:
- 借鉴Clean MVP架构,根据职责将代码拆分为Presenter,Data Repository,Use Case,View,Model等角色;
- UI、动画、数据请求等逻辑在下层仅生存接口,在上层实现并注入到下层;
- 对于两端不同等的数据Model,通过转换器适配为下层同一的模子。
架构大致如图:
这是一种机动、优雅的设计,能够实现部门代码的复用,并能解决两端基础库和UI等差异。这个方案在首页和二级频道页的部门模块利用了一段时间,但是由于学习成本较高等缘故原由推广比较缓慢。别的,这个时期平台化已被提上日程,业务痛点决定了我们必须快速实施模块团体的拆分和复用,而优雅的设计模式并不适合解决这一类题目。即使从复用性的角度来看,如许的设计也会使得业务开发变得更为复杂、调试困难,对于新人来说难以胜任,最终推广落地困难。
中间层实践
通过多次实践,我们认识到要实现两端代码复用,基础库的同一是必然的工作,是其他统统工作的基础。否则必然导致复杂和难以维护的设计,最终导致两端复用无法快速推进下去。
计算机界有一句名言:“计算机科学领域的任何题目都可以通过增加一个中间层来解决。”(原始版本出自计算机科学家David Wheeler)我们当然有想过通过中间层设计屏蔽两端的基础库差异。例如网络库,外卖App基于Volley实现,外卖频道基于Retrofit实现。我们曾经在Volley和Retrofit之上封装了一层网络框架,对外暴露同一的接口,上层可以切换底层依赖Volley或是Retrofit。但这个中间层并没有上线,最终我们将两端的网络库同一成了Retrofit。这里面有多个缘故原由:首先Retrofit本身就是较高层次的封装,并且拥有优雅的设计模式,理论上我们很难封装一套扩展性更强的接口;其次长期来看底层网络框架变更的风险极低,并且适配网络层的各种插件也是一件费时费力的事情,因此保持网络中间层的性价比极低;此外将两端的网络请求都替换为中间层接口,显然工作量远大于只生存一端的依赖。
通过实践我们认识到,中间层设计是一把双刃剑。如果基础框架本身的扩展性足够强,中间层设计就显得多此一举,甚至丧失了原有框架的良好特性。
平台化实践
好的架构源于不停地衍变,而非设计。对于外卖Android客户端的平台化架构构建也是履历了同样的过程。我们从考虑怎样解决代码复用的题目,渐渐的衍变成怎样去解决代码复用和平台化的两个题目。而实际上外卖平台化正是解决两端代码复用的一剂良药。我们通过建立外卖平台,将现有的外卖业务降级为一个频道,将外卖业务以aar的形式分别接入到外卖平台和美团平台,如许在解决外卖平台化的同时,代码复用的题目也将得到完善的解决。
平台化架构
经过了整整一年的艰苦奋斗,形成了如图所示的美团外卖Android客户端平台化架构:
从底层到高层依次为平台层、业务层和宿主层。
- 平台层的内容包罗,承载上层的数据通信和页面跳转;提供外卖核心服务,例如商品管理、订单管理、购物车管理等;提供设置管理服务;提供同一的基础设施本领,例如网络、图片、监控、报警、定位、分享、热修、埋点、Crash上报等;提供其他管理本领,例如生命周期管理、组件化等。
- 业务层的内容包罗,外卖业务和垂直业务。
- 宿主层的内容包罗,Waimai App壳和美团外卖频道Waimai-channel壳,这一层用于Application的初始化、dex加载和其他各种必要的组件或基础库的初始化。
在构建平台化架构的过程中,我们遇到如许一个题目,怎样恒久的维持我们平台化架构的层级界限。试想,如果所有的代码都在一个工程里面开发,通过包名、约定去规范层级界限,任何一个告急的需求都大概破坏层级界限。维持层级界限的最好办法是什么?我们的经验是工程隔离。平台化的每一层都去做工程隔离,业务层的每个业务都建立本身的工程库,实现工程隔离。同时,配套编译脚本,检查业务库之间是否存在相互依赖关系。工程隔离的好处是显而易见的:
- 每个工程都可以独立编译、独立打包;
- 每个工程内部的修改,不会影响其他工程;
- 业务库工程可以快速拆分出来,集成到其他App中。
但工程隔离带来的另一个题目是,同层间的业务库需要通信怎么办?这时候就需要提供业务库通信框架来解决这个题目。
业务库通信框架
在拆分外卖商家业务库的时候,我们就发如许一个案例:在商家页有一个业务,当发现当前商家是打烊的,就会弹出一个浮层,推荐相似的商家列表,而在我们之前划分的外卖子业务库里面,相似商家列表应该是属于页面库里面的内容。那怎么让商家业务库访问到页面库里面的代码呢。如果我们将商家库去依赖页面库,那我们的层级界限就会被打破,我们的依赖关系也会变得复杂。因此我们需要在架构中提供同层间的通信框架,它去解决不打破层级界限的情况下,完成同层间的通信。
汇总同层间通信的场景,大致上可以划分为:页面的跳转、根本数据类型的通报(包罗可序列化的共有类对象的通报)、模块内部自界说方法和类的调用。针对上述情况,在我们的架构里面提供了二种平级间的通信方式:scheme路由和美团自建的ServiceLoaders sdk。scheme路由本质上是利用Android的scheme原理进行通信,ServiceLoader本质上是利用的Java反射机制进行通信。
scheme路由的调用如图所示:
最终效果:所有业务页面的跳转,都需要通过平台层的scheme路由去分发。通过scheme路由,所有业务都得到解耦,不再需要相互依赖而可以实现页面的跳转和根本数据类型的通报。
serviceloader的调用如图所示:
提供方和利用方通过平台层的一个接口作为双方交互的约束。利用方通过平台层的ServiceLoader完成提供方的实现对象获取。这种方式可以解决模块内部自界说方法和类的调用,例如我们之条件到了商家库需要调用页面库代码的题目就可以通过ServiceLoader解决。
外卖内核模块设计
在实践的过程中,我们也遇到业务本身上就不好划分层级界限的业务。大家可以从美团外卖三层架构图上,看出外卖业务库,像商家、订单等,是和外卖的垂类业务库是同级的。而实际上外卖业务的子业务是否应该和垂类业务保持同层是一个现在无法确定的事情。
现在,外卖接入的垂类业务商超业务,是隶属于外卖业务的子频道,它依然依赖着外卖的核心model、核心服务,包罗商品管理、订单管理、购物车管理等,因此现在它和外卖业务的商家、订单如许的子业务库同层是没有题目标。但随着商超业务的发展,商超业务未来大概会建设本身的商品管理、订单管理、购物车管理的服务,那么到时商超业务就会上升到和外卖业务一样同层的业务。这时候,外卖核心管理服务,处在平台层,就会导致架构的层级界限变得不再清晰。
我们的解决办法是通过设计一个属于外卖业务的内核模块来适应未来的变革,内核模块的设计如图:
- 内圈为基础模子类,这些模子类构成了外卖核心业务(从门店→点菜→购物车→订单)的基础;
- 中间圈为依赖基础模子类构建的基础服务(CRUD);
- 最外圈为外卖的各维度业务,向内依赖基础模子圈和外卖基础服务圈。
如果未来确定外卖平台需要接入更多和外卖平级的业务,且最内圈都完全不一样,我们将把外卖内核模块上移,在外卖业务子库下建立对内核模块的依赖;如果未来只是有更多的外卖子业务的接入,那就继承生存我们如今的架构;如果未来接入的业务基础模子类一样,但本身的业务服务需要分化,那么我们将对生存内核模块最核心的内圈,并抽象出服务层由外卖和商超上层本身实现真正的服务。
业务库拆分
在拆分业务库的时候,我们面临着如许的题目:业务之间的关系是较为复杂的,怎样去拆分业务库,才是较为公道的呢?一开始我们准备根据外卖业务核心流程:页面→商家→下单,去拆分外卖业务。但是随着外卖子频道业务的快速发展,子频道业务也建立了本身的研发团队,在页面、商家、下单等环节,也开始建立本身的页面。如果我们仍然按照外卖下单的流程去拆分库,那在同一个库之间,就会有外卖团队和外卖子频道团队共同开发的情况,如许职责界限很不清晰,在实际的开发过程中,肯定会出现理不清的情况。
我们都知道软件工程领域有所谓的康威定律:
Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations. - Melvin Conway(1967)
翻译成中文的大概意思是:设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。
在康威定理的指导下:我们认为技术架构应该反映出团队的组织结构,同时,组织结构的变迁,也应该导致技术架构的演进。美团外卖平台下包罗外卖业务和垂直品类业务,对于在我们团队中已经有了组织结构,优先组织结构,去拆出独立的业务库,方便子业务库的同学内部沟通协作,减少他们跨组织沟通的成本。同时,我们将负责外卖业务的大团队,再进一步细化成页面小组、商家小组和订单小组,由这些小组的同学去在外卖业务下完成更细维度的外卖子业务库拆分。根据组织结构划分的业务库,自然的存在业务界限,每个同学都会按照本身业务的目标去继承完善本身的业务库。如许的拆库对内是高内聚,对外是低耦合的,有用的低落了内外沟通协作的成本。
工程内代码隔离
在实现工程隔离之后,我们发现工程内部的代码还是可以相互引用的。工程内部如果也不能实现代码的隔离,那么工程内部的界限就是含糊的。我们希望工程内至少能够实现页面级别的代码隔离,由于Activity是构成一个App的页面单元,围绕这个Activity,通常会有大量的代码及资源文件,我们希望这些代码和资源文件是被会合管理的。
通常我们想到的做法是以module工程为单位的相互隔离,但在module是相对比较重的一个约束,难道每个Activity都要建一个module吗?如许代码结构会变得很复杂,而且针对一些大的业务体,又会形成巨大化的module。
那我们又想到规范代码,用包名去人为约定,但靠包名约束的代码,界限含糊,时不时的告急需求,就把包名约定打破了,而且资源文件的摆放也是任意的,迁移成本高。
那怎么去解决工程内部的界限题目呢?《微信的模块化架构重构实践》一文中提到了一个重要的概念p(pins)工程,p工程可谓是工程内约束代码界限的重要法宝。通过在Gradle里面设置sourceSets,就可以改变工程内的代码结构目录,完成代码的隔离,设置示例:
sourceSets {
main {
def dirs = [‘p_widget’, ‘p_theme’,
‘p_shop’, ‘p_shopcart’,
‘p_submit_order’,‘p_multperson’,‘p_again_order’,
‘p_location’, ‘p_log’,‘p_ugc’,‘p_im’,‘p_share’]
dirs.each { dir ->
java.srcDir(“src/ d i r / j a v a " ) r e s . s r c D i r ( " s r c / dir/java") res.srcDir("src/ dir/java")res.srcDir("src/dir/res”)
}
}
}
效果如图所示:
从图上可以可以看出,这个业务库被以页面为单元拆分成了多个p工程,每个p工程的界限都是清楚的,实现了工程内的代码隔离。工程内代码隔离带来的好处显而易见:
- p工程实现了最小粒度的代码界限约束;
- 工程内模块职责清晰;
- 业务模块可以被快速的拆分出来。
代码复用
p工程满足了工程内代码隔离的需求,但是别忘了,我们每个模块在外卖两个终端上(外卖App&美团App)上大概存在差异,如果能在模块内部实现两端差异,我们的目标才算告竣。基于上述考虑,我们想到了利用Gradle提供的productFlavors来实现两端的差异化。为此,我们需要界说两个flavor:wm和mt。
productFlavors {
wm {}
mt {}
}
但是,如许生成的p工程是并列的,也就是说,各个p工程中所有的差异化代码都需要被存放在这两个flavor对应的SourceSet下,这岂不是跟模块间代码隔离的理念相违背?理想的结构是在p工程内部进行flavor划分,由p工程内部包容差异化,继承改成Gradle脚本如下:
productFlavors {
wm {}
mt {}
}
sourceSets {
def dirs = [‘p_restaurant’, ‘p_goods_detail’, ‘p_comment’, ‘p_compose_order’,
‘p_shopping_cart’, ‘p_base’, ‘p_product_set’]
main {
manifest.srcFile ‘src/p_restaurant/main/AndroidManifest.xml’
dirs.each { dir ->
java.srcDir(“src/ d i r / m a i n / j a v a " ) r e s . s r c D i r ( " s r c / {dir}/main/java") res.srcDir("src/ dir/main/java")res.srcDir("src/{dir}/main/res”)
}
}
wm {
dirs.each { dir ->
java.srcDir(“src/ d i r / w m / j a v a " ) r e s . s r c D i r ( " s r c / {dir}/wm/java") res.srcDir("src/ dir/wm/java")res.srcDir("src/{dir}/wm/res”)
}
}
mt {
dirs.each { dir ->
java.srcDir(“src/ d i r / m t / j a v a " ) r e s . s r c D i r ( " s r c / {dir}/mt/java") res.srcDir("src/ dir/mt/java")res.srcDir("src/{dir}/mt/res”)
}
}
}
最终工程结构变成如下:
通过p工程和flavor的机动应用,我们最终将业务库设置成以p工程为维度的模块单元,并在p工程内部兼容两端的共性及差异,代码复用被很好的解决了。同时,两端差异的题目是归属在p工程内部本身处理的,并没有建立中间层,或将差异抛给上层壳工程去完成,如许的设计遵守了界限清晰,向下依赖的原则。
但是,工程内隔离也存在与工程隔离一样的题目:同层级p工程需要通信怎么办?我们在拆分商家库的时候,就面临这如许的题目,商品活动页和商品详情页,可以根据页面维度,去拆分成2个p工程,这两个页面都会用到同一个商品样式的item。怎样让同层间商品活动页p工程和商品详情页p工程访问到商品样式item呢?在实际拆库的实践中,我们渐渐的探索出三级工程结构。三级工程结构不仅可以解决工程内p工程通信的题目,而且可以保持架构的机动性。
三级工程结构
总结
末了对于程序员来说,要学习的知识内容、技术有太多太多,要想不被情况淘汰就只有不停提升本身,从来都是我们去适应情况,而不是情况来适应我们!
这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司20年的口试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包罗知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部门。
相信它会给大家带来很多收获:
当程序员轻易,当一个良好的程序员是需要不停学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,大概走向管理,从技术经理到技术总监,每个阶段都需要把握不同的本领。早早确定本身的职业方向,才能在工作和本领提升中甩开同龄人。
《Android学习笔记总结+移动架构视频+大厂口试真题+项目实战源码》,点击传送门,即可获取!
当程序员轻易,当一个良好的程序员是需要不停学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,大概走向管理,从技术经理到技术总监,每个阶段都需要把握不同的本领。早早确定本身的职业方向,才能在工作和本领提升中甩开同龄人。
《Android学习笔记总结+移动架构视频+大厂口试真题+项目实战源码》,点击传送门,即可获取!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |