精通 Python 设计模式——架构型设计模式

104 阅读21分钟

在上一章中,我们介绍了行为型模式——它们帮助我们处理对象之间的互联与算法。本章要讨论的下一类是架构型设计模式。这些模式为解决常见的架构问题提供了模板,便于构建可扩展、可维护、可复用的系统。

本章将涵盖以下主要主题:

  • 模型–视图–控制器(MVC)模式
  • **微服务(Microservices)**模式
  • **无服务器(Serverless)**模式
  • **事件溯源(Event Sourcing)**模式
  • 其他架构型设计模式

在本章结束时,你将理解如何使用流行的架构型设计模式来构建健壮且灵活的软件。

技术要求

参见第 1 章给出的通用要求。本章相关代码的附加技术要求如下:

微服务模式一节需要安装:

  • gRPC:python -m pip install grpcio
  • gRPC-tools:python -m pip install grpcio-tools
  • Lanarky 及其依赖:python -m pip install "lanarky[openai]"==0.8.6 uvicorn==0.29.0
    (注意:撰写时与 Python 3.12 不兼容;可使用 Python 3.11 复现相关示例。)

无服务器模式一节需要安装:

  • Docker
  • LocalStack(用于本地测试 AWS Lambda):python –m pip install localstack
    (注意:撰写时与 Python 3.12 不兼容;此处可使用 Python 3.11。)
  • awscli-local:python -m pip install awscli-local
  • awscli:python -m pip install awscli

事件溯源一节需要安装:

  • eventsourcing:python –m pip install eventsourcing

MVC 模式

MVC 模式是松耦合原则的又一应用。名称来自将应用拆分为三个主要组件:模型(Model)视图(View)控制器(Controller)

即使我们很少从零实现 MVC,也需要熟悉它,因为主流框架都使用 MVC 或其变体(稍后会提到)。

  • 模型(Model) 是核心,代表“知识”。它包含并管理应用的(业务)逻辑、数据、状态与规则
  • 视图(View) 是模型的可视化呈现:如桌面 GUI、终端文本输出、手机 App 界面、PDF、饼图、柱状图等。视图只显示数据,不处理数据。
  • 控制器(Controller) 是模型与视图之间的链接/粘合剂。模型与视图之间的所有通信都经由控制器进行。

一个使用 MVC 的应用(在初始屏幕渲染后)通常按以下流程工作:

  1. 用户通过点击/输入/触摸等触发视图
  2. 视图告知控制器用户的动作。
  3. 控制器处理输入并与模型交互。
  4. 模型完成必要的校验与状态变更,并告知控制器该做什么。
  5. 控制器指示视图更新并按模型的指示展示输出。

image.png 图 6.1 – MVC 模式

控制器是否一定需要?
理论上可以省略,但会失去 MVC 的一个重要收益:在不修改模型的前提下支持多个视图(甚至同时) 。为实现模型与其表示的解耦,通常每个视图需要自己的控制器。若模型直接与特定视图通信,将很难(或至少不够干净、模块化地)支持多视图。

现实世界示例

MVC 体现的是关注点分离(Separation of Concerns) 。生活中大量使用该原则:

  • 盖房子时,通常分别由不同专业人员进行水电安装粉刷
  • 餐厅中,服务员负责点单与上菜,而厨师负责烹饪。

在 Web 开发中,多个框架采用 MVC 思想,例如:

  • Web2py:轻量级 Python 框架,拥抱 MVC。项目网站(web2py.com/)与 GitHub 上有大量示例。
  • Django:本质上也是 MVC,但命名不同——称为 MVT(Model-View-Template) 。在 Django 中,controllerview,而 viewtemplate。Django 的设计者认为,view 描述用户“看到哪些数据”,因此以 Python 回调函数(与 URL 绑定)命名为 view;template 用于内容与表现分离,描述“数据如何被看到”,而非“看到哪些数据”。

MVC 的适用场景

MVC 十分通用实用。几乎所有流行的 Web 框架(Django、Rails、Symfony、Yii)与应用框架(iPhone SDK、Android、QT)都使用 MVC 或其变体(如 MVA、MVP、MVT)。即便不使用框架,自行实现 MVC 也很有意义,收益包括:

  • 视图与模型分离:设计师专注 UI,程序员专注开发,互不干扰。
  • 松耦合:任一方可独立修改/扩展而不影响另一方。例如,新增一个视图非常容易——为其实现一个控制器即可。
  • 职责清晰,便于维护。

从零实现 MVC 时,请确保做到:聪明的模型(Smart Models)瘦控制器(Thin Controllers)笨视图(Dumb Views)

Smart Model(聪明的模型)

  • 包含所有校验/业务规则/业务逻辑
  • 处理应用状态
  • 访问应用数据(数据库、云等)
  • 不依赖 UI

Thin Controller(瘦控制器)

  • 当用户与视图交互时,更新模型
  • 当模型变化时,更新视图
  • 必要时对数据做预处理再交给模型/视图
  • 不直接显示数据
  • 不直接访问应用数据
  • 不包含校验/业务规则/逻辑

Dumb View(笨视图)

  • 展示数据
  • 允许用户与其交互
  • 仅做极少量处理(通常由模板语言提供,如变量与循环)
  • 不存储数据
  • 不直接访问应用数据
  • 不包含校验/业务规则/逻辑

自检你的 MVC 实现是否到位,可思考:

  • 若应用有 GUI,是否易于换肤(skin) ?能否在运行时切换皮肤?若不容易,说明实现可能存在问题。
  • 若当前无 GUI(如终端应用),添加 GUI 的难度如何?或者,是否容易新增视图以图表(饼图、柱状图)或文档(PDF、表格)方式展示结果?若这些更改并非“只需为新视图创建一个控制器、而无需修改模型”,则 MVC 未正确实现。

满足这些条件,应用的灵活性与可维护性将显著优于未使用 MVC 的实现。

MVC 模式的实现

为完整说明 MVC,我们不依赖任何框架,而是从零实现一个极简示例: “名言打印机” 。思路:用户输入一个数字,程序显示对应的名言。名言保存在 quotes 元组中(现实中通常在数据库或文件里,且只有模型能直接访问)。

示例 quotes 元组如下:

quotes = (
    "A man is not complete until he is married. Then he is finished.",
    "As I said before, I never repeat myself.",
    "Behind a successful man is an exhausted woman.",
    "Black holes really suck...",
    "Facts are stubborn things.",
)

模型极简:只有 get_quote() 方法,按索引 n 返回对应字符串:

class QuoteModel:
    def get_quote(self, n):
        try:
            value = quotes[n]
        except IndexError as err:
            value = "Not found!"
        return value

视图有三个方法:

  • show():在屏幕上打印名言或“Not found!”
  • error():打印错误信息
  • select_quote():读取用户选择
class QuoteTerminalView:
    def show(self, quote):
        print(f'And the quote is: "{quote}"')
    def error(self, msg):
        print(f"Error: {msg}")
    def select_quote(self):
        return input("Which quote number would you like to see? ")

控制器负责协调:__init__() 初始化模型与视图;run() 校验用户输入的索引,从模型取回名言并交给视图显示:

class QuoteTerminalController:
    def __init__(self):
        self.model = QuoteModel()
        self.view = QuoteTerminalView()
    def run(self):
        valid_input = False
        while not valid_input:
            try:
                n = self.view.select_quote()
                n = int(n)
                valid_input = True
            except ValueError as err:
                self.view.error(f"Incorrect index '{n}'")
        quote = self.model.get_quote(n)
        self.view.show(quote)

最后,main() 初始化并启动控制器:

def main():
    controller = QuoteTerminalController()
    while True:
        controller.run()

示例回顾(完整代码见 ch06/mvc.py):

  1. 定义名言列表变量;
  2. 定义模型类 QuoteModel
  3. 定义视图类 QuoteTerminalView
  4. 定义控制器类 QuoteTerminalController
  5. 编写 main() 测试,并按惯例调用。

运行 python ch06/mvc.py 的示例输出:

Which quote number would you like to see? 3
And the quote is: "Black holes really suck..."
Which quote number would you like to see? 2
And the quote is: "Behind a successful man is an exhausted woman."
Which quote number would you like to see? 6
And the quote is: "Not found!"
Which quote number would you like to see? 4
And the quote is: "Facts are stubborn things."
Which quote number would you like to see? 3
And the quote is: "Black holes really suck..."
Which quote number would you like to see? 1
And the quote is: "As I said before, I never repeat myself."

微服务(Microservices)模式

传统上,开发者在构建服务端应用时,通常使用单一代码库,在其中实现全部或大部分功能,配合我们在本书前面介绍过的函数、类以及各种设计模式来组织代码。

然而,随着 IT 行业的发展、经济因素的影响,以及对更快上市投资回报的压力,工程团队需要持续改进实践,以在服务器、服务交付与运维方面获得更强的响应性与可扩展性。这意味着我们需要了解除面向对象编程(OOP)之外的更多有用模式。

image.png 图 6.2 – 微服务模式

近年来工程师常用模式目录中最重要的新增成员之一,就是微服务架构(Microservice Architecture,简称微服务)。其思想是:把应用构建为一组松耦合、可协作的服务。在这种架构风格下,一个应用可能由订单管理服务客户管理服务等多个服务组成。它们松耦合可独立部署,并通过定义良好的 API 进行通信。

现实世界示例

例如:

  • Netflix:最早采用微服务以同时处理数以百万计的内容流。
  • Uber:用微服务分别处理计费、通知、行程跟踪等不同职能。
  • Amazon:从单体架构迁移到微服务以支撑持续增长的规模。

适用场景

当你的应用具备以下一项或多项特征时,微服务往往是聪明的选择:

  • 需要同时支持桌面与移动端等不同客户端
  • 需要对外提供第三方可用的 API
  • 必须通过消息与其他应用通信
  • 处理请求时需要访问数据库、与其他系统通信,并返回 JSON/XML/HTML/PDF 等不同类型的响应
  • 存在按功能领域划分的逻辑组件

实现微服务模式——使用 gRPC 的支付服务

先简谈微服务世界里的安装与部署。从“部署一个单体应用”切换到“部署许多小服务”,需要处理的事项会指数级增长。以往也许一个应用服务器加少量运行时依赖就够用;到了微服务时代,依赖会大幅增加:某个服务需要关系型数据库,另一个需要 ElasticSearch;这边用 MySQL,那边用 Redis……因此,采用微服务通常也意味着要使用容器

得益于 Docker,我们可以把“应用服务器、依赖与运行库、编译产物、配置”等一并打包到容器里,然后启动这些容器化服务,并确保它们之间能够通信。

当然,你也可以直接用 Django/Flask/FastAPI 来实现 Web 或 API 的微服务。为了快速给出可运行示例,这里采用 gRPC ——一个高性能的通用 RPC 框架,使用 Protocol Buffers(protobuf) 作为接口描述语言,跨语言、效率高,非常适合微服务间通信。

设想一个架构,其中有一个专门处理支付的微服务(称为 PaymentService),负责处理支付,并与 OrderServiceAccountService 等其他服务交互。我们将聚焦于使用 gRPC 实现该支付服务。

首先,在 ch06/microservices/grpc/payment.proto 中用 protobuf 定义服务与方法(包括请求与响应消息):

syntax = "proto3";
package payment;

// The payment service definition.
service PaymentService {
  // Processes a payment
  rpc ProcessPayment (PaymentRequest) returns (PaymentResponse) {}
}

// The request message containing payment details.
message PaymentRequest {
  string order_id = 1;
  double amount = 2;
  string currency = 3;
  string user_id = 4;
}

// The response message containing the result of the payment process.
message PaymentResponse {
  string payment_id = 1;
  string status = 2; // e.g., "SUCCESS", "FAILED"
}

然后使用 protocpayment.proto 编译为 Python 代码。一般命令形式如下:

python -m grpc_tools.protoc -I<PROTO_DIR> --python_out=<OUTPUT_DIR> --grpc_python_out=<OUTPUT_DIR> <PROTO_FILES>

在本例中,切到正确目录(如 cd ch06/microservices/grpc),执行:

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. payment.proto

该命令将在当前目录生成两份文件:payment_pb2.pypayment_pb2_grpc.py不要手动修改它们)。

接着在 payment_service.py 中编写服务逻辑,继承生成文件中的基类并实现方法。例如:

from concurrent.futures import ThreadPoolExecutor
import grpc
import payment_pb2
import payment_pb2_grpc

class PaymentServiceImpl(payment_pb2_grpc.PaymentServiceServicer):
    def ProcessPayment(self, request, context):
        return payment_pb2.PaymentResponse(payment_id="12345", status="SUCCESS")

grpc.server(ThreadPoolExecutor(max_workers=10)) 启动服务:

def main():
    print("Payment Processing Service ready!")
    server = grpc.server(ThreadPoolExecutor(max_workers=10))
    payment_pb2_grpc.add_PaymentServiceServicer_to_server(PaymentServiceImpl(), server)
    server.add_insecure_port("[::]:50051")
    server.start()
    server.wait_for_termination()

写一个客户端(ch06/microservices/grpc/client.py)来调用服务:

import grpc
import payment_pb2
import payment_pb2_grpc

with grpc.insecure_channel("localhost:50051") as chan:
    stub = payment_pb2_grpc.PaymentServiceStub(chan)
    resp = stub.ProcessPayment(
        payment_pb2.PaymentRequest(
            order_id="order123",
            amount=99.99,
            currency="USD",
            user_id="user456",
        )
    )
    print("Payment Service responded.")
    print(f"Response status: {resp.status}")

启动服务(ch06/microservices/grpc/payment_service.py):

python ch06/microservices/grpc/payment_service.py

输出应类似:

Payment Processing Service ready!

另开一个终端运行客户端(ch06/microservices/grpc/client.py):

python ch06/microservices/grpc/client.py

你应看到:

Payment Service responded.
Response status: SUCCESS

这正是预期结果。

虽然 gRPC 是微服务通信的强力之选,但在更强调可读性Web 集成的场景下,基于 HTTP 的 REST 也完全可用。gRPC 的优势在于性能与对流式请求/响应的支持,本例用它来做一个简明介绍。

实现微服务模式——使用 Lanarky 的 LLM 服务

Lanarky 基于 FastAPI,为构建使用**大语言模型(LLM)**的微服务提供“电池全备”的体验。我们按照其官网 lanarky.ajndkr.comGetting started 步骤,展示一个由 Lanarky 驱动的微服务。

要测试该示例,需要设置环境变量 OPENAI_API_KEY 以使用 OpenAI。访问 openai.com 获取 API Key。

服务端示例代码(节选):

import os
import uvicorn
from lanarky import Lanarky
from lanarky.adapters.openai.resources import ChatCompletionResource
from lanarky.adapters.openai.routing import OpenAIAPIRouter

# 传入 OpenAI API Key(示例仅为演示)
os.environ["OPENAI_API_KEY"] = "Your OpenAI API key here"

安全实践
建议在shell 环境变量里设置密钥,再由代码读取,不要把密钥硬编码在源码中。

创建应用与路由(与 FastAPI 约定一致),Lanarky 提供 OpenAIAPIRouter

app = Lanarky()
router = OpenAIAPIRouter()

定义 /chat 的 POST 处理函数:

@router.post("/chat")
def chat(stream: bool = True) -> ChatCompletionResource:
    system = "Here is your assistant"
    return ChatCompletionResource(stream=stream, system=system)

把路由挂到应用并用 uvicorn 启动:

if __name__ == "__main__":
    app.include_router(router)
    uvicorn.run(app)

编写客户端与服务交互,例如:

import click
import sys
from lanarky.clients import StreamingClient

args = sys.argv[1:]
if len(args) == 1:
    message = args[0]
    client = StreamingClient()
    for event in client.stream_response(
        "POST",
        "/chat",
        params={"stream": "false"},
        json={"messages": [dict(role="user", content=message)]},
    ):
        print(f"{event.event}: {event.data}")
else:
    print("You need to pass a message!")

测试方式与前面 gRPC 示例类似。首先在一个终端启动 LLM 服务(ch06/microservices/lanarky/llm_service.py):

python ch06/microservices/lanarky/llm_service.py

你应看到类似输出:

INFO:   Started server process [18617]
INFO:   Waiting for application startup.
INFO:   Application startup complete.
INFO:   Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

另开终端运行客户端:

python ch06/microservices/lanarky/client.py "Hello"

输出示例:

completion: Hello! How can I assist you today?

继续发送消息,服务会返回补全结果,如同在 ChatGPT 界面中交互。例如:

python ch06/microservices/lanarky/client.py "What is the capital of Switzerland?"
completion: The capital of Switzerland is Bern.

无服务器(Serverless)模式

Serverless 模式将服务器管理抽象掉,让开发者只需专注于编写代码。云服务商根据事件触发(例如 HTTP 请求、文件上传或数据库修改)来负责扩缩容与执行。

image.png 图 6.3 —— 无服务器(Serverless)模式
Serverless 尤其适用于微服务、API 与事件驱动架构。

现实世界示例

我们可以想到多种 Serverless 的应用场景,例如:

  • 自动数据备份:定时触发的无服务器函数将重要数据自动备份到云存储
  • 图片处理:用户上传图片后,函数自动进行裁剪、压缩或滤镜处理
  • 电商收据 PDF 生成:交易完成后,函数生成 PDF 收据并通过邮件发送给客户

适用场景

Serverless 主要适用于两类架构:

  1. 事件驱动架构:当事件发生时执行特定函数,例如图片处理(裁剪、缩放)或动态 PDF 生成。
  2. 微服务架构:每个微服务都可以做成一个无服务器函数,便于管理与横向扩展。

上一节我们已经讨论了微服务,本节聚焦第一类用法的实现方式。

实现 Serverless 模式

下面以 AWS Lambda 为例,演示一个对数字求平方的最小函数。AWS Lambda 是亚马逊的无服务器计算服务,可在数据变化、系统状态改变或用户动作等触发下运行代码。

我们不额外增加业务复杂度,专注于 Serverless 架构与 AWS Lambda 的部署要点。

首先,编写 Python 函数。创建 lambda_handler(),接收 eventcontext 两个参数。这里我们从 event 字典的 "number" 键读取输入,计算平方并返回结果字符串:

import json

def lambda_handler(event, context):
    number = event["number"]
    squared = number * number
    return f"The square of {number} is {squared}."

有了函数后,需要将其部署为 AWS Lambda。为了学习与本地快速测试,我们不直接部署到真·AWS,而是使用 LocalStack 在本地模拟 AWS 环境。

安装好 LocalStack 后,在你的环境中以 Docker 容器方式启动它(localstack 可执行文件安装自 Python 环境):

localstack start -d

将 Python 源文件(ch06/lambda_function_square.py)打成 ZIP 包,例如:

zip lambda.zip lambda_function_square.py

我们还需要 awslocal(需单独安装)。使用它把 Lambda 函数部署到“本地 AWS”基础设施中。命令如下:

awslocal lambda create-function \
    --function-name lambda_function_square \
    --runtime python3.11 \
    --zip-file fileb://lambda.zip \
    --handler lambda_function_square.lambda_handler \
    --role arn:aws:iam::000000000000:role/lambda-role

请按你的 Python 版本调整 --runtime
上述示例在撰写时使用 Python 3.11。请根据你的环境修改此参数。

使用 payload.json 提供输入,调用该 Lambda 函数:

awslocal lambda invoke --function-name lambda_function_square \
    --payload file://payload.json output.txt

随后查看 output.txt 的内容,应该能看到:

The square of 6 is 36.

这是本地的初步测试。我们还可以为该函数创建一个 函数 URL。同样用 awslocal:

awslocal lambda create-function-url-config \
    --function-name lambda_function_square \
    --auth-type NONE

该命令会生成一个用于调用 Lambda 的 URL,形如:

http://<XXXXXXXX>.lambda-url.us-east-1.localhost.localstack.cloud:4566

现在就可以用 cURL 触发这个 URL 了,例如:

curl -X POST \
  'http://iu4s187onr1oabg50dbvm77bk6r5sunk.lambda-url.us-east-1.localhost.localstack.cloud:4566/' \
  -H 'Content-Type: application/json' \
  -d '{"number": 6}'

关于 AWS Lambda 的最新与详细指南,请参考官方文档:docs.aws.amazon.com/lambda/

以上是一个极简示例。另一个典型的 Serverless 应用,是生成业务收据 PDF 的函数:企业无需管理服务器,只为实际消耗的计算时间付费。

事件溯源(Event Sourcing)模式

事件溯源模式将状态变更保存为一系列事件,从而可以重放这些事件来重建任意历史时刻的状态,并提供可审计的变更轨迹。该模式特别适用于状态复杂业务规则(状态迁移)复杂的系统。

正如稍后实现示例所示,事件溯源强调:对应用状态的所有更改都要捕获为有序事件。其结果是:只需回放这些事件,就能在任意时间点重建应用状态。

现实世界示例

在软件领域有不少实例:

  • 审计追踪:为合规而记录对数据库所做的全部更改
  • 协同编辑:允许多个用户同时编辑同一文档
  • 撤销/重做:为应用中的操作提供撤销与重做能力

适用场景

典型用例包括(举三例):

  • 金融交易:将账户余额的每一次变更记录为时间顺序上的不可变事件(存款、取款、转账均为独立事件),从而形成透明、可审计且安全的账本。
  • 库存管理:以事件记录每件物品生命周期中的每次变更,保持库存数据准确、可追溯,并有助于识别销量/使用模式与预测需求;在召回或质量调查时可追根溯源。
  • 用户行为追踪:将用户的每次交互(浏览、加购、下单、退货等)记录为事件,以便进行行为分析、个性化营销、体验优化与推荐改进。

下面看看如何实现该模式。

实现事件溯源模式——手工方式

先给出一些定义:

  • Event(事件) :对状态变更的表示,通常包含事件类型与相关数据。事件一旦创建并应用,就不可更改
  • Aggregate(聚合) :代表统一业务逻辑或数据边界的对象(或对象簇)。它维护自身的状态,每次发生变更(事件)时记录该事件。
  • Event store(事件存储) :保存系统中发生的全部事件的集合。

通过“事件驱动”的方式处理状态变更,可以让业务逻辑更灵活、易扩展:新增事件类型或调整既有事件的处理都能将影响面降到最低。

在这个银行账户的示例中,我们手工实现事件溯源:定义事件数据结构并手写事件应用逻辑。

我们先定义 Account 类,表示带有余额与事件列表的银行账户。该类充当聚合,其 events 属性同时扮演事件存储。这里的事件用字典表示,包含操作类型("deposited""withdrawn")与金额:

class Account:
    def __init__(self):
        self.balance = 0
        self.events = []
    def apply_event(self, event):
        if event["type"] == "deposited":
            self.balance += event["amount"]
        elif event["type"] == "withdrawn":
            self.balance -= event["amount"]
        self.events.append(event)

接着添加存取款方法,内部均调用 apply_event()

    def deposit(self, amount):
        event = {"type": "deposited", "amount": amount}
        self.apply_event(event)
    def withdraw(self, amount):
        event = {"type": "withdrawn", "amount": amount}
        self.apply_event(event)

最后是用于测试的 main()

def main():
    account = Account()
    account.deposit(100)
    account.deposit(50)
    account.withdraw(30)
    account.deposit(30)
    for evt in account.events:
        print(evt)
    print(f"Balance: {account.balance}")

运行(python ch06/ event_sourcing/bankaccount.py)输出示例:

{'type': 'deposited', 'amount': 100}
{'type': 'deposited', 'amount': 50}
{'type': 'withdrawn', 'amount': 30}
{'type': 'deposited', 'amount': 30}
Balance: 150

这个例子用最简方式帮你建立了对事件溯源的直观理解。更复杂的系统可使用专门的库来管理事件存储、查询与处理。下面试试一个库。

实现事件溯源模式——使用库

第二个例子我们使用 eventsourcing 库来实现一个库存管理系统,跟踪商品数量。

首先导入需要的内容:

from eventsourcing.domain import Aggregate, event
from eventsourcing.application import Application

定义聚合 InventoryItem,继承 Aggregate,并用 @event 装饰器声明会产生事件的方法:

class InventoryItem(Aggregate):
    @event("ItemCreated")
    def __init__(self, name, quantity=0):
        self.name = name
        self.quantity = quantity
    @event("QuantityIncreased")
    def increase_quantity(self, amount):
        self.quantity += amount
    @event("QuantityDecreased")
    def decrease_quantity(self, amount):
        self.quantity -= amount

再定义应用层 InventoryApp,继承 Applicationsave() 会收集聚合上未提交的事件并写入应用的事件存储:

class InventoryApp(Application):
    def create_item(self, name, quantity):
        item = InventoryItem(name, quantity)
        self.save(item)
        return item.id

增加数量增/减的方法:先通过仓库取回聚合,调用其行为方法,然后 save()

    def increase_item_quantity(self, item_id, amount):
        item = self.repository.get(item_id)
        item.increase_quantity(amount)
        self.save(item)
    def decrease_item_quantity(self, item_id, amount):
        item = self.repository.get(item_id)
        item.decrease_quantity(amount)
        self.save(item)

测试代码如下:

def main():
    app = InventoryApp()
    # Create a new item
    item_id = app.create_item("Laptop", 10)
    # Increase quantity
    app.increase_item_quantity(item_id, 5)
    # Decrease quantity
    app.decrease_item_quantity(item_id, 3)
    notifs = app.notification_log.select(start=1, limit=5)
    notifs = [notif.state for notif in notifs]
    for notif in notifs:
        print(notif.decode())

运行(python ch06/ event_sourcing/inventory.py)输出示例:

{"timestamp":{"_type_":"datetime_iso","_data_":"2024-03-18T08:05:10.583875+00:00"},"originator_topic":"__main__:InventoryItem","name":"Laptop","quantity":10}
{"timestamp":{"_type_":"datetime_iso","_data_":"2024-03-18T08:05:10.584818+00:00"},"amount":5}
{"timestamp":{"_type_":"datetime_iso","_data_":"2024-03-18T08:05:10.585128+00:00"},"amount":3}

很好!这个例子与前一个一起展示了事件溯源式应用的设计思路。对于更有野心的项目,可以借助 eventsourcing 等库,降低事件存储与重放等基础设施的实现复杂度。

其他架构设计模式

你还可能遇到下列架构模式:

  • 事件驱动架构(EDA) :强调事件的产生、检测、消费与响应,具有高度适应性与可扩展性,适用于需要对重要事件实时反应的环境。
  • 命令查询职责分离(CQRS) :将“写入(命令)”模型与“读取(查询)”模型分离,尤其在读写关注点差异明显时,有助于扩展性与可维护性。
  • 整洁架构(Clean Architecture) :通过依赖反转等原则,将业务逻辑与对外接口解耦,强调以领域为中心来组织代码。

小结

本章我们探讨了若干关键的架构设计模式,它们在现代软件开发中各有侧重、各解难题:

  • MVC:通过将应用划分为模型、视图与控制器三部分,促进关注点分离,使代码更易管理、扩展与测试。
  • 微服务:将应用拆分为小而独立的服务,每个服务负责单一业务能力,提升伸缩性、灵活性与部署效率,适合复杂且快速演进的业务。
  • 无服务器(Serverless) :把重心从服务器管理转向业务逻辑,借助云服务按事件执行代码,降低成本、自动扩缩容并提升生产力。
  • 事件溯源(Event Sourcing) :把每次数据变更记录为事件序列,既提供强大的审计追踪与复杂业务能力,又能重建历史状态、洞察数据演进。
  • 其他模式(如 CQRS、整洁架构):进一步丰富工程师的架构工具箱,帮助构建结构良好、可维护的系统。

在下一章中,我们将讨论并发与异步模式与技术,帮助程序同时管理多项操作,或在等待耗时任务时继续处理其他工作。