2

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

 1 year ago
source link: http://www.androidchina.net/11784.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

关于Android架构,你是否还在生搬硬套? – Android开发中文站

最新消息:欢迎访问Android开发中文站!商务联系微信:loading_in
你的位置:Android开发中文站 > 热点资讯 > 关于Android架构,你是否还在生搬硬套?

关于Android架构,可能在很多人心里一直都是虚无缥缈的存在,似懂非懂、为了用而用、处处生搬硬套,这种情况使用的意义真的很有限。本人有多个项目重构的经验,恰好对设计领域较为感兴趣,今天我将毫无保留的将自己对架构、设计的理解分享给大家。

本文不会具体去讲什么是MVC、MVP、MVVM,但我描述的点应该都是这些模式的基石,从本质上讲明白为什么这样做,这样做的好处是什么,有了这些底层思想的支持再去看对应的架构模式,相信会让你有一种焕然一新的感觉。

知识储备:需掌握Java面向对象、六大设计原则,如果不理解也无妨,我尽量将用到的设计原则加以详细描述

  • 1. 模块化的意义何在?
    • 1.1 基本概念以及底层思想
    • 1.2 我们要基于哪些特性去做模块化划分?
    • 1.3 Android如何做分层处理?
    • 1.4 Data Mapper或许是解药
    • 1.5 无处安放的业务逻辑
  • 2. 合理分层是给 数据驱动UI 做铺垫
    • 2.1 什么是 控制反转?
    • 2.2 什么是数据驱动UI?
    • 2.3 为什么说数据驱动UI底层思想是控制反转?
    • 2.4 为什么引入Diff?
  • 3. 为什么我建议使用 函数式编程
    • 3.1 什么是 函数式编程?
    • 3.2 Android视图开发可以借鉴函数式编程思想

1. 模块化的意义何在?

1.1 基本概念以及底层思想

所有的模块化都是为了满足单一设计原则 (字面意思理解即可),一个函数或者一个类再或者一个模块,职责越单一复用性就越强,同时能够间接降低耦合性

在软件工程的背景下,改动就会有出错的可能,不要说"我注意一点就不会出错"这种话,因为人不是机器。我们能做的就是尽可能让模块更加单一,职责越单一影响到外层模块的可能性就越小,这样出错的概率也就越低。

所以模块化核心思想即:单一设计原则

1.2 我们要基于哪些特性去做模块化划分?

做模块化处理的时候尽量基于两种特性进行功能特性业务特性

网络、图片加载等等都可称之为功能特性。比如网络:我们可以将网络框架的集成、封装等等写到同一个模块(module、package等)当中,这样可以增强可读性(同一目录一目了然)、降低误操作概率,方便于维护也更加安全。同时也可将模块托管至远程如maven库,可供多个项目使用,进一步提升复用性

业务特性字面意思理解即可,就是我们常常编写的业务,需要以业务的特性进行模块划分

为什么说业务特性优先级要高于功能特性

举个例子如下图:

image.png

相信很多人见过或者正在使用这种分包方式,在业务层把所有的AdapterPresenterActivity等等都放在对应的包中,这种方式合理吗?先说答案不合理,首先这已经是在业务层,我们做的所有事情其实都在为业务层服务,所以业务的优先级应该是最高的,我们应当优先根据业务特性将对应的类放入到同一个包中。

功能模块核心是功能,应当以功能进行模块划分。业务模块核心是业务,应当优先以业务进行模块划分,其次再以功能进行模块划分。

1.3 Android如何做分层处理?

前端开发其实就是做数据搬运,再展示到视图中。数据视图是两个不同的概念,为了提高复用性以及可维护性,我们应当根据单一设计原则我们应当将二者进行分层处理,所以无论是MVCMVP还是MVVM最核心的点都是将数据视图进行分层。

绊脚石:

通常来讲,我们通过网络请求拿到数据结构都是后端定义的,这也就意味着视图层不得不直接使用后端定义的字段,一旦后端进行业务调整会迫使我们前端从数据层-->视图层都会进行对应的改动,如下伪代码所示:

//原始逻辑
数据层
Model{
    title
}
UI层
View{
    textView = model.title
}

//后端调整后
数据层
Model{
    title
    prefix
}
UI层
View{
    textView = model.prefix + model.title
}

起初我们的textView显示的是model中的title,但后端调整后我们需要在model中加一个prefix字段,同时textView显示内容也要做一次字符串拼接。视图层因为数据层的改动而被动做了修改。既然做了分层我们想要的肯定是视图、数据互不干扰,如何解决?往下看…

1.4 Data Mapper或许是解药

Data Mapper是后端常用的一个概念,一般情况下他们是不会直接使用数据库里面的字段,而是加一个Data Mapper(数据映射)将数据库表转按需换成Java Bean,这样做的好处也很明显,表结构甭管怎么折腾都不会影响到业务层代码。

对于前端我觉得可以适当引入Data Mapper,将后端数据转换成本地模型,本地模型只与设计图对应,将后端业务视图完全隔离。这也就解决了 1.3 面临的问题,具体方式如下:

数据层
Model{
    title
    prefix
}
本地模型(与设计图一一对应)
LocalModel{
    //将后端模型转换为本地模型
    title = model.prefix + model.title
}
UI层
View{
    textView = localModel.title
}

LocalModel相当于一个中间层,通过适配器模式将数据层与视图层做隔离。

前端引入Data Mapper后可以脱离后端进行开发,只要需求明确就可以做视图层的开发,完全不需要担心后端返回什么结构字段。并且这种做法是一劳永逸的,比如后端需要对某些字段做调整,我们可以不暇思索直奔数据层,涉及到的调整100%不会影响到视图层

注意点:

当下有一部分公司为了将前后端分离更彻底,由前端开发人员提供Java Bean(相当于LocalModel)的结构,好处也很明显,更多的业务内聚到后端,很大程度提升了业务的灵活性,毕竟App发一次版成本还是比较大的。面对这种情况我们其实没必要再编写Data Mapper。所以任何架构设计都要结合实际情况,适合自己的才是最好的。

1.5 无处安放的业务逻辑

关于业务逻辑其实是一个很笼统的概念,甚至可以将任意一行代码称之为业务逻辑,如此宽泛的概念我们该如何去理解?我先大致将它分为两个方面:

  • 界面交互逻辑:视图层的交互逻辑,比如手势控制、吸顶悬浮等等都是根据业务需要实现的,所以严格来说这部分也属于业务逻辑。但这部分业务逻辑一般在视图层实现。
  • 数据逻辑:这部分是大家常说的业务逻辑,属于强业务逻辑,比如根据不同用户类型获取不同数据、展示不同界面,加上Data Mapper一系列操作其实就是给后端兜底,帮他们补全剩余逻辑而已。为了方便大家理解下文我将数据逻辑统称为业务逻辑

前面我们说到,Android开发应该具备数据层视图层,那业务逻辑放在哪一层比较合适呢?比如MVVM模式下大家都说将业务逻辑放到ViewModel处理,这么说也没有太大的问题,但如果一个界面足够复杂那对应的ViewModel代码可能会有成百上千行,看起来会很臃肿可读性也非常差。最重要的一点这些业务很难编写单元测试用例

关于业务逻辑我建议单独写一个use case处理。

use case通常放在ViewModel/Presenter数据层之间,业务逻辑以及Data Mapper都应该放在use case中,每一个行为对应一个use case。这样就解决了ViewModel/Presenter臃肿的问题,同时更方便编写测试用例。

注意点:

好的设计都是特定场景解决特定问题,过度设计不仅解决不了任何问题反而会增加开发成本。以我目前经验来看Android开发至少一半的场景都很简单:请求-->拿数据-->渲染视图最多再加个Data Mapper,流程很单一并且后期改动的可能也不太大,这种情况就没必要写一个use case,Data Mapper扔到数据层即可。

2. 合理分层是给 数据驱动UI 做铺垫

先说结论:数据驱动UI的本质是控制反转

2.1 什么是 控制反转?

控制即对程序流程的控制,一般由我们开发者承担,此过程为控制。但开发者是人所以不可避免出现错误,此时可以将角色做一个反转由成熟的框架负责整个流程,程序员只需要在框架预留的扩展点上,添加跟自己的业务代码,就可以利用框架来驱动整个程序流程的执行,此过程为反转

控制反转概念和设计原则中的依赖倒置很相似,只是少了一个依赖抽象

打个比方:

现有一个HTTP请求的需求,如果想自己维护HTTT链接、自己管理TCP Socket、自己处理HTTP缓存…..就是整个HTTP协议全部自己封装,先不说这个工程能不能靠个人实现,就算实现也是漏洞百出,此时可以换个思路:通过OkHttp去实现,OkHttp是一个成熟的框架用它基本上不会出错。个人封装HTTP协议到使用OkHttp框架,这个过程在控制HTTP的角色上发生了一个反转个人--->成熟的框架OkHttp即控制反转,好处也很明显,框架出错的概率远低于个人。

2.2 什么是数据驱动UI?

通俗一点说就是当数据改变时对应的UI也要跟着变,反过来说当需要改变UI只需要改变对应的数据即可。现在比较流行的UI框架如FlutterComposeVue其本质都是基于函数式编程实现数据驱动UI,它们共同的目的都是为了解决数据,UI一致性问题。

在当前的Android中可以使用DataBinding实现同样的效果,以Jetpack MVVM为例:ViewModelRepository拿到数据暂存到ViewModel对应的ObservableFiled即可实现数据驱动UI,但前提是从Repository拿到的数据可以直接用,如果在Activity或者Adapter做数据二次处理再notify UI,已经违背数据驱动UI核心思想。所以想实现数据驱动UI必须要有合理的分层(UI层拿到的数据无需处理,可以直接用)Data Mapper恰好解决这一问题,同时也可规避大量编写BindAdapter的现状。

DataBinding并非函数式编程,它只是通过AbstractProcessor生成中间代码,将数据映射到XML中

2.3 为什么说数据驱动UI底层思想是控制反转?

当前Android生态能实现数据绑定UI的框架只有两个:DataBinding、Compose(暂不讨论)

在引入DataBinding之前渲染一条数据通常需要两步,如下:

var title = "iOS"
fun setTitle(){
     //第一步更改数据源
     title = "Android"
     //第二个更改UI
     textView = title
}

共需要两步更改数据源、更改UI,数据源UI有一个忘记修改便会出现BUG,千万不要说:“两个我都不会忘记修改”,当面临复杂的逻辑以及十几个甚至几十个的数据源很难保证不出错。这种问题可以通过DataBinding解决,只需更改对应的ObservableFiledUI便会同步修改,控制UI状态也从个人反转到的DataBinding,个人疏忽的事情DataBinding可不会。

所以说数据驱动UI底层思想是控制反转

2.4 为什么引入Diff?

引入diff之前:

RecyclerView想要实现动态删除、添加、更新需要分别手动更新数据和UI,这样在中间插了一道并且分别更新数据和UI已经违背了前面所说的数据驱动UI,而我们想要的是不管删除、添加或者更新只有一个入口,只要改变数据源就会驱动UI做更新,想要满足这一原则只能改变数据源后对RecyclerView做全部刷新,但这样会造成性能问题,复杂的界面会感到明显的卡顿。

引入diff之后:

Diff算法通过对oldItemnewItem做差异化比对,会自动更新改变的item,同时支持删除、添加的动画效果,这一特性解决了RecyclerView需要实现数据驱动UI的性能问题

3 为什么我建议使用 函数式编程

3.1 什么是 函数式编程?

  • 一个入口,一个出口。
  • 不在函数链内部执行与运算本身无关的操作
  • 不在函数链内部使用外部变量(实际上这一条很难遵守,可以适当突破)

说的通俗点就是给定一个初始值,经过函数链的运行会得到一个目标值,运算的过程中外部没有插手的权限,同时不做与本身无关的操作,从根本上解决了不可预期错误的产生。

举个例子:

//Kotlin代码

listOf(10, 20).map {
   it + 1
}.forEach {
   Log.i("list", "$it")
}

上面这种链式编程就是标准的函数式编程,输入到输出之间开发者根本没有插手的机会(即Log.i(..)之前开发者没有权限处理list),所以整个流程是100%安全的,RxJavaFlow链式高阶函数都是标准的函数式编程,它们从规范层面解决数据安全问题。所以我建议在Kotlin中 碰到数据处理尽量使用链式高阶函数(RxJava、Kotlin Flow亦然)

3.2 Android视图开发可以借鉴函数式编程思想

Android视图开发大都遵循如下流程:请求–>处理数据–>渲染UI,这一流程可以借鉴函数式编程,将请求作为入口,渲染做为出口,在这个流程中尽量不做与当前行为无关的事(这也要求ViewModel,Repository中的函数要符合单一原则)。这样说有点笼统,下面举个反例:

    View{
        //刷新
        fun refresh(){
            ViewModel.load(true)
        }
        //加载更多
        fun loadMore(){
            ViewModel.load(false)
        }
    }

    ViewModel{
        //加载数据
        load(isRefresh){
            if (isRefresh){
                //刷新
            }else{
                //加载更多
            }
        }
    }

View层有刷新、加载更多两种行为,load(isRefresh)一个入口,两个出口。面临的问题很明显,修改刷新加载更多都会对对方产生影响,违反开闭原则中的闭(对修改关闭:行为没变不准修改源代码),导致存在不可预期的问题产生。可以借鉴函数式编程思想对其进行改进,将ViewModelload函数拆分成refreshloadMore,这样刷新加载更多两种行为、两个入口、两个出口互不干涉,通过函数的衔接形成两条独立的业务链条。

函数式编程可以约束我们写出规范的代码,面对不能使用函数式编程的场景,我们可以尝试自我约束往函数式编程方向靠拢,大致也能实现相同的效果。

  • 合理的分层可以提升复用性、降低模块间耦合性
  • Data Mapper 可以让视图层脱离于后端进行开发
  • 复杂的业务逻辑应该写到use case中
  • 数据驱动UI的本质是控制反转
  • 通过函数式编程可以写出更加安全的代码

如果大家对Jetpack MVVM感兴趣欢迎留言,下篇文章我可以写一下自己的看法..

转载请注明:Android开发中文站 » 关于Android架构,你是否还在生搬硬套?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK