You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

115 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 41 | 组件化和平台化该如何组织合理稳定的Flutter工程结构
你好我是陈航。今天我们来聊一聊Flutter应用的工程架构这个话题。
在软件开发中我们不仅要在代码实现中遵守常见的设计模式更需要在架构设计中遵从基本的设计原则。而在这其中DRY即Dont Repeat Yourself原则可以算是最重要的一个。
通俗来讲DRY原则就是“不要重复”。这是一个很朴素的概念因为即使是最初级的开发者在写了一段时间代码后也会不自觉地把一些常用的重复代码抽取出来放到公用的函数、类或是独立的组件库中从而实现代码复用。
在软件开发中,我们通常从架构设计中就要考虑如何去管理重复性(即代码复用),即如何将功能进行分治,将大问题分解为多个较为独立的小问题。而在这其中,组件化和平台化就是客户端开发中最流行的分治手段。
所以今天我们就一起来学习一下这两类分治复用方案的中心思想这样我们在设计Flutter应用的架构时也就能做到有章可循了。
## 组件化
组件化又叫模块化即基于可重用的目的将一个大型软件系统App按照关注点分离的方式拆分成多个独立的组件或模块。每个独立的组件都是一个单独的系统可以单独维护、升级甚至直接替换也可以依赖于别的独立组件只要组件提供的功能不发生变化就不会影响其他组件和软件系统的整体功能。
![](https://static001.geekbang.org/resource/image/4f/82/4f49b09f5bc9fd33db010d9286ae2e82.png)
图1 组件化示意图
可以看到组件化的中心思想是将独立的功能进行拆分而在拆分粒度上组件化的约束则较为松散。一个独立的组件可以是一个软件包Package、页面、UI控件甚至可能是封装了一些函数的模块。
**组件的粒度可大可小,那我们如何才能做好组件的封装重用呢?哪些代码应该被放到一个组件中?**这里有一些基本原则,包括单一性原则、抽象化原则、稳定性原则和自完备性原则。
接下来,我们先看看这些原则具体是什么意思。
**单一性原则指的是**,每个组件仅提供一个功能。分而治之是组件化的中心思想,每个组件都有自己固定的职责和清晰的边界,专注地做一件事儿,这样这个组件才能良性发展。
一个反例是Common或Util组件这类组件往往是因为在开发中出现了定义不明确、归属边界不清晰的代码“哎呀这段代码放哪儿好像都不合适那就放CommonUtil吧”。久而久之这类组件就变成了无人问津的垃圾堆。所以再遇到不知道该放哪儿的代码时就需要重新思考组件的设计和职责了。
**抽象化原则**指的是,组件提供的功能抽象应该尽量稳定,具有高复用度。而稳定的直观表现就是对外暴露的接口很少发生变化,要做到这一点,需要我们提升对功能的抽象总结能力,在组件封装时做好功能抽象和接口设计,将所有可能发生变化的因子都在组件内部做好适配,不要暴露给它的调用方。
**稳定性原则**指的是不要让稳定的组件依赖不稳定的组件。比如组件1依赖了组件5如果组件1很稳定但是组件5经常变化那么组件1也就会变得不稳定了需要经常适配。如果组件5里确实有组件1不可或缺的代码我们可以考虑把这段代码拆出来单独做成一个新的组件X或是直接在组件1中拷贝一份依赖的代码。
**自完备性**即组件需要尽可能地做到自给自足尽量减少对其他底层组件的依赖达到代码可复用的目的。比如组件1只是依赖某个大组件5中的某个方法这时更好的处理方法是剥离掉组件1对组件5的依赖直接把这个方法拷贝到组件1中。这样一来组件1就能够更好地应对后续的外部变更了。
在理解了组件化的基本原则之后,**我们再来看看组件化的具体实施步骤**,即剥离基础功能、抽象业务模块和最小化服务能力。
首先我们需要剥离应用中与业务无关的基础功能比如网络请求、组件中间件、第三方库封装、UI组件等将它们封装为独立的基础库然后我们在项目里用pub进行管理。如果是第三方库考虑到后续的维护适配成本我们最好再封装一层使项目不直接依赖外部代码方便后续更新或替换。
基础功能已经封装成了定义更清晰的组件,接下来我们就可以按照业务维度,比如首页、详情页、搜索页等,去拆分独立的模块了。拆分的粒度可以先粗后细,只要能将大体划分清晰的业务组件进行拆分,后续就可以通过分布迭代、局部微调,最终实现整个业务项目的组件化。
在业务组件和基础组件都完成拆分封装后应用的组件化架构就基本成型了最后就可以按照刚才我们说的4个原则去修正各个组件向下的依赖以及最小化对外暴露的能力了。
## 平台化
从组件的定义可以看到,组件是个松散的广义概念,其规模取决于我们封装的功能维度大小,而各个组件之间的关系也仅靠依赖去维持。如果组件之间的依赖关系比较复杂,就会在一定程度上造成功能耦合现象。
如下所示的组件示意图中组件2和组件3同时被多个业务组件和基础功能组件直接引用甚至组件2和组件5、组件3和组件4之间还存在着循环依赖的情况。一旦这些组件的内部实现和外部接口发生变化整个App就会陷入不稳定的状态即所谓牵一发而动全身。
![](https://static001.geekbang.org/resource/image/4b/73/4bd68bca82792a0e12561d072372c573.png)
图2 循环依赖现象
平台化是组件化的升级即在组件化的基础上对它们提供的功能进行分类统一分层划分增加依赖治理的概念。为了对这些功能单元在概念上进行更为统一的分类我们按照四象限分析法把应用程序的组件按照业务和UI分解为4个维度来分析组件可以分为哪几类。
![](https://static001.geekbang.org/resource/image/b6/78/b65d4d3e320763be9794a31fb6658978.png)
图3 组件划分原则
可以看出经过业务与UI的分解之后这些组件可以分为4类
1. 具备UI属性的独立业务模块
2. 不具备UI属性的基础业务功能
3. 不具备业务属性的UI控件
4. 不具备业务属性的基础功能
按照自身定义这4类组件其实隐含着分层依赖的关系。比如处于业务模块中的首页依赖位于基础业务模块中的账号功能再比如位于UI控件模块中的轮播卡片依赖位于基础功能模块中的存储管理等功能。我们将它们按照依赖的先后顺序从上到下进行划分就是一个完整的App了。
![](https://static001.geekbang.org/resource/image/95/9c/954527aafaaab090b5bb5a044725d49c.png)
图4 组件化分层
可以看到,平台化与组件化最大的差异在于增加了分层的概念,每一层的功能均基于同层和下层的功能之上,这使得各个组件之间既保持了独立性,同时也具有一定的弹性,在不越界的情况下按照功能划分各司其职。
**与组件化更关注组件的独立性相比,平台化更关注的是组件之间关系的合理性,而这也是在设计平台化架构时需要重点考虑的单向依赖原则。**
所谓单向依赖原则,指的是组件依赖的顺序应该按照应用架构的层数从上到下依赖,不要出现下层模块依赖上层模块这样循环依赖的现象。这样可以最大限度地避免复杂的耦合,减少组件化时的困难。如果我们每个组件都只是单向依赖其他组件,各个组件之间的关系都是清晰的,代码解耦也就会变得非常轻松了。
平台化强调依赖的顺序性,除了不允许出现下层组件依赖上层组件的情况,跨层组件和同层组件之间的依赖关系也应当严格控制,因为这样的依赖关系往往会带来架构设计上的混乱。
**如果下层组件确实需要调用上层组件的代码怎么办?**
这时我们可以采用增加中间层的方式比如Event Bus、Provider或Router以中间层转发的形式实现信息同步。比如位于第4层的网络引擎中会针对特定的错误码跳转到位于第1层的统一错误页这时我们就可以利用Router提供的命名路由跳转在不感知错误页的实现情况下来完成。又比如位于第2层的账号组件中会在用户登入登出时主动刷新位于第1层的首页和我的页面这时我们就可以利用Event Bus来触发账号切换事件在不需要获取页面实例的情况下通知它们更新界面。关于这部分内容你可以参考第[20](https://time.geekbang.org/column/article/116382)和[21](https://time.geekbang.org/column/article/118421)篇文章中的相关内容,这里就不再赘述了。
**平台化架构是目前应用最广的软件架构设计,其核心在于如何将离散的组件依照单向依赖的原则进行分层。**而关于具体的分层逻辑除了我们上面介绍的业务和UI四象限法则之外你也可以使用其他的划分策略只要整体结构层次清晰明确不存在难以确定归属的组件就可以了。
比如Flutter就采用Embedder操作系统适配层、Engine渲染引擎及Dart VM层和FrameworkUI SDK层整体三层的划分。可以看到Flutter框架每一层的组件定义都有着明确的边界其向上提供的功能和向下依赖的能力也非常明确。
![](https://static001.geekbang.org/resource/image/eb/dd/eb1550dd9df00fd5b37e181628b782dd.png)
图5 Flutter框架架构
备注:此图引自[Flutter System Overview](https://flutter.dev/docs/resources/technical-overview)
## 总结
好了,今天的分享就到这里,我们总结一下主要内容吧。
组件化和平台化都是软件开发中流行的分治手段能够将App内的功能拆分成多个独立的组件或模块。
其中组件化更关注如何保持组件的独立性只要拆分的功能独立即可约束较为松散在中大型App中容易造成一定程度的功能耦合现象。而平台化则更强调组件之间关系的合理性增加了分层的概念使得组件之间既有边界也有一定的弹性。只要满足单向依赖原则各个组件之间的关系都是清晰的。
分治是一种与技术无关的架构思想有利于降低工程的复杂性从而提高App的可扩展和可维护性。今天这篇文章我重点与你分享的是组件化与平台化这两种架构设计的思路并没有讲解它们的具体实现。而关于组件化与平台化的实现细节网络上已经有很多文章了你可以在网上自行搜索了解。如果你还有关于组件化和平台化的其他问题那就在评论区中给我留言吧。
其实你也可以琢磨出今天这篇文章的目的是带你领会App架构设计的核心思想。因为理解思想之后剩下的就是去实践了当你需要设计App架构时再回忆起这些内容或是翻出这篇文章一定会事半功倍。
## 思考题
最后,我给你留一道思考题吧。
在App架构设计中你会采用何种方式去管理涉及资源类的依赖呢
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。