
                            
                            因收到Google相关通知，网站将会择期关闭。相关通知内容
                            
                            
                            41 组件化和平台化，该如何组织合理稳定的Flutter工程结构？
                            你好，我是陈航。今天，我们来聊一聊Flutter应用的工程架构这个话题。

在软件开发中，我们不仅要在代码实现中遵守常见的设计模式，更需要在架构设计中遵从基本的设计原则。而在这其中，DRY（即Don’t Repeat Yourself）原则可以算是最重要的一个。

通俗来讲，DRY原则就是“不要重复”。这是一个很朴素的概念，因为即使是最初级的开发者，在写了一段时间代码后，也会不自觉地把一些常用的重复代码抽取出来，放到公用的函数、类或是独立的组件库中，从而实现代码复用。

在软件开发中，我们通常从架构设计中就要考虑如何去管理重复性（即代码复用），即如何将功能进行分治，将大问题分解为多个较为独立的小问题。而在这其中，组件化和平台化就是客户端开发中最流行的分治手段。

所以今天，我们就一起来学习一下这两类分治复用方案的中心思想，这样我们在设计Flutter应用的架构时也就能做到有章可循了。

组件化

组件化又叫模块化，即基于可重用的目的，将一个大型软件系统（App）按照关注点分离的方式，拆分成多个独立的组件或模块。每个独立的组件都是一个单独的系统，可以单独维护、升级甚至直接替换，也可以依赖于别的独立组件，只要组件提供的功能不发生变化，就不会影响其他组件和软件系统的整体功能。



图1 组件化示意图

可以看到，组件化的中心思想是将独立的功能进行拆分，而在拆分粒度上，组件化的约束则较为松散。一个独立的组件可以是一个软件包（Package）、页面、UI控件，甚至可能是封装了一些函数的模块。

组件的粒度可大可小，那我们如何才能做好组件的封装重用呢？哪些代码应该被放到一个组件中？这里有一些基本原则，包括单一性原则、抽象化原则、稳定性原则和自完备性原则。

接下来，我们先看看这些原则具体是什么意思。

单一性原则指的是，每个组件仅提供一个功能。分而治之是组件化的中心思想，每个组件都有自己固定的职责和清晰的边界，专注地做一件事儿，这样这个组件才能良性发展。

一个反例是Common或Util组件，这类组件往往是因为在开发中出现了定义不明确、归属边界不清晰的代码：“哎呀，这段代码放哪儿好像都不合适，那就放Common（Util）吧”。久而久之，这类组件就变成了无人问津的垃圾堆。所以，再遇到不知道该放哪儿的代码时，就需要重新思考组件的设计和职责了。

抽象化原则指的是，组件提供的功能抽象应该尽量稳定，具有高复用度。而稳定的直观表现就是对外暴露的接口很少发生变化，要做到这一点，需要我们提升对功能的抽象总结能力，在组件封装时做好功能抽象和接口设计，将所有可能发生变化的因子都在组件内部做好适配，不要暴露给它的调用方。

稳定性原则指的是，不要让稳定的组件依赖不稳定的组件。比如组件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就会陷入不稳定的状态，即所谓牵一发而动全身。



图2 循环依赖现象

平台化是组件化的升级，即在组件化的基础上，对它们提供的功能进行分类，统一分层划分，增加依赖治理的概念。为了对这些功能单元在概念上进行更为统一的分类，我们按照四象限分析法，把应用程序的组件按照业务和UI分解为4个维度，来分析组件可以分为哪几类。



图3 组件划分原则

可以看出，经过业务与UI的分解之后，这些组件可以分为4类：


具备UI属性的独立业务模块；
不具备UI属性的基础业务功能；
不具备业务属性的UI控件
不具备业务属性的基础功能


按照自身定义，这4类组件其实隐含着分层依赖的关系。比如，处于业务模块中的首页，依赖位于基础业务模块中的账号功能；再比如，位于UI控件模块中的轮播卡片，依赖位于基础功能模块中的存储管理等功能。我们将它们按照依赖的先后顺序从上到下进行划分，就是一个完整的App了。



图4 组件化分层

可以看到，平台化与组件化最大的差异在于增加了分层的概念，每一层的功能均基于同层和下层的功能之上，这使得各个组件之间既保持了独立性，同时也具有一定的弹性，在不越界的情况下按照功能划分各司其职。

与组件化更关注组件的独立性相比，平台化更关注的是组件之间关系的合理性，而这也是在设计平台化架构时需要重点考虑的单向依赖原则。

所谓单向依赖原则，指的是组件依赖的顺序应该按照应用架构的层数从上到下依赖，不要出现下层模块依赖上层模块这样循环依赖的现象。这样可以最大限度地避免复杂的耦合，减少组件化时的困难。如果我们每个组件都只是单向依赖其他组件，各个组件之间的关系都是清晰的，代码解耦也就会变得非常轻松了。

平台化强调依赖的顺序性，除了不允许出现下层组件依赖上层组件的情况，跨层组件和同层组件之间的依赖关系也应当严格控制，因为这样的依赖关系往往会带来架构设计上的混乱。

如果下层组件确实需要调用上层组件的代码怎么办？

这时，我们可以采用增加中间层的方式，比如Event Bus、Provider或Router，以中间层转发的形式实现信息同步。比如，位于第4层的网络引擎中，会针对特定的错误码跳转到位于第1层的统一错误页，这时我们就可以利用Router提供的命名路由跳转，在不感知错误页的实现情况下来完成。又比如，位于第2层的账号组件中，会在用户登入登出时主动刷新位于第1层的首页和我的页面，这时我们就可以利用Event Bus来触发账号切换事件，在不需要获取页面实例的情况下通知它们更新界面。关于这部分内容，你可以参考第20和21篇文章中的相关内容，这里就不再赘述了。

平台化架构是目前应用最广的软件架构设计，其核心在于如何将离散的组件依照单向依赖的原则进行分层。而关于具体的分层逻辑，除了我们上面介绍的业务和UI四象限法则之外，你也可以使用其他的划分策略，只要整体结构层次清晰明确，不存在难以确定归属的组件就可以了。

比如，Flutter就采用Embedder（操作系统适配层）、Engine（渲染引擎及Dart VM层）和Framework（UI SDK层）整体三层的划分。可以看到，Flutter框架每一层的组件定义都有着明确的边界，其向上提供的功能和向下依赖的能力也非常明确。



图5 Flutter框架架构

备注：此图引自Flutter System Overview

总结

好了，今天的分享就到这里，我们总结一下主要内容吧。

组件化和平台化都是软件开发中流行的分治手段，能够将App内的功能拆分成多个独立的组件或模块。

其中，组件化更关注如何保持组件的独立性，只要拆分的功能独立即可，约束较为松散，在中大型App中容易造成一定程度的功能耦合现象。而平台化则更强调组件之间关系的合理性，增加了分层的概念，使得组件之间既有边界，也有一定的弹性。只要满足单向依赖原则，各个组件之间的关系都是清晰的。

分治是一种与技术无关的架构思想，有利于降低工程的复杂性，从而提高App的可扩展和可维护性。今天这篇文章，我重点与你分享的是组件化与平台化这两种架构设计的思路，并没有讲解它们的具体实现。而关于组件化与平台化的实现细节，网络上已经有很多文章了，你可以在网上自行搜索了解。如果你还有关于组件化和平台化的其他问题，那就在评论区中给我留言吧。

其实，你也可以琢磨出，今天这篇文章的目的是带你领会App架构设计的核心思想。因为，理解思想之后剩下的就是去实践了，当你需要设计App架构时再回忆起这些内容，或是翻出这篇文章一定会事半功倍。

思考题

最后，我给你留一道思考题吧。

在App架构设计中，你会采用何种方式去管理涉及资源类的依赖呢？

欢迎你在评论区给我留言分享你的观点，我会在下一篇文章中等待你！感谢你的收听，也欢迎你把这篇文章分享给更多的朋友一起阅读。

                        
                        
                            