推荐阅读
- 先从需求与约束出发做架构分层
- 再看核心组件、数据流与边界
- 最后看扩展性、成本与演进策略
一个修改商品价格的功能,接口检查价格是不是整数,应用服务检查一次,领域对象检查一次,保存时又检查一次。代码分成了四层,同一个问题也被问了四遍。 这些检查看起来都在提高可靠性,但如果删掉其中一条,我们能说清楚究竟失去了什么保护吗? 之前的《让领域模型和仓储模型各自负责》讨论了模型与持久化之间的边界。这篇继续讨论一个更具体的问题:边界已经划分之后,哪些检查应该保留,哪些可以交给前面的代码保证? 如果你已经接触过 Python 的 dataclass 和简单分层,但觉得业务代码越来越像校验框架,可以从下面这个改价功能开始。 一个价格,为什么检查四遍 假设商品后台收到这样的 JSON: { "sku": "P1001", "price_cents": 9900 } 价格使用整数分。对于这个例子,业务还约定:价格不能为负数,停售商品不能改价。 容易写出的代码是,在入口、服务、模型和 Repository 中都放上类似判断: if type(price_cents) is not int: raise ValueError("价格必须是整数分") 问题在于,这四次检查通常处理的是同一个已经解析完成的值。只要内部代码没有重新引入不可信数据,重复检查提供的保护就很有限。 真正需要区分的是两个问题: 问题 例子 负责的位置 输入是否符合接口约定? 缺少价格、价格是字符串 外部输入入口 操作是否符合业务规则? 负价格、对停售商品改价 领域模型 应用服务负责组织操作:加载商品、调用改价方法、保存商品。Repository 负责读写和映射。它们不需要把前两类判断各自重写一份。 先建立一个可以信任的入口 Python 的类型注解不会自动验证运行时数据。写下 price_cents: int,并不能阻止调用方传入字符串。 因此,“内部可以信任类型”需要前提:外部数据进入内部流程之前,确实经过了解析和检查。 下面是这个入口。示例采用严格协议,只接受两个字段;不自动把字符串价格转换成整数。 from dataclasses import dataclass, replace @dataclass(frozen=True) class ChangePriceRequest: sku: str price_cents: int def decode_change_price(data: object) -> ChangePriceRequest: if not isinstance(data, dict): raise ValueError("请求必须是 JSON 对象") if set(data) != {"sku", "price_cents"}: raise ValueError("请求必须且只能包含 sku 和 price_cents") sku = data["sku"] if not isinstance(sku, str) or not sku.strip(): raise ValueError("sku 必须是非空字符串") if type(data["price_cents"]) is not int: raise ValueError("价格必须是整数分") return ChangePriceRequest(sku.strip(), data["price_cents"]) 这里使用 type(value) is int,是因为 Python 的 bool 是 int 的子类。这个接口不接受 true 作为价格。 ...
一个策略选择工具,本来只需要让 Agent 查看模板、选择版本、取得写作规则。写着写着,代码却变成了另一副样子:遍历字段名、判断字典形状、检查数组元素、猜测字段类型,再把已经检查过的数据交给下一层继续检查。 每一条判断单独看都有解释,放在一起却让人看不清工具到底做什么。 这发生在一次写作策略工具教程的审查中。删除一个通用转换助手后,代码甚至更长了:检查没有减少,只是搬进了另一个函数。问题并不是少用了一个框架,也不是变量命名不够规范,而是还没决定哪些错误需要处理,就先把能想到的检查写了出来。 本文适合已经了解 Python dataclass、正在让 Agent 编写小型工具或分层应用的读者。以下例子从那次审查中缩小而来,用来说明设计取舍,不是完整策略工具的实现。 前两篇分别讨论模型与仓储的职责和入口检查与业务约束的分工。这篇往前追问一步:在决定校验放在哪里之前,我们为什么需要这条校验? 一个数组转换,怎么长成了通用校验器 假设模板包含 structure 字段。JSON 中它是数组,领域对象中希望用 tuple 表示: structure = tuple(data["structure"]) 这行代码完成转换,但不保证输入正确: assert tuple("post") == ("p", "o", "s", "t") 如果 Agent 把数组写成字符串,转换不会报错,错误会以另一种形式进入模型。因此,在接收 Agent 请求的地方,检查它是不是字符串数组是有具体理由的。 但这不意味着每个处理 structure 的方法,都应该重新执行下面三项检查: 它是不是数组? 数组里的元素是不是字符串? 数组是不是非空? 前两项属于输入协议;第三项取决于业务:这个模板是否允许没有正文结构?如果模型已经保证结构不能为空,入口无需重复保证一次。 同样的字段,也可能来自程序自己写出的文件。是否检查,要先确定文件的使用方式。 数据来源 能依赖的前提 处理方式 Agent 提交的编辑 JSON 尚未确认类型和字段 入口解析,拒绝不符合约定的结构 已解析的内部请求对象 调用者遵守既定契约 直接使用,不重复判断类型 同一程序写出、没有独立编辑入口的文件 写入与读取遵循同一格式 可以直接映射,加载失败时暴露错误 允许人工编辑或导入的文件 内容可能不符合格式 把文件读取当作输入边界,定义需要的检查 “放在仓库里”不等于“可信”。如果维护者经常手改 JSON,它就是一个真实的编辑入口。反过来,受控文件也不是永远不会损坏;选择直接映射,意味着不承诺为每一种损坏提供定制诊断,更不保证发现所有能被转换的错误类型。 一个能直接运行的例子 缩小需求:模板名称不能为空,结构至少有一项,每一项不能是空白文字。外部请求只接受 name 和 structure,不自动转换数字或字符串数组。 保存下面代码为 template_boundary.py,使用 Python 3.10 或更新版本运行: from dataclasses import dataclass @dataclass(frozen=True) class Template: name: str structure: tuple[str, ...] def __post_init__(self): if not self.name.strip(): raise ValueError("模板名称不能为空") if not self.structure or any(not step.strip() for step in self.structure): raise ValueError("模板必须包含非空的写作步骤") def decode_template(data: object) -> Template: if not isinstance(data, dict) or set(data) != {"name", "structure"}: raise ValueError("请求必须包含且只能包含 name 和 structure") if not isinstance(data["name"], str): raise ValueError("name 必须是字符串") steps = data["structure"] if not isinstance(steps, list) or not all(isinstance(step, str) for step in steps): raise ValueError("structure 必须是字符串数组") return Template(name=data["name"], structure=tuple(steps)) def writing_outline(template: Template) -> str: return "\n".join(f"{i}. {step}" for i, step in enumerate(template.structure, 1)) if __name__ == "__main__": template = decode_template({ "name": "问题与解释", "structure": ["提出具体问题", "解释原因与适用条件"], }) print(writing_outline(template)) cases = [ ({"name": "问题与解释", "structure": "post"}, "structure 必须是字符串数组"), ({"name": "问题与解释", "structure": [42]}, "structure 必须是字符串数组"), ({"name": "问题与解释", "structure": []}, "模板必须包含非空的写作步骤"), ({"name": "问题与解释", "structure": [" "]}, "模板必须包含非空的写作步骤"), ({"name": " ", "structure": ["解释"]}, "模板名称不能为空"), ] for payload, expected in cases: try: decode_template(payload) except ValueError as error: assert str(error) == expected else: raise AssertionError(f"错误输入被接受:{payload!r}") print("正常输入和 5 个失败场景均符合约定") 运行 python3 template_boundary.py,输出: ...
在业务系统逐渐变复杂时,一个对象往往同时被拿来承载业务规则、数据库字段和 HTTP 响应。开始时这很省事,后来却会出现连锁问题:数据库新增一个内部字段,接口结构跟着变化;领域代码为了查询数据而依赖 ORM;仓储把 SQLAlchemy 行对象直接交给控制器。 更稳定的做法,是让不同边界拥有不同模型,并通过转换连接它们: Controller -> Domain Model -> Repository Contract -> ORM Model -> Database | ^ +------ API Request/Response ---+ 这里的重点不是增加更多目录,而是明确每个模型对谁负责。也不需要为每个领域类都复制一套存储类:仓储结果可以直接组合领域对象与读取元数据,只有确实不同的格式或契约才需要独立表示。 四类模型,各自回答一个问题 领域模型回答“什么状态符合业务规则”。例如商品必须有 SKU 和名称,价格不能为负数。SKU 是业务标识,数据库自增 id 则不必进入领域对象。领域模型不应该导入 SQLAlchemy、Pydantic 或 Web 框架。 ORM 模型回答“如何把数据存进数据库”。它可以拥有自增主键、外键、索引、时间戳和数据库约束,但这些细节不必进入领域对象。 Repository 契约回答“应用可以怎样保存和读取数据”。它的参数和返回值属于仓储边界:保存商品可以返回 StoredProduct,其中包含领域对象和 updated_at;分页查询可以返回 ProductPage 这样的查询模型。 API 模型回答“客户端能发送和接收什么”。请求模型负责输入形状,响应模型负责 HTTP 输出。例如商品响应可以包含 updated_at,并将其序列化为日期字符串;这不意味着商品领域对象也需要这个字段。 一个最小例子 假设一个后台允许按 SKU 保存商品,再查询商品信息。下面使用同步 SQLAlchemy 2.x 展示关键代码,省略数据库连接和 HTTP 路由配置。 领域对象放在 app/domain/product.py,只表达业务状态。价格统一使用人民币分,避免浮点数表示金额: from dataclasses import dataclass @dataclass(frozen=True) class Product: sku: str name: str price_cents: int def __post_init__(self): if not self.sku or not self.name: raise ValueError("商品 SKU 和名称不能为空") if self.price_cents < 0: raise ValueError("商品价格不能为负数") 数据库模型放在 infra/db/models.py。自增主键、唯一约束和更新时间属于存储设计: ...
副标题 / 摘要 事件驱动通过解耦与异步化让系统更容易横向扩展。本文解释原理、适用场景与工程取舍。 目标读者 设计高并发系统的工程师 关注架构扩展性的团队 需要理解异步架构的人 背景 / 动机 同步调用耦合强、扩展难。 事件驱动能把生产者与消费者解耦,降低系统扩展的阻力。 核心概念 事件:状态变化的不可变记录 发布/订阅:生产者与消费者解耦 异步处理:削峰与并行 实践指南 / 步骤 识别系统中的“事件点” 定义事件契约与版本 引入消息队列或事件总线 为消费者设计幂等处理 可运行示例 # 简化事件驱动示例 subscribers = [] def subscribe(fn): subscribers.append(fn) def emit(event): for fn in subscribers: fn(event) if __name__ == "__main__": subscribe(lambda e: print("A", e)) subscribe(lambda e: print("B", e)) emit({"type": "order.created", "id": 1}) 解释与原理 事件驱动把“谁处理”与“谁产生”分离,让消费者可横向扩展。 异步队列还能削峰,降低高并发冲击。 常见问题与注意事项 事件驱动一定更复杂吗? 是的,需要处理一致性与重试。 如何保证消息不丢? 需要持久化与确认机制。 同步与异步如何取舍? 核心路径保持同步,扩展路径用异步。 最佳实践与建议 事件要小而清晰 消费者必须幂等 对关键事件做审计与追踪 小结 / 结论 事件驱动通过解耦与异步提升扩展性,但会增加一致性与运维复杂度。 合理拆分同步与异步路径是关键。 ...
副标题 / 摘要 “相似商品推荐”的核心是共购关系。本文用最小协同过滤思路解释实现方法。 目标读者 需要搭建推荐功能的工程师 负责电商系统的开发者 学习基础推荐算法的人 背景 / 动机 推荐系统能提升转化率与停留时间。 最简单的实现方式是基于“共购/共点”统计。 核心概念 协同过滤:基于用户行为的相似性 共购矩阵:商品一起出现的次数 召回与排序:先找候选,再排序 实践指南 / 步骤 收集用户行为(购买/浏览) 统计共现关系 生成候选集 结合热度或规则排序 可运行示例 from collections import Counter def recommend(orders, item): co = Counter() for order in orders: if item in order: for x in order: if x != item: co[x] += 1 return [x for x, _ in co.most_common(3)] if __name__ == "__main__": orders = [ ["A", "B", "C"], ["A", "B"], ["A", "D"], ["B", "C"], ] print(recommend(orders, "A")) 解释与原理 协同过滤假设“经常一起出现的商品更相关”。 这是冷启动与小规模电商的常用起点。 ...
副标题 / 摘要 CPU 不再靠单核频率无限提升,而是通过多核、缓存层级与指令并行提升性能。本文解释编程影响。 目标读者 关注性能优化的工程师 学习系统与硬件基础的开发者 需要理解并发趋势的人 背景 / 动机 “频率增长带来的免费午餐”已经结束。 现代 CPU 的性能提升更多来自并行与缓存,这改变了编程方式。 核心概念 缓存层级:L1/L2/L3 影响访问延迟 多核与并行:性能来自并发执行 分支预测与流水线:影响指令效率 实践指南 / 步骤 关注内存访问局部性 优化缓存友好数据结构 利用并行,但避免过度同步 关注分支与热点路径 可运行示例 # 简单示意:顺序访问 vs 随机访问 import random def sequential(n): data = list(range(n)) s = 0 for x in data: s += x return s def random_access(n): data = list(range(n)) idx = list(range(n)) random.shuffle(idx) s = 0 for i in idx: s += data[i] return s if __name__ == "__main__": print(sequential(10000)) print(random_access(10000)) 解释与原理 现代 CPU 更依赖缓存与并行。 顺序访问通常比随机访问更快,因为缓存命中率更高。 ...
副标题 / 摘要 DoS 不一定来自攻击。设计缺陷也可能导致资源被耗尽。本文总结常见架构陷阱。 目标读者 负责系统稳定性的工程师 关注性能与可靠性的团队 架构与运维负责人 背景 / 动机 系统高负载时,设计缺陷会放大为雪崩。 理解这些风险能提前避免“自我 DoS”。 核心概念 雪崩效应:局部故障扩散 资源耗尽:线程、连接、内存被占满 放大效应:重试与级联调用放大负载 实践指南 / 步骤 限制重试与并发 设置超时与熔断 在关键路径加限流 避免长链路同步调用 可运行示例 # 简化的“重试放大”示意 def request(retry=3): for _ in range(retry): # 失败后重试会放大负载 pass return "done" if __name__ == "__main__": print(request()) 解释与原理 无上限的重试、同步长链路与共享资源竞争,会让系统在高负载下崩溃。 这类问题往往比攻击更常见。 常见问题与注意事项 重试为什么危险? 重试会放大流量,导致雪崩。 限流会影响用户体验吗? 会,但比整体崩溃更可控。 缓存也会导致 DoS 吗? 缓存击穿会导致瞬时洪峰。 最佳实践与建议 引入熔断与限流 做压力测试与混沌演练 对缓存击穿进行保护 小结 / 结论 DoS 不只来自外部攻击,设计缺陷也会造成系统不可用。 控制重试与资源使用是关键。 参考与延伸阅读 Release It! Chaos Engineering 元信息 阅读时长:6~8 分钟 标签:可靠性、DoS SEO 关键词:拒绝服务, 架构缺陷 元描述:总结设计缺陷导致 DoS 的常见原因。 行动号召(CTA) 列出你系统中的“高放大系数”路径,并制定降级策略。
副标题 / 摘要 故障切换保证服务可用,会话管理保证用户体验。本文给出常见策略与实践建议。 目标读者 设计高可用系统的工程师 负责用户体验的后端团队 架构与运维负责人 背景 / 动机 分布式系统不可避免会发生节点故障。 如何快速切换并保持用户会话,是高可用系统的关键。 核心概念 故障切换:主节点失败时快速切换 会话存储:本地或共享 无状态服务:降低切换成本 实践指南 / 步骤 使用健康检查与心跳检测故障 实现主备或多活切换 把会话外置到共享存储 使用粘性会话或无状态策略 可运行示例 # 简化“会话外置”示意 session_store = {} def set_session(uid, data): session_store[uid] = data def get_session(uid): return session_store.get(uid) if __name__ == "__main__": set_session("u1", {"cart": [1, 2]}) print(get_session("u1")) 解释与原理 故障切换要求服务无状态或会话可共享。 会话外置能保证切换后用户状态不丢失。 常见问题与注意事项 会话一定要外置吗? 高可用场景建议外置。 粘性会话可以吗? 可以,但会降低切换能力。 多活会话一致性怎么做? 需要一致性存储或冲突解决策略。 最佳实践与建议 服务尽量无状态化 会话数据存入 Redis 等共享存储 故障切换定期演练 小结 / 结论 故障切换与会话管理密切相关。 无状态服务与外置会话是实现高可用的关键。 ...
副标题 / 摘要 P2P 系统的核心是去中心化的节点发现与数据分发。本文给出设计要点与简化示例。 目标读者 学习分布式架构的工程师 想设计去中心化系统的团队 关注可扩展性与鲁棒性的开发者 背景 / 动机 P2P 系统不依赖中心节点,天然具有扩展性与鲁棒性。 但它也带来一致性与安全挑战。 核心概念 节点发现:让新节点找到网络 路由:在节点间转发请求 一致性:保证数据分布与收敛 实践指南 / 步骤 定义节点身份与地址 设计引导节点或 DHT 机制 实现消息转发与路由表 加入心跳与节点淘汰 可运行示例 # 简化的 P2P 广播示例 class Node: def __init__(self, name): self.name = name self.peers = [] def connect(self, peer): self.peers.append(peer) def broadcast(self, msg): print(self.name, "->", msg) for p in self.peers: p.receive(msg) def receive(self, msg): print(self.name, "received", msg) if __name__ == "__main__": a, b, c = Node("A"), Node("B"), Node("C") a.connect(b) b.connect(c) a.broadcast("hello") 解释与原理 P2P 的难点在于“无中心”。 需要通过节点发现与路由机制保证请求可达。 ...
副标题 / 摘要 统一设计能保证一致性,但也可能削弱团队自治。本文讨论这一张力,并给出可行平衡方案。 目标读者 架构师与技术负责人 需要治理多团队协作的管理者 关注组织效率的工程师 背景 / 动机 大型系统需要统一设计以避免混乱,但过度集中决策会压制创新。 如何在一致性与自治之间找到平衡,是组织设计难题。 核心概念 统一设计:统一标准与技术路线 自治团队:独立决策与快速试错 架构治理:通过规则而非控制实现统一 实践指南 / 步骤 明确哪些是必须统一的(协议、数据、基础设施) 允许在边界内自由实验 建立架构评审而非架构审批 用平台化能力替代强制管控 可运行示例 # 简化“统一与自治”的策略表 policy = { "must": ["logging format", "auth"], "free": ["framework choice", "code style"], } if __name__ == "__main__": print(policy) 解释与原理 统一设计不是“架构师独裁”,而是“在关键处统一、在边界内自治”。 平台化能力能减少强制控制的需求。 常见问题与注意事项 过度统一会带来什么问题? 抑制创新与降低团队积极性。 完全自治会怎样? 系统碎片化与治理成本激增。 如何避免架构审批瓶颈? 建立规则与标准,减少人为审批。 最佳实践与建议 明确“统一清单”与“自由清单” 用平台能力统一基础设施 通过评审传播最佳实践 小结 / 结论 统一设计不等于贵族统治。 关键在于明确边界、用规则治理而非人治。 参考与延伸阅读 Team Topologies Evolutionary Architecture 元信息 阅读时长:6~8 分钟 标签:架构治理、团队协作 SEO 关键词:统一设计, 架构治理 元描述:讨论统一设计与团队自治的平衡。 行动号召(CTA) 列出你团队当前“必须统一”的项目,并评估是否过度集中。