浅谈模块化设计
- 架构笔记
- 2021-04-25
- 268热度
- 0评论
导航
1 模块与模块化设计
1.1 什么是模块?
广义来说,模块就是“一个软件块”,下至函数、类,上至服务、应用,都可称之为模块。但这样的说法未免过于笼统,本文讨论的模块是狭义的,定义如下:
软件模块是可部署的、可管理的、原生可重用的、可组合的、无状态的软件单元,它为用户提供了简洁的接口。
- 可部署:模块是一个独立部署单元(因此区别于类和包)。
- 可管理:模块是一个可管理的单元。模块可以单独安装、卸载以及更新。一旦规划好边界,不同的模块可以并行开发。
- 可测试:模块是一个测试单元,可以独立测试。
- 原生可重用:模块是进程内部的可重用单元,其操作是通过直接调用方法触发的。
- 可组合:模块是可组合的单元。模块可以由其他模块组成。
- 无状态:模块是无状态的,我们不会实例化软件模块(但需要实例化模块中的类,类是有状态的)。
模块的概念,比“服务”小一级,比“类”大一级。
更具体一些,在 Java 中,模块化单元就是 JAR 文件。
1.2 什么是模块化设计?
粗浅理解:
- 针对模块、包的设计,就是模块化设计
- 以模块为基本元素来设计系统,就是模块化设计
1.3 为什么需要模块化设计?
1.3.1 分离复杂度
要编写复杂软件又不至于一败涂地的唯一方法,就是用定义清晰的接口把若干简单模块组合起来,如此一来,多数问题只会出现在局部,那么还有希望对局部进行改进或优化,而不至于牵动全身。——《UNIX 编程艺术》
当我们要构建一个复杂软件系统时,仅有 OO 设计模式是不够的。因为设计模式工作在类级别,过于琐碎和具体。设想一个项目,包含上千个类,相互之间耦合严重,依赖关系错综复杂,很难理解和维护;假如把这些类分组,封装到十几个不同的模块中,定义好每个模块的接口和依赖关系,这个项目会清晰很多。也就更容易维护和扩展。
1.3.2 辅助沟通,对齐理解
架构师更关注应用和服务,开发人员更关注具体的代码,中间有一个空白地带——模块和包。
架构师和开发人员,都应主动关注,填补空白。
2 模块化设计思路
总的来说,把系统划分为一个个相互协作的模块,尽可能做到“低耦合高内聚”。
2.1 模块划分原则
除了 1.1 中给出的模块定义之外,还有一些指导原则:
- 大小适宜:封装出来的模块,不能过大,也不能过小(详见 2.2.1 和 3.1 关于“粒度”的讨论);
- 紧凑性:就是一个设计是否能装进人脑中的特性。与“内聚”的概念类似。符合用户直觉,没有多余的东西;
- 正交性:每一个动作只改变一件事,不会影响其他。改变每个属性的方法有且只有一个。例如,显示器就是正交控制的。你可以独立改变亮度而不影响对比度;
- 单点性:即 DRY 原则(或称 SPOT 原则——Single Point of Truth,真理的单点性)。任何一个知识点在系统内都应当有一个唯一、明确、权威的表述。尽可能复用,不要重新发明轮子。
2.2 分层架构
书上有很多模块级别的模式,可以去查阅,这里不展开。主要讲一下实战中用得最多的架构模式——分层。
2.2.1 粒度与层次
粒度指的是一个系统要拆分成的各个部分的范围。
- 粗粒度的实体做得更多,行为上更丰富;细粒度的实体做得更少,具备更高的可重用性。
- 粗粒度的实体更加抽象,更靠近用户;细粒度的实体更加具体,更靠近机器。
很自然地产生一种层次关系:粗粒度的实体(策略)在上层,贴合用户需求;细粒度的实体(机制)在底层,驱动硬件设备。
这两种层次截然不同,无法交流,因此中间还需要一个“胶合层”。
例如:源代码->编译器->机器码,就是这样一种三层架构。
后端服务中有:Controller -> Service -> DAO,也是三层架构。
下面是一个典型的架构分层图:
2.2.2 模块与层次——全景图
一方面,层次本身就是一种广义的模块;另一方面,把模块嵌入各个层次,更容易定义其职责和依赖关系。
以下是全景图:
2.2.3 前后端分离与层次
这个值得单独提一下。我在帆软的时候,一直对前后端分离感兴趣,好奇它是怎么实践的,到底有什么好处;后来在依图,用前后端分离的方式做了快两年的开发,深刻体会到,它确实是个好东西。
前后端分离,是一种模块化设计思路,是一种层次架构。系统粗分为前端模块、后端模块;或称之为前端展现层、后端服务层。
前端实现策略,后端实现机制。策略变化相对快,机制变化相对慢,在接口固定的情况下,两边可以独立变化,互不干扰。
3 关键设计权衡
3.1 模块粒度:可用/重用悖论
最大化重用会使得可用复杂化。
可重用的模块必须是灵活的,而随着灵活性的增加,会对应着复杂性的增加。同样地,增加模块使用、管理以及部署的便利性会减少模块的可重用性。
启示
一味追求灵活性、“松耦合”(细粒度),是不对的;一味追求便利性(粗粒度),也是不对的。
需要根据实际情况做出取舍。
常用解决方案
先有一堆细粒度的模块,作为灵活方案;然后再将它们组织成一个粗粒度的模块,作为便利方案。
例如,软件配置文件中,尽量多提供选项(细粒度、灵活性高),同时,又提供一份默认配置,开箱即用(粗粒度)。就像智能手机,无需配置,开机即用;当你真的需要某个冷门功能时,又可以去设置里面把选项找出来。
3.2 灵活性:好钢用在刀刃
为了尽量做到“低耦合高内聚”,OO 设计中提供了 SOLID 原则。
灵活性虽然提高了,但是由于额外抽象层的引入,系统复杂性也相应增加。如果盲目使用,增加的复杂性实际上会降低我们维护和扩展软件的能力。(Linux 坚持使用宏内核的紧耦合方案,亦有其道理)
那么,最适合使用 SOLID 原则的地方在哪里?系统中的结合点,也就是两个模块边界上的连接点。这里需要最大的灵活性和弹性。
3.3 薄胶合层:勿滥用抽象
所谓的胶合层,就是整合上层策略(应用逻辑)与底层机制(域元语级)的中间代码。
如果过于强调抽象,很可能写出大量的中间代码,使得胶合层越来越厚,越来越复杂。这样做可能弊大于利。
按照 UNIX 传统,胶合层越薄越好。如果有许多代码既不属于策略,又不属于机制,很可能除了增加系统复杂度之外,毫无用处。
(例如职场上某些人,既不做上层规划和决策,又不做底层执行,只是在中间传话(就像胶合层)。在扁平化的公司里,生存空间是极小的)
ref:
- 《UNIX 编程艺术》
- 《Java 应用架构设计——模块化模式与 OSGi》
- 耦合与内聚