将系统划分为模块(微服务)的必要性

时间:2022-10-14 07:10:29

日常编程中,很少有人谈论怎么去设计这些程序或者什么样的程序是好程序。

关键软件开发,我们能在软件工程的课中学到敏捷开发、结对编程、版本控制系统、面向对象程序设计等等。但是软件设计的核心问题呢?


David Parnas 在 1971 年的论文(On the criteria to be used in decomposing systems into modules)提到了如何将系统分解为模块,如果就今天而言,众多微服务的本质也就是这篇论文的核心,复杂系统如果将系统分解为服务的标准上。


从摘要的第一句话开始,您将发现一些具有现代发展的共同目标:“本文讨论模块化,这是提高系统灵活性和可理解性的一种机制,同时允许其缩短其开发时间。”灵活性(我们倾向于将其称为敏捷性),而今天用更快的开发时间仍然是最重要的。

少得多的可理解性,但也许我们应该在那里更加关注?假设您都购买了基于云的本机微服务架构,是否将系统分为多个独立服务,以帮助您实现目标?Parnas 带来了敏锐的见解:

"模块化 "的有效性取决于将系统划分为模块时使用的标准。

需要友情提示的是,这不是简单的拥有大量的小模块,你的系统的成功与否很大程度上取决于你首先选择如何将系统划分为模块。 当 Parnas 在论文中谈到 "模块 "时,他的定义是一个工作分配单元,而不是一个子程序单元。

Parnas 提出了模块化编程的三个预期好处。我们也可以从微服务的角度来看待这些:

1. 开发时间应该缩短,因为不同的小组可以在每个模块(微服务)上工作,几乎不需要沟通。

2. 应提高产品的灵活性--希望可以在不改变其他模块(微服务)的情况下,对一个模块(微服务)进行相当大的改变或改进。这一点又可被看做降低程序的耦合。

3. 可理解性--希望系统可以一次研究一个模块(微服务),结果是整个系统可以被更好地设计,因为它被更好地理解。

将系统划分为模块的不同方式带来了在这些模块上工作的个人(或团队)之间不同的沟通和协调要求,并有助于在或多或少的程度上实现上述好处。


该论文核心的著名例子是开发了一个系统,以生成一个 "KWIC "索引,给定一组有序的行作为输入。任何一行都可以通过重复删除第一个词并将其添加到行尾来进行 "循环移位"。该系统输出一个按字母顺序排列的所有行的循环移位列表。Parnas 承认这有点像一个玩具的例子,但 "我们将通过练习,把这个问题当作一个大项目来处理"。

研究了两种分解方式。在第一个分解中,处理工作流程中的每个主要步骤或任务都被做成自己的独立模块(服务)。 这就导致了五个模块:

1. 一个输入模块,从输入介质中读取数据线

2. 一个圆形移位器

3. 一个 "字母表"(分类器)

4. 一个输出模块,可以创建一个很好的格式化的输出

5. 一个主控制模块,对其他四个模块进行排序

模块 1 到 4 是在内存中的共享数据结构上操作。

将系统划分为模块(微服务)的必要性

这是模块化编程的所有支持者所理解的模块化。系统被划分为一些相对独立的模块,并有明确的接口;每个模块都足够小,足够简单,可以被彻底理解和很好地编程。 小规模的实验表明,这大约是大多数程序员为指定任务提出的分解。

第二次分解在表面上看起来类似:

1. 一个带有对线路进行操作的程序的线路存储模块

2. 和以前一样的输入模块,但它调用行存储模块,使行在内部存储

3. 一个循环移位器,利用线路存储模块移位线路

4. 一个字母表

5. 一个建立在循环移位器功能基础上的输出模块

6. 一个主控制模块

将系统划分为模块(微服务)的必要性

然而,这种分解是在信息隐藏的基础上产生的。

有一些设计决定是有问题的,而且在许多情况下可能会发生变化......正是通过观察这样的变化,我们可以看到两种模块化之间的差异。

在第一次分解中,许多变化(例如,决定将所有行存储在内存中)需要每个模块的变化,但在第二次分解中,许多更多的潜在变化被限制在单个模块中。

此外,在第一次分解中,模块之间的接口是相当复杂的格式,代表了不能轻视的设计决定。"这些格式的开发将是模块开发的主要部分,这部分必须由几个开发小组共同完成"。 在第二次分解中,接口更简单、更抽象,导致模块的独立开发更快。

关于可理解性,在第一次分解中,系统只能真正被理解为一个整体," Parnas主观判断是,在第二次模块化中,这不是真的"。

第二个分解中的每个模块的特点是它对一个设计决定的了解,它对所有其他的模块都是隐藏的。它的界面或定义被选择为尽可能少地揭示其内部工作原理。

不过,第二种分解方式有一个潜在的缺点,在将模块打包成独立服务时,这个缺点就更加重要。

如果我们不小心,第二种分解方式的效率就会大大降低。如果每个 "函数 "实际上是作为一个具有复杂调用顺序的过程来实现的,那么由于模块之间的反复切换,将会有大量的这种调用。而第一种分解方式则不会出现这种问题,因为模块之间的控制转移相对较少。

为了避免这种开销,Parnas 建议使用一种工具,使程序的编写就像函数是子程序一样,但通过任何适当的机制进行组装。这在微服务的世界里更具挑战性!

总之,虽然人们对将系统划分为模块(微服务)的必要性给予了很多关注,但对我们决定模块边界的标准却关注得很少。正如 Parnas 所展示的,在你的下一个项目中考虑这些标准可能是一个好主意,因为它们对开发时间、系统敏捷性和可理解性有很大影响。

我们试图通过这些例子来证明,在流程图的基础上开始将一个系统分解成模块,几乎总是不正确的。

相反,我们建议从一个困难的设计决定或可能会改变的设计决定的清单开始。然后,每个模块都被设计成对其他模块隐藏这样的决定。由于在大多数情况下,设计决定超越了执行的时间,模块将不对应于处理过程中的步骤。


论文原文:On the criteria to be used in decomposing systems into modules David L Parnas, 1971


灵感来源:https://blog.acolyer.org/2016/09/05/on-the-criteria-to-be-used-in-decomposing-systems-into-modules/