前言
聊聊 组件化
、插件化
以及 模块化
三个概念如何区分,以及在工程实践当中具体要解决什么样的问题,在实践当中要达到的目的,在整个项目团队中要起到怎样的作用。
组件化与插件化
组件:通俗点就是组件化就是基于可重用的目的,将一个大的软件系统按照分离关注点的形式,拆分成多个独立的组件。组件的出现是为了解决全局工程中有很多重复代码的问题,是为了复用,而且划分力度是相对较小的模块。组件化的另一个目的是为了解耦,把系统拆分成多个组件,分离组件边界和责任,便于独立升级和维护。
插件:可以理解为是封装了一层对外调用的接口的组件。插件的概念比较形象,一般存在一个“插拔”过程,所以要求可插拔的插件有一个相同的接口(这里所说的接口只是概念上的接口,即调用方法及参数等)。而组件是不存在这个相同接口的。
拿我们最常见的网络请求功能举例,无论哪种开发语言,github
上可能都有多种网络请求组件,那么对于一个项目而言,从一个网络组件 ComponentA
切换为另一个网络组件 ComponentB
是基本无法做到调用方法不改动的。而如果把网络请求组件插件化,即在组件外层抽象一层统一化的调用接口 NetworkInterface
,然后将当前使用网络请求组件 ComponentA
包装成实现该接口的网络请求插件 PluginA
。那么如果以后需要将使用的 ComponentA
切换为 ComponentB
,就只需要将 ComponentB
包装成 PluginB
并插入到应用中即可。
实际调用时,业务代码还是调用 NetworkInterface
,不用做任何修改。
从上面这个例子我们可以看出,插件和组件的实质区别就在于通过统一接口隔绝业务代码对于组件的直接依赖,这也是我们常听到的所谓的“项目开发时应该把第三方组件封装一下再用”。
组件化与模块化
- 模块:模块化的目的在于将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块之间通过接口调用。将一个大的系统模块化之后,每个模块都可以被高度复用。但是值得注意的是模块不等于功能,二者的关系大概为:功能 > 模块。
一个功能可能包含多个模块。
两者的实质区别在于:组件化开发是纵向分层,模块化开发是横向分块。
所以,模块化并没有要求一定组件化,就是说进行模块化拆分时你可以完全不考虑代码重用,只是把同一业务的代码做内聚整合成不同的模块。只不过这样得到的成果相对简单,我们一般不会这样而已。
组件化就比如项目中公共的 alert
框,它的出现其实是基于代码复用的目的,所以我们把它封装,并给多个地方使用。
而模块化就比如一个资讯列表界面,它本身可能只在一个地方使用,没有复用的需求,但我们也要把它封装成模块,这是高内聚的要求,我们不应该把资讯相关的代码在项目中放得到处都是。
但像这样的简单模块只是轻模块,统一接口较少。而统一定义的接口越多,其实和主应用的耦合就越高,也便是重模块。
而路由就是解决高耦合问题的,不过耦合问题不是模块化开发的需求,只不过我们一般都会在这个时间考虑这一事情而已,就像我们不会只做模块化开发同时不做组件化开发一样。
以一个项目实例来渐进式辨析
上面讲了一下几个概念的区别,拿一个渐进式开发移动端项目的例子进行辨析。
首先我们定义一个虚拟的产品——一款知识类应用,包含常见的资讯、问答、学院、直播等功能。
接下来我们从设计的角度逐步拆分这个产品。
原始态
如果开发时没有考虑任何组件化、模块化开发,那么此应用的所有功能都是堆积在一起的,总结起来就是代码特点就是高耦合,低内聚,无重用。
面对这样的一堆代码,技术经理可能要让你做一下代码重构,这就是你下一步的工作。
组件
那么你进行代码重构的第一步是什么呢?
答:将工程中重复的代码合并成为一份,也就是重用。
如果让我们来看组件化开发的定义,它的着重点就是代码重用。那这一步最后的结果就是提炼出一个个组件给不同的功能使用。
这里我们可以看一下其中的依赖关系:具体功能依赖提炼出来的组件,组件本身之间可能也有依赖关系,但一般不多。
所以我们总结组件化开发的原则就是高重用,低耦合。当然这只是相对而言。
基于这样的认识,我们甚至于可以把资讯、问答、学院、直播等功能封装成组件,只不过这些组件比较大,依赖可能多些,不过本质上没有多少区别。
就在你进行重构的过程中,这时需求来了:运营人员要求首页顶部的九宫格样式工具栏可动态配置,通过服务端数据修改显示功能,并调用对应的功能页面。
插件
代码重构从来不是超然物外的,在进行过程中接到新需求也是常有的事情。那么,对于这样一个需求,应该怎么考虑呢?
这个动态化需求很普遍,只不过这里有一个隐性要求——既然需求中要求功能动态配置,那么调用功能的地方就不知道功能的具体实现。
所以最终的方案中被调用功能必须有统一接口。我们这里说的接口只是编程领域的抽象概念,并非是指具体语言的 interface
或者 protocol
。
而有了这一统一接口,其配置功能其实就是“插拔”过程了。这样的成果实质上已经是插件了。
插件可以解释成可插拔式组件,它的核心就是不同功能实现提供统一接口。
项目中插件化的例子其实也不少,再举一个例子:比如资讯和问答功能使用的弹框样式不同,但是在两个功能内部其弹框样式是一致的。
面对这样的问题,你在重构时可能会简单的封装出两个组件 AlertA
和 AlertB
,分别给两个功能使用。
这样确实很便捷,而且适合当下的场景,但是从设计或者长远发展的角度上来考虑,如果资讯里面弹框样式需要换成和问答一样,甚至其他样式,那么基于现有的方法,你就需要修改资讯功能中所有调用弹框的地方。
所以插件化是解决这个问题的好办法:定义 AlertInterface
接口给具体业务功能使用,并实现 AlertPluginA
、 AlertPluginB
,在外面给不同的功能指定不同的插件即可。
其实 iOS
中最常见的插件就是 UITableView
,就是通过接口(协议-代理)实现。
模块
这时候项目的组件化拆分完成,技术经理说以后不同的模块会交由不同的人来维护,各人维护各自负责的代码。
这个需求初看上去只是项目管理上的需求,但实际执行时若资讯、问答、学院、直播分别由四个人维护,那么他们虽然大部分代码是相互隔离的,但仍然会有相当一部分代码耦合在一起,有时候会同时修改同一个代码文件。
这时候要做的自然就是模块化。
为什么是模块化呢?按照模块的定义,它是以关注点进行划分的,而关注点说到底就是功能,也就是说根据我们上面的例子,资讯、问答、学院、直播可以分成不同的模块。
我们最开始定义这个虚拟产品的时候说,它有三个特点——高耦合、低内聚、无重用。
而第一点组件化开发主要是解决了重用问题,提升了部分内聚,而耦合问题则没有涉及。
所以说我们上面可以将这个产品在逻辑上划分为资讯、问答、学院、直播四个模块,但在代码层面上它们却不是四个模块,因为它们的代码都是混杂在一起的。
比如产品首页,可能推荐了部分资讯、显示了热门问答、推送了目前的直播,而这些功能的代码则是写在一起的;再比如程序启动的时候,这四个模块都需要初始化一些数据,而初始化数据的代码也是写在一起的。
而模块化开发就是为了解决这一问题,即提高内聚,将分属同一模块代码放到一起;降低耦合,将不同模块间的耦合程度弱化。
高内聚是这一步的目标。但现状是有许多地方会用到多个模块,比如启动的时候会调用四个模块,首页会展示三个模块的界面。
如果要高内聚,那么必然需要这些模块为不同的场景提供相同的方法,这就是说所有模块要实现同一套多个接口。
而低耦合其实并非是模块化开发的要求,其实更多时候是基于产品上的动态化要求,所以最常见的解决方案就是路由机制。
总结
- 组件:代码重用,功能相对单一或者独立,无统一接口。组件化开发的成果是基础库和公共组件。
- 插件:近乎组件,有统一接口,可以说是封装了一层对外调用的接口的组件。
- 模块:高内聚,松耦合,功能相对复杂,有多个统一接口。模块化开发的基础是框架。
首先,可以肯定的是,组件化和模块化的中心思想都是分而治之。目的都是将一个庞大的系统拆分成多个组件或者说是模块。
而且如果大的组件也可以称为模块,小的模块也可以称为组件,所以,在我看来组件和模块的划分并没有那么的泾渭分明。随意两者的粒度的大小改变,两者是可以转换的。
最后说一下,这三个概念是经常同时出现在一个项目中的,我们往往对复杂大项目进行模块化划分的时候,也会进行组件化,而且插件化的本质是面向接口编程,对于组件化和模块化都是适用的,可实现随意插拔的灵活和高扩展性,属于项目架构的高端设计。