当前位置: 首页 > 产品展示 > 数码模块

体育williamhill

PRODUCTS
×

william威廉希尔:Android 项目架构设计深入浅出

发布时间:2022-02-01 01:50:05 来源:william威廉希尔 作者:williamhill吧

  前言:本文结合个人在架构设计上的思考和理解,介绍如何从0到1设计一个大型Android项目架构。

  该章节主要对一个Android项目架构从0到1再到N的演进历程做出总结(由于项目的开发受业务、团队和排期等各方面因素影响,因此该总结不会严格匹配每一步的演进历程,但足以说明项目发展阶段的一般性规律)。

  对于一个新开启项目而言,每端的开发人员通常非常有限,往往只有1-2个。这时候比项目的架构设计和各种开发细节更重要的是开发周期,快速将idea进行落地是该阶段最重要的目标。现阶段项目的架构往往是这样

  此时项目中几乎所有的代码都会写在一个独立的app模块中,在时间为王的背景下,最原始的开发模式往往就是最好最高效的。

  随着项目最小化MVP已经开发完成,接下来打算继续完善App。此时大概率会遇到以下几个问题:

  1.代码的版本控制问题,为保证项目加快迭代,团队新招1-3名开发同学,多人同时在一个项目上开发时,Git代码合并总会出现冲突,非常影响开发效率;

  2.项目的编译构建问题,随着项目代码量逐渐增多,运行App都是基于源码编译,以至于首次整包编译构建的速度逐渐变慢,甚至会出现为了验证一行代码的改动而需要等待大几分钟或者更久时间的现象;

  3.多应用的代码复用问题,公司可能同时在进行多个App的开发,同样的代码总是需要通过复制粘贴的方式进行复用,维持同一个功能在多个App之间的逻辑一致性也会存在问题;

  基于以上的一种或多种原因,我们往往会把那些相对于整个项目而言,一旦开发完成后就很少再改动的功能进行模块化封装。

  我们把原本只包含一个应用层的项目,向下抽取了一个包含网络库、图片加载库和UI库等众多原子能力库的基础层。这样做之后,对于协同开发、整包构建和代码复用都起到了很大的改善作用。

  在这个时候往往非常关键,随着业务增长、客户使用量增大、迭代需求增多等各方面挑战。如果项目没有一套良性的架构设计,开发的人效会随着团队规模的扩大而反向降低,之前单位时间内1个人能开发5个需求,现在10个人用同样的时间甚至连20个需求都开发不完,单纯的依靠加人是很难彻底解决这个问题的。这时候着重需要做的两件事

  1.开发职责分离,团队成员需要分为两部分,分别对应业务开发和基础架构。业务开发组负责完成日常业务迭代的支撑,以业务交付为目标;基础架构组负责底层基础核心能力建设,以提升效率、性能和核心能力拓展为目标;

  2.项目架构优化,基于1,要在应用层和基础层之间,抽象出核心架构层,并将其连带基础层一起交由基础架构组负责,如图;

  该层会涉及到很多核心能力的建设,这里不做过多赘述,下文会对以上各个模块做详细展开。

  注:从全局视角来看,基础层和核心层也能作为一个整体,共同支撑上层业务。这里将其分为两层,主要考虑到前者是必选项,是整体架构的必要组成部分;后者是可选项,但同时也是衡量一个App中台能力的核心指标。

  随着业务规模继续扩大,App的产品经理(下简称PD)会从一个变为多个,每个PD负责独立的一条业务线,比如App中包含首页、商品和我的等多个模块,则每个PD会对应这里的一个模块。但该调整会带来一个很严重的问题

  项目的版本迭代时间是确定的,只有一个PD的时候,每个版本会提一批需求,开发能按时交付就上线,不能交付就把这个迭代适当顺延,这样不会有什么问题;

  但如今多个业务线并行,很难在绝对意义上保证各个业务线的需求迭代都能正常交付,就好像你组织一个活动约定了几点集合,但总会有人会遇到一些特殊的情况不能及时赶到。同理,这种难以完全保持一致的情况在项目开发中也会遇到。在当前的项目架构下,业务上虽然拆分了业务线,但我们工程项目的业务模块还是一个整体,内部包含着各种错综复杂的依赖关系网,即使每个业务线按分支区分,也很难规避这个问题。

  业务层中,可以按照开发人员或者小组进行更细粒度的划分,以保证业务间的解耦合和开发职责的界定。

  业务规模和用户体量继续扩大,为了应对随之而来的是业务需求暴增,整个端侧团队开始考虑研发成本问题。

  为什么每个业务需求都至少需要Android和iOS两端都实现一遍?有没有什么方案能够满足一份代码能运行在多个平台?这样岂不是既降低了沟通成本,又提升了研发效率。答案当然是肯定的,此时端侧部分业务开始进入了跨平台开发的阶段。

  至此,一个相对完整的端侧系统架构已经初具雏形了。后续业务上会继续有着更多的迭代,但项目的整体结构基本都不会偏离太多,更多的是针对于当前架构中的某些节点做更深层次的改进和完善。

  以上是对Android项目架构迭代过程的总结,接下来我会对最终的架构图按照自下而上的层级顺序进行逐一展开,并对每层中涉及到的核心模块和可能遇到的问题进行分析和总结。

  比如App的主色调、普通正文的文字颜色和大小、页面的内外边距、网络加载失败的默认提示文案、空列表的默认UI等等,尤其是在下文提到项目模块化之后这些基础的UI样式统一会变得非常重要。

  在项目和团队规模逐渐发展扩大时,为了提高上层业务的开发效率,秉承DRY的开发原则,我们有必要对一些高频UI组件进行统一封装,以供给业务上层调用;另外一个角度来看,必要的抽象封装还能够降低最终构建的安装包大小,以免一份语义的资源文件在多处出现。

  基础UI组件通常包含内部开发和外部引用两部分,内部开发无可厚非,根据业务需求进行开发和封装即可;外部引用要着重强调一下,Github上有大量可复用、经过很多项目验证过的优秀UI组件库,如果是为了快速满足业务开发诉求,这些都将不失为一种很不错的选择。

  选择一个合适的UI库,会给整个开发进程带来很大的加速,自己手动去实现也许没问题,但会非常花费时间和精力,如果不是为了研究实现原理或深度定制,建议优先选择成熟的UI库。

  绝大多数的App应用都需要联网,网络模块几乎成为了所有App必不可少的部分。

  这里不做具体展开,如果不是基础层对网络层有自己额外的定制,则推荐直接使用Retrofit2作为网络库首选,上层Java Interface风格的Api,面向开发者非常友好;下层依赖功能强大的Okhttp框架也几乎能够满足绝大多数场景的业务诉求。官网的用例参考

  用例中对Retorfit声明式接口的优势做了很好的展现,不需要手动实现接口,声明即可使用,其背后的原理是基于Java的动态代理来做的。

  无论上一步选择的是什么网络库,都需要考虑到该网络库对于统一拦截的能力支持。比如我们想在App的整个运行过程中,打印所有请求的日志,就需要有一个支持配置类似Interceptor这样的全局。

  举一个具体的例子,在现如今服务端很多分布式部署的场景,传统的session方式已经无法满足对客户端状态记录的诉求。有一个比较公认的解决方案是JWT(JSON WEB TOKEN),它需要客户端侧在登录认证之后,把包含用户状态的请求头信息传递给服务端,此时就需要在网络层做类似于下面的统一拦截处理。

  此外还有一点需要额外说明,如果应用中有一些跟业务强相关的信息,也建议根据实际业务情况考虑直接通过请求头进行统一传递。比如社区App的社区Id、门店App的门店Id等,这类参数有个普遍性特点,一旦切换过来之后,接下来的很多业务网络请求都会需要该参数信息,而如果每个接口都手动传入将会降低开发效率,也更容易引发一些不必要的人为错误。

  图片库和网络库不同的是,目前行业里比较流行的几个库差异性并没有那么大,这里建议根据个人喜好和熟悉度自行选择。以下是我从各个图片库官网整理出来的使用示例。

  图片库的选型比较灵活,但是它的基础原理我们需要弄清楚,以便在图片库出问题时有足够的应对解决策略。

  另外需要着重提出来的是,对于图片库最核心的是对图片缓存的设计,有关该部分的延伸可以参考下文的「核心原理总结」章节。

  在Android开发中异步会使用的非常之多,同时其中也包含很多知识点,因此这里将该部分单独抽出来讲解。

  总结下来一句话就是,主线程处理UI操作,子线程处理耗时任务操作。如果反其道而行之就会出现以下问题:

  3.子线程做UI操作,会出现CalledFromWrongThreadException异常(这里只做一般性讨论,实际上在满足某些条件下子线程也能更新UI,参《Android 中子线程真的不能更新 UI 吗?》,本文不讨论该情况);

  Android开发使用的是Java和Kotlin这两种语言,如果我们的项目中引入了Kotlin当然是最好,对于异步调用时只需要按照如下方式进行调用即可。

  这里适当延伸一下,类似于async + await的异步调用方式,在其他很多语言都已经得到了支持,如下

  但是如果我们的项目中还是纯Java项目,在复杂的业务交互场景下,常常会遇到串行异步的业务逻辑,此时我们的代码可读性会变得很差,一种可选的应对方案是通过引入RxJava来解决,参考如下

  1.Android(Native开发)不同于Web能够随时发布上线,Android发布几乎都要走应用平台的审核;

  基于以上几点,就决定了我们在Android开发过程中,对代码逻辑有动态配置的诉求。

  基于这个最基本的模型单元,业务上可以演化出非常丰富的玩法,比如配置启动页停留时长、配置商品中是否展示大图、配置每页加载多少条数据、配置要不要是否允许用户进入某个页面等等。

  推是指通过建立客户端与服务端的长连接,服务端一旦有配置发生变化,就将变化的数据推到客户端以进行更新;

  基于这两种模式,还会演化出推拉结合的方式,其本质就是两种方式都使用,技术层面没有新变化,这里不做赘述。下面将推拉两种方式进行对比

  综合来看,如果业务上对时效性要求没有非常高的情况下,我个人还是倾向于选择拉的方式,主要原因更改配置是低频事件,为了这个低频事件去做C-S的长连接,会有种牛刀杀鸡的感觉。

  推配置的实现思考相对清晰,有配置下发客户端更新即可,但需要做好长连接断开后的重连逻辑。

  1.按照namespace进行多模块划分配置,避免全局一个大而全的配置;

  2.每个namespace在初始化和每次改动时都会有个flag标识,以标识当前版本;

  3.客户端每个业务请求都在请求头处统一拉上各flag或他们共同组合的md5等标识,为了在服务端统一拦截时进行flag时效性校验;

  4.服务端时效性检验结果通过统一响应头下发,与业务接口隔离,上层业务方不感知;

  5.客户端收到时效性不一致结果时,再根据具体的namespace进行拉取,而不是每次全量拉取;

  App与用户联系最紧密的就是交互,它是我们的App产品与用户之间沟通的桥梁。

  用户点击一个按钮之后要执行什么动作,进入一个页面之后要展示什么内容,某个操作之后要执行什么请求,请求之后要执行什么提示,这些都是用户最直观能看到的东西。全局拦截就是针对于这些用户能接触到的最高频的交互逻辑做出可支持通过前面动态配置来进行定制的技术方案。

  具体的交互响应(如弹出一个Toast或Dialog,跳转到某个页面)是需要通过代码逻辑来控制的,但该部分要做到的就是在App发布之后还能实现这些交互,因此我们需要将一些基础常见的交互进行结构化处理,然后在App中提前做出通用的预埋逻辑。

  这里值得注意的是,Dialog的Action中嵌套了Toast的逻辑,多种Action的灵活组合能给我们提供丰富的交互能力。

  交互结构化的数据协议约定了每个Action对应的具体事件,客户端对结构化数据的解析和封装,进而能够将数据协议转化为与用户的产品交互,接下来要考虑的就是如何让一个交互信息生效。参考如下逻辑

  1.提供根据页面和事件标识来获取服务端下发的Action的能力,这里用到的DynamicConfig即为前面提到的动态配置。

  2.提供包装点击事件的处理逻辑(performAction为对具体Action的解析逻辑,功能比较简单,这里不做展开)

  // 这里返回一个OnClickListener对象, 降低上层业务方的理解成本和代码改动难度

  有了上面的基础,我们便能够快速实现支持远端动态改变App交互行为的功能,下面对比一下上层业务方在该能力前后的代码差异。

  可以看到,业务侧多透传一些对于当前上下文的标识参数,除此之外没有其他更多的改动。

  截止目前,我们对于addGoodsButton这一按钮的点击事件就已经完成了远端的hook能力,如果现在突然出现了一些原因导致添加商品页不可用,则只需要在远端动态配置里添加如下配置即可。

  上面介绍了对于点击事件的远端拦截思路,与点击事件对应的,还有跳转页面、执行网络请求等常见的交互,它们的原理都是一样的,不再一一枚举。

  在App开发测试阶段,通常需要添加一些本地化配置,从而实现一次编译构建允许兼容多种逻辑。比如,在与服务端接口联调过程中,App需要做出常见的几种环境切换(日常、预发和线上)。

  理论上,基于前面提到的动态配置也能实现这个诉求,但动态配置主要面向的是线上用户,而如果选择产研阶段使用该种能力,无疑会增加线上配置的复杂度,而且还会依赖网络请求的结果才能实现。

  因此,我们需要抽象出一套支持本地化配置的方案,该套方案需要尽可能满足以下能力

  1.对本地配置提供默认值支持,未做任何配置时,配置返回默认值。比如,默认环境为线上,如果没有更改配置,就不会读取到日常和预发环境。

  2.简化配置的读写接口,让上层业务方尽可能少感知实现细节。比如,我们不需要让上层感知到本地配置的持久化信息写入的是SharedPreferences还是SQLite,而只需提供一个写入的API即可。

  3.向上层暴露进入本地配置页的API方式,以满足上层选择性进入的空间。比如,上层通过我们暴露出去的API可以选择实际使用App过程中,是通过点击页面的某个操作按钮、还是手机的音量键或者是摇一摇来进入配置页面。

  4.对于App中是否拥有本地配置能力的控制,尽可能放到编译构建级别,保证线上用户不会进入到配置页面。比如,如果线上用户能够进入到预发环境,那很可能会酝酿着一场安全事故即将发生。

  在移动客户端中,Android应用不同于iOS只能在AppStore进行发布,Android构建的产物.apk文件支持直接安装,这就给App静默升级提供了可能。基于该特性,我们可以实现用户在不通过应用市场即可直接检测和升级新版本的诉求,缩短了用户App升级的路径,进而能够提升新版本发布时的覆盖率。

  我们需要在应用中考虑抽象出来版本检测和升级的能力支持,这里需要服务端提供检测和获取新版本App的接口。客户端基于某种策略,如每次刚进入App、或手动点击新版本检测时,调用服务端的版本检测接口,以判断当前App是否为最新版本。如果当前是新版本,则提供给App侧最新版本的apk文件下载链接,客户端在后台进行版本下载。下面总结出核心步骤的流程图

  客户端的日志监控主要用来排查用户在使用App过程中出现的Crash等异常问题,对于日志部分总结几项值得注意的点

  2.本地持久化,对于关键重要日志(如某个位置错误引起的Crash)要做本地持久化保存;

  3.日志上报,在用户授权允许的情况下,将暂存用户本地的日志进行上传,并分析具体的操作链路;

  服务端能查询到的是客户端接口调用的次数和频率,但无法感知到用户具体的操作路径。为了能够更加清晰的了解用户,进而分析分析产品的优劣势和瓶颈点,我们可以将用户在App上的核心操作路径进行收集和上报。

  比如,下面是一个电商App的用户成交漏斗图,通过客户端的埋点统计能够获取到漏斗各层的数据,然后再通过数据制作做出可视化报表。

  分析以下漏斗,我们可以很明显的看出成交流失的关键节点是在「进入商品页」和「购买」之间,因此接下来就需要思考为什么「进入商品页」的用户购买意愿降低?是因为商品本身问题还是商品页的产品交互问题?会不会是因为购买按钮比较难点击?还是因为商品页图片太大导致商品介绍没有展示?这些流失流量的页面停留时长又是怎样的?对于这些问题的思考,会进一步促使我们去在商品页添加更多的ABTest和更细粒度的埋点统计分析。总结下来,埋点统计为用户行为分析和产品优化提供了很重要的指引意义。

  1.客户端埋点一般分为P点(页面级别)、E点(事件级别)和C点(自定义点);

  2.埋点分为收集和上报两个步骤,用户量级较大时要注意对上报埋点进行合并、压缩等优化处理;

  3.埋点逻辑是辅逻辑,产品业务是主逻辑,客户端资源紧张时要做好资源分配的权衡;

  热修复(Hotfix)是一种对已发布上线的App在不进行应用升级的情况下进行动态更新原代码逻辑的技术方案。主要同于以下场景

  1.应用出现重大缺陷并严重影响到用户使用时,比如,在某些系统定制化较强的机型(如小米系列)上一旦进入商品详情页就出现应用Crash;

  2.应用出现明显阻塞问题并影响到用户正常交互时,比如,在某些极端场景下,用户无法关闭页面对线.应用出现资损、客诉、舆论风波等产品形态问题,比如将价格单位“元”误显示为“分”;

  有关热修复相关的技术方案探究,可以延展出很大篇幅,本文的定位是Android项目整体的架构,因此不做详细展开。

  对于抽象和封装,主要取决于我们日常Coding过程中对一些痛点和冗余编码的感知和思考能力。

  面向RecyclerView框架层,为了提供框架的灵活和拓展能力,所以把API设计到足够原子化,以支撑开发者千差万别的开发诉求。比如,RecyclerView要做对多itemType的支持,所以内部要做根据itemType开分组缓存vitemView的逻辑。

  但实际业务开发过程中,就会抛开很多特殊性,我们页面要展示的绝大多数列表都是单itemType的,在连续写很多个这种单itemType的列表之后,我们就开始去思考一些问题:

  4.为什么Adapter每次设置数据之后,都要调用对应的notifyXXX方法?

  5.为什么Android上实现一个简单的列表需要大几十行代码量?这其中有多少是必需的,又有多少是可以抽象封装起来的?

  对于以上问题的思考最终引导我封装了RecyclerViewHelper的辅助类,相对于标准实现而言,使用方可以省去繁琐的Adapter和ViewGolder声明,省去一些高频且必需的代码逻辑,只需要关注最核心的功能实现,如下

  1.页面埋点,基于前面提到的埋点统计,在用户进入、离开页面等时机,将用户页面的交互情况进行收集上报;

  2.公共UI,页面顶部状态栏和ActionBar、页面常用的下拉刷新能力实现、页面耗时操作时的加载进度条;

  3.权限处理,进入当前页面所需要的权限申请,用户授权后的回调逻辑和拒绝后的异常处理逻辑;

  4.统一拦截,结合前面提到的统一拦截对进入页面后添加支持动态配置交互的定制能力;

  这里提到的模块化是指,基于App的业务功能对项目工程进行模块化拆分,主要为了解决大型复杂业务项目的协同开发困难问题。

  前面「抽象和封装」章节提到的 BaseActivity、BaseFragment 等通用业务能力在项目模块化之后,也需要同步做改造,要下沉到业务层中单独的一个 base 模块中,以便提供给其他业务模块引用。

  模块化之后,各模块间没有相互依赖关系,此时跨模块进行页面跳转时不能直接引用其他模块的类。

  比如,在首页展示某一个商品推荐,点击之后要跳转到商品详情页,在模块化之前的写法是

  但在模块化之后,在首页模块无法引用 GoodsActivity 类,因此页面跳转不能再继续之前的方式,需要对页面进行隐式路由改造,如下

  2.替换跳转逻辑,代码中根据上一步注册的 Activity 标识进行隐式跳转

  更进一步,我们将隐式跳转的逻辑进行抽象和封装,提炼出一个专门提供隐式路由能力的静态方法,参考如下代码

  2.统一收口路由逻辑,便于结合前面的「动态配置」和「统一拦截」章节进行线上App路由逻辑的动态更改;

  3.标准化 Android 端页面跳转参数格式,统一使用 String 类型,解除目标页面解析参数时的类型判断歧义;

  4.对跳转页面所需要的数据做了标准化支持,iOS 端再配合同步改造后,页面跳转逻辑将支持完全由业务服务端进行下发;

  模块化后另一个需要解决是模块通信问题,没有直接依赖关系的模块间是拿不到任何对方的 API 进行直接调用的。对于该问题往往会按照如下类别进行分析和处理

  1.通知式通信,只需要将事件告知对方,并不关注对方的响应结果。对于该种通信,一般采用如下方式实现

  定义出 biz-service 模块,将业务接口 interface 文件收口到该模块,再由各接口对应语义的业务模块进行接口的实现,然后再基于某种机制(手动注册或动态扫描)完成实现类的注册;

  跨平台一般有两个接入的时机,一个是在最开始的前期项目调研阶段,直接技术选型为纯跨平台技术方案;另一个是在已有Native工程上需要集成跨平台能力的阶段,此时App属于混合开发的模式,即Native + 跨平台相结合。

  有关更多跨平台的选型和细节不在本文范畴内,具体可以参考《移动跨平台开发框架解析与选型》,文中对于整个跨平台技术的发展、各框架原理及优劣势讲得很详细。参跨平台技术演进图

  在Android开发中,我们会接触到的框架不计其数,并且这些框架还还在不断的更新迭代,因此我们很难对每个框架都能了如指掌。

  但这并不影响我们对Android中核心技术学习和研究,如果你有尝试过深入剖析这些框架的底层原理,就会发现它们中很多原理都是相通的。一旦我们掌握了这些核心原理,就会发现绝大多数框架只不过是利用这些原理,再结合框架要解决的核心问题,进而包装出来的通用技术解决方案。

  下面我把在SDK框架和实际开发中一些高频率使用的核心原理进行梳理和总结。

  双缓存是指在通过网络获取一些资源时,为提高获取速度而在内存和磁盘添加双层缓存的技术方案。该方案最开始主要用于上文「图片模块」提到的图片库中,图片库利用双缓存来极大程度上提高了图片的加载速度。一个标准双缓存方案如下图示

  基于该方案,我们在实际开发中还能拓展另一个场景,对于业务上一些时效性低或更改较少的接口数据,为了提高它们的加载效率,也可以结合该思路进行封装,这样就将一个依赖网络请求页面的首帧渲染时长从一般的几百ms降到几十ms以内,优化效果相当明显。

  1.在开发框架中,网络库和图片库获取网络资源需用到线.在项目开发中,读写SQLite和本地磁盘文件等IO操作需要用到线.在类似于AsyncTask这种提供任务调度的API中,其底层也是依赖线程池来完成的;

  如此多的场景会用到线程池,如果我们希望对项目的全局观把握的更加清晰,熟悉线程池的一些核心能力和内部原理是尤为重要的。

  就其直接暴露出来的API而言,最核心的方法就两个,分别是线程池构造方法和执行子任务的方法。

  // 这里做子任务操作}});其中,提交子任务就是传入一个 Runnable 类型的对象实例不做赘述,需要重点说明也是线程池中最核心的是构造方法中的几个参数。

  上图表明的是往线程池中不断提交子任务且任务来不及执行时线程池内部对任务的处理机制,该图对理解线程池内部原理和配置线程池参数尤为重要。

  反射和注解都是Java语言里一种官方提供的技术能力,前者用来在程序运行期间动态读写对象实例(或静态)属性、执行对象(静态)方法;后者用来在代码处往类、方法、方法入参、类成员变量和局部变量等指定域添加标注信息。

  通过反射和注解技术,再结合对代码的抽象和封装思维,我们可以非常灵活的实现很多泛化调用的诉求,比如1.前面「热修复」章节,基于ClassLoader 的方案,其内部实现几乎全是通过反射进行 dex 更改的;

  2.前面「网络模块」章节,Retorfit 只需要声明一个接口再添加注解即可使用,其底层也是利用了反射注解和下面要介绍的动态代理技术;

  4.如果了解 Java 服务端开发,主流的开发框架 SpringBoot 其内部大量运用了注射和注解的技术;

  注入方式的优势是,对使用方屏蔽依赖对象的实例化过程,这样方便对依赖对象进行统一管理。

  // ...}}我们拿到 Manager 的对象实例后,希望调用到 doSomething 这个私有方法,按照一般的调用方式如果不更改方法为 public 就是无解的。但利用反射可以做到

  诸如此类的场景在开发中会有很多,可以说熟练掌握反射和注解技术,既是掌握 Java 高阶语言特性的表现,也能够让我们在对一些通用能力进行抽象封装时提高认知和视角。

  在使用动态代理时,通常都会伴随着反射和注解的应用,但相对于反射和注解而言,动态代理的作用相对会比较晦涩难懂。下面结合一个具体的场景来看动态代理的作用。背景

  项目开发过程中,需要调用到服务端接口,因此客户端封装一个网络请求的通用方法。

  {// 实现略..}}由于业务上有多个页面都需要查询商品列表数据,因此需要封装一个 GoodsApi 的接口。

  透过每个方法内部代码实现的现象看其核心的本质,我们可以抽象归纳为以上的“模板”逻辑。

  有没有一种技术可以让我们只需要写网络请求所必需的请求协议相关参数,而不需要每次都要做以下几步重复琐碎的编码?

  相比之前,上层的调用方式只有极小的调整;但内部的实现却有了很大的改进,直接省略了所有的接口实现逻辑,参考如下代码对比图。

  我们进行架构设计的场景下通常是不同的,但有些问题的底层设计方案是相通的,这一章节会对这些相通的设计方案进行总结。

  通信设计一句话概括,通信的本质就是解决 A 和 B 之间如何调用的问题,下面按抽象出来的 AB 模型依赖关系进行逐一分析。

  这是最常见的关联关系,A 类中直接依赖 B,只需要通过最基本的方法调用和设置回调即可完成。

  该种关系的通信也是借助于事件管理器,唯一不同点是对于 EventManager 对象实例的获取方式不同了,不再是直接由当前上下文获取到的,而是来源于全局唯一的实例对象,比如从单例中获取到。

  }});问题以上是很常见的一种回调设置方式,如果仅仅是做业务开发,这种写法没有任何问题,但如果是做成给外部客户使用的SDK,这种做法就会存在瑕疵。

  }});这种方案确实能解决外部无法静默升级SDK的问题,但却会带来另外的问题,随着每次接口升级,外部设置回调函数的代码将会越来越多。对外优化

  }}此时,外部可以选择通过单接口、组合接口和空实现类等多种方式设置回调函数。// 单接口方式设置回调

  这时候再升级SDK,对外部客户之前的调用逻辑没有任何影响,能够很好的做到向前兼容。

  SDK每次对外部回调的时候都要添加这种判断着实麻烦,我们接下来将这段判断逻辑单独封装起来。

  业务领域千差万别,可能是电商项目,可能是社交项目,还有可能是金融项目;开发技术也一直在快速迭代,也许在用纯 Native 开发模式,也许在用 Flutter、RN 开发模式,也许在用混合开发模式;但无论如何,这些项目在架构设计方面的底层原理和设计思路都是万变不离其宗的,这些东西也正是我们真正要学习和掌握的核心能力。相关链接:

  N个技巧,编写更高效 Dockerfile做到这4点,才是真正的持续交付!从现在开始 学习技术

  声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。