当前位置: 首页 > 科技观察

关于Android架构,你还在生搬硬套吗?

时间:2023-03-13 18:36:02 科技观察

前言关于Android架构,可能很多人的心中一直都是虚无缥缈的。他们似乎理解它,为了它而使用它,并且到处机械地应用它。这种情况的意义实在是有限。我有多个项目重构的经验,正好对设计领域比较感兴趣。今天,我将毫无保留地与大家分享我对建筑和设计的理解。本文不会具体讲什么是MVC、MVP、MVVM,但我所描述的点应该是这些模式的基石。本质上,明白了为什么要这样做,这样做的好处是什么,在这些底层思想的支持下,再看看相应的架构模式,相信会给你全新的感受。知识储备:你需要掌握Java面向对象和六大设计原则。如果你不明白也没关系。我将尝试详细描述所使用的设计原则。1、模块化的意义是什么?1.1基本概念和底层思想所有的模块化都是为了满足单一的设计原则(字面理解就可以),一个函数或者一个类或者一个模块,职责越单一,复用性越强,可以间接降低耦合.在软件工程的语境下,变化是会有出错的可能的,所以不要说“只要注意就不会出错”之类的话,因为人不是机器。我们能做的就是让模块尽可能的简单。职责越简单,对外部模块的影响就越小,出错的概率也会越低。因此,模块化的核心思想是:单体设计原则1.2我们应该根据什么特点来进行模块化?在做模块化的时候,尽量根据两个特性来进行功能特性、业务特性、功能特性网络、图片加载等。这是一个功能特性。比如网络:我们可以把网络框架的集成和封装写到同一个模块(module、package等)中,这样可以增强可读性(同一个目录一目了然),减少误操作的概率,方便维修,更安全。同时模块也可以托管到maven等远程库中,供多个项目使用,进一步提高复用性。业务特性是否比功能特性具有更高的优先级?比如如下图所示:相信很多人都见过或者正在使用这种分包方式。在业务层,所有的Adapters、Presenters、Activity等都放在对应的包中。这种做法合理吗?首先,答案是不合理的。首先,这个已经在业务层了。我们所做的一切其实都是为业务层服务的,所以业务的优先级应该是最高的。我们应该优先考虑业务层。Traits将相应的类放入同一个包中。功能模块的核心是功能,应该按功能划分模块。业务模块的核心是业务,应该先按业务分模块,再按功能分模块。1.3Android是如何进行分层处理的?前端开发其实就是在做数据处理,然后在视图中展示出来。数据和视图是两个不同的概念。为了提高可重用性和可维护性,我们应该按照单一的设计原则将两者分层,所以无论是MVC、MVP还是MVVM,核心点都是Layerdata和views。绊脚石:一般来说,我们通过网络请求得到的数据结构都是后端定义的,也就是说视图层不得不直接使用后端定义的字段。Layer-->View层会做相应的改变,如下伪代码所示://原逻辑数据层Model{title}UI层View{textView=model.title}//后端调整数据层Model{titleprefix}UI层View{textView=model.prefix+model.title}一开始我们的textView在model中显示的是title,但是后端调整后需要给model添加一个prefix字段,同时此时,textView显示内容也需要显示一次。字符串拼接。视图层由于数据层的变化而被动修改。既然已经分层了,我们想要的就是视图和数据互不干扰。如何解决?往下看...1.4DataMapper或许是解药。DataMapper是后端常用的一个概念。一般不会直接使用数据库中的字段,而是会加一个DataMapper(数据映射),按需将数据库表转换成JavaBeans。这样做的好处也是显而易见的。再硬的表结构,也不会影响到业务层。代码。对于前端,我觉得可以适当引入DataMapper,将后端数据转化为本地模型。本地模型只对应设计图,后端业务与视图完全隔离。这样也解决了1.3面临的问题,具体方法如下:数据层Model{titleprefix}本地模型(与设计图一一对应)LocalModel{//将后端模型转换为本地模型标题=模型.前缀+模型。title}UI层View{textView=localModel.title}LocalModel相当于一个中间层,通过适配器模式将数据层与视图层隔离。前端引入DataMapper后,无需后端即可开发。只要需求明确,视图层的开发就可以搞定,不用担心后端返回的结构体和字段。而且这种方法是一劳永逸的。比如后端需要调整一些字段,我们可以不用想就直接上数据层。所涉及的调整不会100%影响视图层。末端分离更彻底。JavaBean(相当于LocalModel)结构由前端开发人员提供。好处也是显而易见的。更多的业务内聚到后端,大大提高了业务的灵活性。毕竟,应用程序是一次性发布的。成本还是比较大的。面对这种情况,我们其实已经不需要写DataMapper了。因此,任何建筑设计都要结合实际情况,适合自己的才是最好的。1.5无处安放业务逻辑业务逻辑其实是一个很笼统的概念,甚至任何一行代码都可以称为业务逻辑。这么宽泛的概念我们应该怎么理解呢?我大致分为两个方面:界面交互逻辑:view层的交互逻辑,比如手势控制,吊顶悬浮等,都是根据业务需求来实现的,所以严格来说这部分也属于业务逻辑。但是这部分业务逻辑一般是在视图层实现的。数据逻辑:这部分就是大家常说的业务逻辑,属于强业务逻辑,比如根据不同的用户类型获取不同的数据,展示不同的界面,加上DataMapper的一系列操作其实就是给后端的底线和帮助他们完成剩下的逻辑而已。为了方便大家理解,下面我将数据逻辑统称为业务逻辑。前面我们提到,Android开发应该有数据层和视图层,那么哪一层更适合做业务逻辑呢?比如MVVM模式,大家都说业务逻辑应该放在ViewModel中处理,问题不大,但是如果一个接口足够复杂,对应的ViewModel代码可能有几百几千行,看起来臃肿,有可读性差。最重要的一点是,这些业务很难编写单元测试用例。关于业务逻辑,我建议单独写一个用例。用例通常放在ViewModel/Presenter和数据层之间,业务逻辑和DataMapper应该放在用例中,每个行为对应一个用例。这解决了ViewModel/Presenter臃肿的问题,也让编写测试用例变得更简单。注意事项:好的设计是在特定场景下解决特定问题。过度设计解决不了任何问题,反而会增加开发成本。以我目前的经验,至少有一半的Android开发场景是非常简单的:请求-->获取数据-->渲染视图,顶多加个DataMapper。过程很简单,后期改动可能不会太大。这样的话就不用写用例了,直接把DataMapper扔到数据层就可以了。2、合理的分层是为数据驱动的UI做铺垫。一、结论:数据驱动UI的本质是控制反转2.1什么是控制反转?控制就是对程序流程的控制,一般由我们的开发人员来承担。这个过程是控制。但是开发人员也是人,所以错误在所难免。这时候,角色可以反过来,一个成熟的框架来负责整个过程。程序员只需要在框架预留的扩展点上添加自己的业务代码即可。使用框架来驱动整个程序流程的执行,这个过程是相反的。控制反转的概念和设计原则上的依赖反转非常相似,只是少了一个依赖抽象。例如:有一个现有的HTTP请求需求。如果要自己维护HTTP连接、管理TCPSockets、处理HTTP缓存……也就是自己封装整个HTTP协议。先不说这个项目能不能靠个人实施,就算实施了也是漏洞百出。这时候,我们可以换个思路:通过OkHttp来实现。OkHttp是一个成熟的框架,使用起来基本不会出错。个人封装HTTP协议使用OkHttp框架。这个过程颠倒了控制HTTP的角色。个人--->成熟的框架OkHttp是控制反转。好处也是显而易见的。框架错误的概率远低于个人。2.2什么是数据驱动的用户界面?通俗地说,当数据发生变化时,相应的UI也应该发生变化。反之,当需要改变UI时,只需要改变相应的数据即可。目前比较流行的Flutter、Compose、Vue等UI框架,本质上都是基于函数式编程来实现数据驱动的UI。它们的共同目的是解决数据和UI的一致性问题。在目前的Android中,可以使用DataBinding来达到同样的效果。以JetpackMVVM为例:ViewModel从Repository获取数据,暂存在ViewModel对应的ObservableFiled中,实现数据驱动UI,但前提是从Repository获取的数据可以直接使用,如果对数据进行二次处理在Activity或者Adapter中再通知UI,已经违背了数据驱动UI的核心思想。因此,如果要实现数据驱动的UI,必须要有合理的分层(UI层获取的数据不需要处理,可以直接使用)。DataMapper恰好解决了这个问题,也可以避免现在写大量BindAdapter的情况。DataBinding不是函数式编程,只是通过AbstractProcessor生成中间代码,将数据映射到XML2.3为什么数据驱动UI的底层思想是控制反转?目前Android生态系统中只有两个框架可以实现数据绑定UI:DataBinding,Compose(暂未讨论)在引入DataBinding之前通常需要两步渲染一段数据,如下:vartitle="iOS"funsetTitle(){//第一步改变数据源title="Android"//第二步改变UItextView=title}改变数据源和改变UI需要两步。如果忘记修改数据源和UI之一,就会出现BUG。不要说:“我不会忘记修改两者。”当面对复杂的逻辑和十几个数据源时,一个甚至几十个数据源都很难保证数据源没有错误。这类问题可以通过DataBinding来解决。只需更改相应的ObservableFiledUI即可同步修改。控件UI状态也从个人反转为DataBinding。DataBinding不会做个人的疏忽。所以数据驱动UI的底层思想是控制反转2.4为什么要引入Diff?引入diff之前:RecyclerView要实现动态删除、添加和更新,需要手动分别更新数据和UI,这样中间插入一块,数据和UI分别更新,违反了前面提到的数据驱动UI以上,而我们想要的是无论删除、添加或更新,都只有一个条目。只要更改数据源,UI就会更新。要满足这个原则,满足这个原则的唯一方法就是改变数据源。彻底刷新RecyclerView,但是这样会造成性能问题,复杂的界面会感觉到明显的卡顿。引入diff后:Diff算法会通过比较oldItem和newItem的差异,自动更新变化项。同时支持删除和添加动画效果。这个特性解决了RecyclerView实现数据驱动UI需要的性能问题3为什么是我推荐使用函数式编程3.1什么是函数式编程?一进一出。不要在函数链内执行与操作本身无关的操作。函数链内不要使用外部变量(其实这个很难遵守,可以适当突破)。对于目标值,计算过程没有外部权限干预,同时不进行与自身无关的操作,从根本上解决了意外错误的发生。例如://Kotlin代码listOf(10,20).map{it+1}.forEach{Log.i("list","$it")}上面的链式编程是标准的函数式编程,开发者没有输入和输出之间有机会介入(即在Log.i(..)之前,开发者没有权限处理列表),所以整个过程是100%安全的。RxJava、Flow、链式高阶函数都是标准的函数式编程,从规范层面解决数据安全问题。所以我建议在遇到Kotlin中的数据处理时,尽量使用链式高阶函数(RxJava,KotlinFlow也一样)。3.2Android视图开发可以借鉴函数式编程思想。Android视图开发大都遵循以下流程:请求-->处理数据-->渲染UI。在此过程中尽量不要做与当前行为无关的事情(这也需要ViewModel和Repository中的函数符合单一原则)。这个有点笼统,这里举个反例:View{//Refreshfunrefresh(){ViewModel.load(true)}//加载更多funloadMore(){ViewModel.load(false)}}ViewModel{//Loaddataload(isRefresh){if(isRefresh){//Refresh}else{//Loadmore}}}View层有两个行为:刷新和加载更多,load(isRefresh)有一个入口和两个出口。面临的问题是显而易见的。修改、刷新或加载更多都会对对方产生影响,违反了开闭原则中的关闭(为修改而关闭:如果行为没有改变,则不允许修改源代码),导致不可预知的问题。可以参考函数式编程的思想进行改进,将ViewModel的加载函数拆分为refresh和loadMore,这样刷新和加载更多两个行为,两个入口和两个出口互不干扰,两个函数是通过函数的连接形成的。独立的业务链。函数式编程可以约束我们编写标准化的代码。面对无法使用函数式编程的场景,我们可以尝试自律向函数式编程靠拢,也可以达到大致相同的效果。综上所述,合理的分层可以提高复用性,降低模块之间的耦合度。DataMapper可以让视图层和后台分离出来进行开发。复杂的业务逻辑应该写在用例中。数据驱动UI的本质是控制反转。可以通过函数式编程编写更安全的代码。如果您对JetpackMVVM感兴趣,欢迎留言。我可以在下一篇文章中写下自己的看法。