软件架构:5种你应该知道的模式
Singleton(单例模式)、仓储模式(repository)、工厂模式(factory)、建造者模式(builder)、装饰模式(decorator)……大概每个上课听讲的程序员都不会陌生——软件的设计模式为我们提供了针对现有的、重复出现的问题以可靠的解决方案。
在软件架构方面同样存在类似的机制,通用的、可重用的解决方案在给定上下文中的软件体系结构中经常出现的问题。不同的软件架构模式各有千秋,以下是目前较为主流的5种软件架构模式。
分层模式(Layered Pattern)
分层模式大概是最知名的软件架构模式之一,有大量的开发者使用,但并不知道它的名字。分层模式将代码拆分为“层”,每个层都有一定的责任,并为更高“层”服务。
分层模式并没有规定层的数量,但通常会有以下结构:
- 表现层 / UI层
- 应用层
- 业务/域(domain)层
- 持久/数据访问层
- 数据库层
分层模式的想法是用户通过执行某些动作(例如点击按钮)在表现层启动一段代码。随后,表现层调用应用层、进入业务层,最后持久层将所有内容存储在数据库中。简单来说,分层模式中的高层调用并依赖低层。
根据应用复杂程度,我们会看到很多相应的变体。例如某些应用会省略应用层,而某些应用会添加缓存层,甚至会出现两层合一的情况。
层责任
如上所述,每层有每层的责任。表现层包含应用的图形设计和处理交互的代码。理论上来讲,我们不应该在这一层添加任何与user interface无关的逻辑。
业务层是放置特定业务问题模型和逻辑的地方。
应用层位于业务层和和表现层之间。一方面为表现层提供业务层抽象,另一方面为应用层提供放置某些不适合放置于业务层或表现层的某些协调逻辑。
持久层包含访问数据库层的代码,而数据库层是底层数据库技术,例如SQL Server、MongoDB。持久层是用于操作数据库的代码集:SQL语句、连接详情等。
优势
- 大多数开发者都很熟悉
- 一种用于编写组织良好并且可测试应用的简单方法
劣势
- 分层模式往往会导致应用的“一体化”并使之变得难以拆分
- 开发者往往会发现自己编写了大量的代码来传递不同的层,而没有在这些层里添加任何值。如果我们做的只是编写一个简单的CRUD应用,分层模式可能有点过分了。
适用于
- 标准的、不仅仅用于完成CRUD操作的业务线(line-of-business)应用
微内核模式(Microkernel)
当应用程序有一组核心职责和一组可互换的部件时,微内核模式(或插件模式)非常有用。微内核将提供应用程序的入口点和一般流程,而不需要真正了解不同的插件在做什么。
例如任务调度,微内核可以包含所有的调度和触发逻辑,而插件负责特定的任务。只要插件遵循特定的API,微内核就可以出发它们,而不需要了解实现的细节。
另一个例子是工作流。工作流的实现包含诸如不同步骤的顺序、评估步骤的结果、决定下一步的内容等概念,步骤的的具体实现对于工作流的核心代码并不重要。
优势
- 灵活性 & 可扩展性
- 某些实现允许我们在应用运行时添加插件
- 微内核和插件可以由不同团队开发
劣势
- 难以确定哪些东西属于微内核
- 预定义的API可能不适合未来的插件
适用于
- 从不同来源获取数据、转换数据并写入不同地方的应用
- 工作流应用
- 任务和作业调度应用
命令职责查询分离模式(CQRS)
CQRS是Command and Query Responsibility Segregation的缩写。这种模式的核心概念是应用具有完全分离的读取操作和写入操作,这也意味着用于写操作(命令)的模型和读取(查询)不同。此外,数据将存储在不同的位置。在关系数据库中,意味着将存在用于命令模型的表和用于读取模型的表。一些实现甚至将不同的模型存储在完全不同的数据库中,例如用于命令模型的SQL Server和用于读取模型的MongoDB。
CQRS模式通常和事件溯源(Event Sourcing)结合,下一小节会讲到。
CQRS如何工作?当用户执行操作,应用会向命令服务器发送命令。命令服务从命令数据库中检索所需的任何数据,进行必要的操作并将其存储回数据库中,然后它通知读取服务,以便更新读取模型。如下所示:
当应用需要向用户显示数据时,它可以通过调用读取服务来检索读取模型,如下所示:
优势
- 命令模型专注于业务逻辑和验证,读取模型根据特定情境进行定制
- 可以避免复杂的查询,让读取更高效
劣势
- 保持命令和读取模型同步可能会让事情变得很复杂
适用于
- 需要大量读取的应用
- 有复杂域的应用
事件溯源模式(Event Sourcing)
这种模式不会将模型的当前状态存储在数据库中,而是将时间存储其中。因此,例如customer name发生变化时,该值不会存储在“name“列中,我们会存储一个”NameChanged“事件。
当我们需要检索模型时,我们将检索其存储的所有事件并在新对象上重新应用,即rehydrating an object。
用EXCEL记账来理解event sourcing会容易一些。当我们添加支出时,我们不需更改总值,而是增加一“行”,出现错误,也增加一“行”,最终利用EXCEL的公式自动计算出总数,而这里计算总数可以看作是读取模型。
您可以看到我们在添加Invoice 201805时发生了错误。我们添加了两条新行,而不是更改行,首先是一行取消错误的行,然后是新的正确行。这就是event sourcing的工作方式。你永远不会删除事件,因为它们无可否认地发生在过去。为了纠正情况,我们添加了新事件。
另外,请注意我们是如何获取总值的,它是上面单元格中所有值的总和。在Excel中,它会自动更新,因此我们可以说它与其他单元格同步。这就是一个读取模型。
Event sourcing通常会与CQRS同时使用,因为rehydrating an object可能会对性能产生影响,尤其是当实例中存在大量事件时。快速读取模型可以显着改善应用的响应时间。
优势
- 提供“开箱即用”的audit log,每个事件即特定时间点上的特定操作
劣势
- event sourcing有一定的限制要求,我们不能直接通过数据库中的简单编辑来修复错误数据
- 改变事件的结构比较复杂
适用于
- 需要发布事件到外部系统的应用
- CQRS应用
- 有复杂域的应用
- 需要数据修改audit log的应用
微服务模式(Microservices)
编写一组微服务实际上就是编写可以协同的多个应用。每个微服务都有自己独特的责任,团队可以独立于其他微服务开发它们。他们之间唯一的依赖是通信。当微服务相互通信时,我们必须确保它们之间发送的消息保持backwards-compatible(向后兼容)。这需要一些协调,特别是当不同的团队负责不同的微服务时。
如下图所示——
在上图中,应用调用一个中央API,将调用转发给正确的微服务。在此示例中,有为用户配置文件、库存、订单和付款提供单独的服务。我们可以想象这是一个用户订购应用。单独的微服务也可以相互调用。例如,支付服务可以在支付成功时通知订单服务。然后,订单服务可以调用库存服务来调整库存。
没有明确规定微服务有多大。在前面的示例中,用户配置文件服务可能负责用户的用户名和密码等数据,也可能负责家庭地址、头像图像、收藏夹等,也可以选择将所有这些责任分成更小的微服务。
优势
- 我们可以单独编写、维护、部署微服务
- 微服务架构容易扩展
- 重写变得很容易,因为微服务之间松耦合
劣势
- 实际上没有合适的工具平台,反而在最初编写结构良好的一体化应用,再拆分为微服务的办法实际上更容易。使用微服务,会产生许多额外的问题:通信、协调,向后兼容、日志记录等。没有编写结构良好的整体结构的必要技能的团队可能很难编写一组良好的微服务。
- 传统微服务架构可能会有多个失败点,查找问题可能会比较复杂
适用于
- 某些组件被密集使用并需要扩展的应用
- 为其他应用提供功能的应用
- 一体化架构下很复杂的应用
- 能够明确定义bounded contexts的应用
总结
各种软件架构模式很多时候会结合起来使用,他们之间并没有我们想象得那么水火难容。换句话说,没有万能的软件架构模式,如何选择取决于我们对于解决方案利弊的权衡。