探索软件构造的艺术:项目代码优化与架构演进学习笔记
引言:从“跑通就好”到“软件构造”
作为一名计算机专业的大三学生,在过去的一坤年里,我的编程追求往往停留在“代码能跑通,功能能实现”的阶段。无论是在课堂大作业还是早期的个人项目中,我习惯于从控制层直接调用数据库,或者在一个函数里塞满各种网络请求、业务校验和数据拼装。
然而,在我进入实验室实习,跟随师哥师姐开发项目后发现,随着项目规模的逐渐扩大,这种“面条式”的代码结构开始暴露出致命的缺陷:修改一个微小的需求往往会引发难以预料的连锁Bug,单元测试几乎无法编写,新技术的引入(比如替换一个数据库或者第三方服务)更是堪比重构整个系统。
万幸跟随刘明义老师学习了“软件构造”课程,深度参与并学习了一个具有工业级架构规范“前景”的开源项目优化过程,我才真正领悟到“软件工程”与“写代码”的本质区别。软件构造不仅仅是算法和数据结构的堆砌,更是关于如何控制复杂性、如何建立合理的边界、如何让代码在未来数年的演进中依然保持生命力的一门艺术。
这篇文章,是我对近期在软件构造、代码重构、领域驱动设计(DDD)、防御性编程以及设计模式等核心知识点的深度梳理与总结。为了让这些抽象的架构理念更加具象化,我将抛开具体的特定项目背景,采用通用的、脱敏的业务场景(如电商订单、支付网关等)作为案例进行剖析。希望这篇文章不仅能成为我大学阶段的技术沉淀,也能为每一位在架构演进道路上探索的同学提供一些启发。
一、 架构模式探秘:从面条代码到六边形架构
1. 软件复杂度的万恶之源:耦合与“上帝模块”
在传统的MVC架构中,我们常常不知不觉地创造出“上帝类”或“万能工具层”。以一个典型的Web应用为例,控制器(Controller)往往不仅处理HTTP请求,还包含了鉴权、参数校验、核心业务规则判断,甚至直接通过ORM框架操作数据库。与此同时,我们还会创建一个名为 utils 的包,里面堆满了诸如 db_utils、network_utils 等工具类。
这种架构的痛点在于:业务逻辑与基础设施(如数据库、第三方API、文件系统)高度耦合。一旦我们需要将MySQL迁移到PostgreSQL(鄙人有幸被折磨过),或者将本地文件存储切换到云端OSS,业务代码将面临伤筋动骨的修改。
2. 破局之道:端口与适配器架构(六边形架构)
为了解决这一问题,六边形架构(Hexagonal Architecture,又称端口与适配器架构)应运而生。它的核心思想是:应用的核心业务逻辑(领域层)必须与外部世界(用户界面、数据库、外部API)完全隔离。
在六边形架构中,系统被清晰地划分为内部(领域和应用逻辑)和外部(各种基础设施)。内外之间的通信必须通过“端口(Ports)”来进行,而外部的具体技术实现则是“适配器(Adapters)”。
- 端口(Ports):本质上是应用层定义的一组接口(Interface或Protocol)。它声明了应用层需要什么样的数据,或者需要外部系统提供什么样的行为,但绝对不关心这些行为是如何实现的。
- 适配器(Adapters):是端口的具体实现者。它们负责将外部世界的技术细节翻译成应用层能理解的语言。
3. 案例:电商订单服务的重构
假设我们正在开发一个电商系统的订单处理模块。在重构前,我们的订单服务是这样写的:
# 【反面教材:高度耦合的代码】
from sqlalchemy.orm import Session
from third_party.sms_sdk import send_sms
from db.models import OrderModel
class OrderService:
def __init__(self, db_session: Session):
self.db = db_session
def place_order(self, user_id: str, product_id: str, amount: float):
# 1. 核心业务逻辑:校验金额
if amount <= 0:
raise ValueError("Invalid amount")
# 2. 基础设施细节:直接操作ORM
new_order = OrderModel(user_id=user_id, product_id=product_id, amount=amount)
self.db.add(new_order)
self.db.commit()
# 3. 基础设施细节:直接调用第三方SDK
send_sms(user_id, f"您的订单已生成,金额:{amount}")
return new_order.id
这段代码的问题显而易见:OrderService 直接依赖了具体的ORM框架(SQLAlchemy)和具体的短信SDK。这违反了依赖倒置原则。
采用六边形架构重构后:
# 【优雅重构:端口与适配器模式】
from typing import Protocol
# --- 1. 端口定义(Ports) ---
# 位于应用层,应用层定义自己需要的接口
class OrderRepositoryPort(Protocol):
def save(self, order: OrderAggregate) -> str:
...
class NotificationGatewayPort(Protocol):
def notify_user(self, user_id: str, message: str) -> None:
...
# --- 2. 核心应用服务 ---
# 只依赖端口,完全不知道数据库和短信的具体技术
class OrderService:
def __init__(self, repo: OrderRepositoryPort, notifier: NotificationGatewayPort):
self.repo = repo
self.notifier = notifier
def place_order(self, user_id: str, product_id: str, amount: float):
# 纯粹的业务逻辑与领域操作
if amount <= 0:
raise ValueError("Invalid amount")
order = OrderAggregate(user_id=user_id, product_id=product_id, amount=amount)
order_id = self.repo.save(order)
self.notifier.notify_user(user_id, f"您的订单已生成,金额:{amount}")
return order_id
# --- 3. 适配器实现(Adapters) ---
# 位于基础设施层,负责实现端口
class SqlAlchemyOrderRepository(OrderRepositoryPort):
def __init__(self, db_session):
self.db = db_session
def save(self, order: OrderAggregate) -> str:
db_order = map_to_db_model(order)
self.db.add(db_order)
self.db.commit()
return db_order.id
class AliyunSmsGateway(NotificationGatewayPort):
def notify_user(self, user_id: str, message: str) -> None:
# 调用阿里云SDK发短信
pass
通过引入 OrderRepositoryPort 和 NotificationGatewayPort,OrderService 彻底摆脱了对外部技术的依赖。这种解耦带来的直接好处是极高的可测试性:在单元测试中,我们只需要传入一个内存中的字典来模拟 Repository,传入一个空函数来模拟 Gateway,就可以在毫秒级验证订单的业务逻辑,而无需启动真实的数据库。
二、 依赖倒置原则(DIP)的深度实践
1. 为什么我们要抽象接口?
在上一节的重构中,我们实际上深刻应用了面向对象设计五大原则(SOLID)中的核心——依赖倒置原则(Dependency Inversion Principle, DIP)。
传统的开发思维是“自顶向下”的:高层模块决定需要实现什么功能,然后调用底层模块的基础工具去完成。这种思维导致高层模块强依赖于底层模块。 而DIP要求我们反转这种依赖关系:高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
2. 控制反转(IoC)与依赖注入(DI)
为了实现依赖倒置,我们通常会结合控制反转(IoC)思想,并通过依赖注入(Dependency Injection)来实现。应用层在启动时,由外部框架或组装脚本将具体的适配器实例“推”给服务类,而不是由服务类自己去 new 一个底层对象。
这种设计使得代码具备了极其灵活的替换能力。假设某天公司因为成本原因,要求将所有短信通知渠道从阿里云切换为腾讯云。在符合DIP架构的系统中,我们只需要做两件事:
- 编写一个新的
TencentSmsGateway类,实现NotificationGatewayPort接口。 - 在应用启动的依赖注入容器中,将原来绑定到该接口的实现类替换为新的实现类。
整个核心业务模块 OrderService 不需要修改哪怕一行代码。这正是“对扩展开放,对修改关闭”(开闭原则,OCP)的完美体现。
三、 防御性编程与状态安全:写出坚不可摧的代码
除了宏观架构的治理,微观层面的代码鲁棒性同样决定了软件的质量。在学习中,我深刻认识到了防御性编程(Defensive Programming)的重要性,尤其是抽象函数(AF)与表示不变量(RI)的概念,彻底改变了我对对象状态管理的认知。
1. 抽象函数(AF)与表示不变量(RI)
在构建抽象数据类型(ADT)时,我们不仅是在写类和方法,更是在建立一套严格的契约。
- 表示不变量(Rep Invariant, RI):定义了对象内部状态必须始终满足的合法条件。它是对象存活期间的“绝对真理”。
- 抽象函数(Abstraction Function, AF):描述了对象内部的物理表示(属性)是如何映射到客户端所理解的逻辑抽象的。
例如,我们正在开发一个AI对话系统的“会话上下文(SessionContext)”管理器,它负责维护用户与AI之间的历史聊天记录。
class SessionContext:
"""
AF:
- 表示一次对话会话中的消息上下文,按时间顺序维护可发送给模型的消息序列。
RI:
- messages 列表不能为 None。
- 每条消息必须包含且仅关注 role 与 content 两个核心字段。
- role 必须属于 {'system', 'user', 'assistant'}。
- content 必须是字符串。
- 消息总数不得超过 max_messages 限制。
"""
def __init__(self, max_messages: int = 100):
self._messages = []
self._max_messages = max_messages
self._check_rep()
def _check_rep(self):
"""内部防御性校验机制,在每次改变状态后调用"""
assert self._messages is not None
assert len(self._messages) <= self._max_messages
for msg in self._messages:
assert "role" in msg and "content" in msg
assert msg["role"] in {"system", "user", "assistant"}
assert isinstance(msg["content"], str)
在类的设计中,我们通过私有方法 _check_rep() 强制约束 RI。这是一种极佳的防御性编程手段,一旦系统的任何操作导致上下文状态被破坏,程序会立即在此处抛出异常,而不是让错误的数据流向底层大模型引发更难以排查的故障。这种“尽早失败(Fail Fast)”的思想是构建高可靠系统的基石。
2. 警惕表示暴露(Representation Exposure)的致命危害
仅仅有内部校验是不够的。许多时候,Bug是由于外部代码在不经意间篡改了对象的内部状态引起的,这就是所谓的“表示暴露”。
来看一个初学者常犯的错误:
class Team:
"""
AF:
- ...
RI:
- members 列表不能为 None。
- ...
- 不能出现 "hit" 字段
"""
def __init__(self, members):
self._members = members
def get_members(self):
return self._members # 经典的暴露引用
你在外部使用时:
t = Team(['xu', 'qi', 'zhe'])
m = t.get_members()
m.append('hit') # 看似在操作自己的列表,实际上修改了内部数据
print(t.get_members()) # ['xu', 'qi', 'zhe', 'hit'] ← 内部被改了!
显然,这种暴露会让我们代码中对RI的约束直接废掉,不仅会对我们自己的日常开发造成影响,也会让外部人员通过此漏洞篡改我们的数据。
基础防御:浅拷贝(返回副本)
为了防止表示暴露,当我们需要向外提供内部可变对象的访问时,必须进行防御性拷贝(Defensive Copying)。我直接不交原件,给一份复印件不就可以了,外部随便改!
class Team:
def __init__(self, members):
self._members = members
def get_members(self):
return self._members.copy() # 返回列表的浅拷贝
现在外部对返回的列表做增删就不会影响内部了:
t = Team(['xu', 'qi', 'zhe'])
m = t.get_members()
m.append('hit')
print(t.get_members()) # ['xu', 'qi', 'zhe'] ← 安全了
浅拷贝的破绽
但如果列表中装的不是简单字符串,而是可变对象(比如字典、列表),浅拷贝就不够用了。
假设我们把队员信息改成字典,包含姓名和角色:
class Team:
def __init__(self):
self._members = [
{'name': 'xu', 'role': 'Developer'},
{'name': 'qi', 'role': 'Designer'}
]
def get_members(self):
return self._members.copy() # 浅拷贝:只复制了列表,内部字典还是共享的
外部代码拿到拷贝后,如果修改了某个字典的内容:
t = Team()
m = t.get_members()
m[0]['role'] = 'Manager' # 修改了字典
print(t.get_members()[0]) # {'name': 'xu', 'role': 'Manager'} ← 又暴露了!
解释:list.copy() 只复制了最外层容器(列表本身),里面的字典并没有复制,新旧列表里的字典还是同一个对象。所以修改字典会影响到原对象。
最佳实践:深拷贝与不可变性
所以如果对象层级较深,还需要使用深拷贝(Deep Copy)。
import copy
class Team:
def __init__(self):
self._members = [
{'name': 'xu', 'role': 'Developer'},
{'name': 'qi', 'role': 'Designer'}
]
def get_members(self):
return copy.deepcopy(self._members) # 深层复制:连内部字典也复制了
现在外部怎么改都不会影响内部:
t = Team()
m = t.get_members()
m[0]['role'] = 'Manager'
m[0]['name'] = 'zhe'
m.append({'name': 'hit', 'role': 'Hacker'})
print(t.get_members())
# [{'name': 'xu', 'role': 'Developer'}, {'name': 'qi', 'role': 'Designer'}]
# 内部数据完好无损!
我们的防御思路没有变,依旧是返回一份拷贝,只是拷贝得更加彻底。实际开发中,是否必须用深度拷贝取决于数据结构有多“深”。如果内部数据比较简单(例如只装不可变元素),浅拷贝就够了;一旦有嵌套可变对象,得用 deepcopy 把“保险箱”里的每一层文件都复印一遍。
四、 策略模式与动态注册:构建可插拔的 Provider 架构
在现代软件开发中,我们经常需要对接同一领域的不同供应商。比如电商系统需要对接微信支付、支付宝、银联;或者AI应用需要对接OpenAI、Anthropic、智谱等多种大语言模型。
1. 拒绝无限的 if-else
如果不做良好的设计,我们的代码很可能演变成这样:
def call_llm(provider_name: str, prompt: str):
if provider_name == "openai":
# 组装 openai 的参数并调用
pass
elif provider_name == "anthropic":
# 组装 anthropic 的参数并调用
pass
elif provider_name == "zhipu":
# 组装 zhipu 的参数并调用
pass
else:
raise ValueError("Unsupported provider")
这种硬编码分支(Hard-coded Branches)违反了开闭原则。每增加一个新供应商,我们都必须修改这个核心函数。当供应商数量达到十几个,且每个供应商的调用逻辑都很复杂时,这个文件将变成无法维护的“灾难现场”。
2. 抽象基类与反射注册表的优雅结合
解决这一痛点的黄金组合是“策略模式(Strategy Pattern) + 动态注册表(Registry)”。我们将每一种供应商抽象为一个 Provider 策略,并通过工厂模式动态分发。
可以理解为抽象出通用框架(基类),随后逐个进行个性化定制(子类)。
第一步:定义统一的 Provider 契约(基类)
from abc import ABC, abstractmethod
class ModelProvider(ABC):
@property
@abstractmethod
def provider_id(self) -> str:
"""提供商的唯一标识"""
pass
@abstractmethod
async def generate_response(self, prompt: str) -> str:
"""核心业务逻辑契约"""
pass
第二步:实现具体的 Provider 子类
我们将不同的实现分散在独立的文件中,互不干扰。
# openai_provider.py
class OpenAIProvider(ModelProvider):
@property
def provider_id(self) -> str:
return "openai"
async def generate_response(self, prompt: str) -> str:
return f"OpenAI Response for: {prompt}"
# anthropic_provider.py
class AnthropicProvider(ModelProvider):
@property
def provider_id(self) -> str:
return "anthropic"
async def generate_response(self, prompt: str) -> str:
return f"Anthropic Response for: {prompt}"
第三步:构建自动扫描与注册的 ProviderManager
我们可以利用 Python 的元编程或模块扫描机制,在项目启动时自动加载所有的 Provider,将其注册到一个内部的字典中。
class ProviderManager:
def __init__(self):
self._providers = {}
def register(self, provider: ModelProvider):
self._providers[provider.provider_id] = provider
def get_provider(self, provider_id: str) -> ModelProvider:
if provider_id not in self._providers:
raise ValueError(f"Provider {provider_id} not found")
return self._providers[provider_id]
# 运行时调用
manager = ProviderManager()
manager.register(OpenAIProvider())
manager.register(AnthropicProvider())
# 业务代码彻底清爽,无需关心底层分支
async def process_user_query(provider_id: str, query: str):
provider = manager.get_provider(provider_id)
return await provider.generate_response(query)
这种架构的优越性在于它的可插拔性(Pluggability)。当未来需要接入百度文心一言时,开发者只需在 providers 目录下新建一个符合契约的 BaiduProvider.py 类,系统重启后即可自动识别并接管对应流量。核心的路由层和业务服务层对此毫不知情,达到了零代码修改扩展的最高境界。
五、 代码坏味道与重构手法的刻意练习
在持续优化的过程中,我学会了像老中医一样去“嗅探”代码中的坏味道(Code Smells),并使用结构化的重构手法进行手术。
1. 拆解“上帝类”与职责委派(Delegation)
在早期的项目中,我经常发现某些核心服务类承担了太多不属于自己的工作。比如一个数据库连接器(DatabaseConnector)不仅负责管理TCP连接池,还负责遍历整个数据库去采集所有的表结构、外键和索引(很明显这不是一个”连接“器该做得事情)。
这不仅导致类文件长达上千行,更破坏了单一职责原则(SRP),后果就是:后续某个功能需要遍历整个数据库去采集所有的表结构、外键和索引时,是单独重写一个还是使用这个连接器?协作者看到使用了连接器,他难道还要费心费力去找到连接器具体代码看写了啥额外”兼职“?
重构手法:委派(Delegation)我的超级大脑告诉我该使用超级拼装了
我们将原本混杂在连接器中的元数据采集逻辑提取出来,创建一个新的服务类 DatabaseStructureService。原先的连接器仅仅作为参数传递给这个服务,提供纯粹的“执行SQL并返回结果”的能力。采集服务则专注于拼装和映射数据模型。
通俗点来说(外卖小哥的隐喻):
想象一下,你在经营一家公司,雇佣了一个“外卖小哥”(相当于 DatabaseConnector)。外卖小哥的本职工作(单一职责)应该只是“把包裹安全快速地从A点送到B点”(提供数据库连接、执行基础SQL并把原始结果拿回来)。
但在早期的“上帝类”设计里,你不仅让外卖小哥送包裹,还让他把包裹里的零件一个个拆出来,帮你组装成一台复杂的机器,顺便再写一份详细的零件清单(对应:遍历解析复杂的表结构、外键和索引)。这会导致极其荒谬的局面:
- 外卖小哥不堪重负:他的工作手册(类文件)写了上千行,既有骑车交通指南(TCP连接池管理),又有高级机械组装图纸(元数据解析),极难维护。
- 别人不敢用,导致重复造轮子:当公司另一个部门只想找人简单送个信时,看到这个外卖小哥的名片上写着“精通跑腿与机械自动化组装”,他们觉得太重了,大概率会重新去招一个纯粹的跑腿小弟(重新写一个连接逻辑),导致代码重复。
委派(Delegation)是怎么解决这个问题的呢?
我们专门设立一个“高级装配部”(相当于新创建的 DatabaseStructureService)。当装配部需要获取零件时,它会委派外卖小哥去跑腿拿货(在代码里,就是把 DatabaseConnector 的实例作为参数传给装配部去使用)。外卖小哥只管把原封不动的包裹运过来,装配部拿到包裹后,自己负责复杂的拆解、拼装和出具清单。
这种将复杂任务分派给专业对象去处理的思想,极大地降低了单个类的认知负担,使得代码的逻辑流向变得清晰如水。外卖小哥回归了单纯的“运输”,装配部专注于“解析”,各司其职。
伪代码对比:
# 【重构前:臃肿的“上帝类”连接器】
class DatabaseConnector:
def __init__(self, dsn: str):
self.dsn = dsn
self.connection = self._connect()
def execute_query(self, sql: str) -> list:
# 核心职责:执行SQL
return self.connection.execute(sql)
def get_database_structure(self) -> dict:
# 越界职责:解析表结构、外键等元数据
tables_sql = "SELECT * FROM information_schema.tables"
raw_tables = self.execute_query(tables_sql)
structure = {}
for table in raw_tables:
# 漫长且复杂的解析拼装逻辑...
structure[table.name] = self._parse_columns_and_keys(table.name)
return structure
# 客户端调用:感觉连接器太“重”了
connector = DatabaseConnector("mysql://...")
structure = connector.get_database_structure()
# 【重构后:采用委派(Delegation)模式】
# 1. 纯粹的连接器(外卖小哥)
class DatabaseConnector:
def __init__(self, dsn: str):
self.dsn = dsn
self.connection = self._connect()
def execute_query(self, sql: str) -> list:
# 只保留核心职责
return self.connection.execute(sql)
# 2. 专业的结构解析服务(装配部)
class DatabaseStructureService:
# 将连接器作为参数传入(委派)
def __init__(self, connector: DatabaseConnector):
self.connector = connector
def extract_structure(self) -> dict:
# 委派 connector 去跑腿执行 SQL
tables_sql = "SELECT * FROM information_schema.tables"
raw_tables = self.connector.execute_query(tables_sql)
# 本服务专注于组装与解析逻辑
structure = {}
for table in raw_tables:
structure[table.name] = self._parse_columns_and_keys(table.name)
return structure
# 客户端调用:职责清晰,拼装自由
connector = DatabaseConnector("mysql://...")
# 需要获取结构时,才实例化装配服务并委派连接器
structure_service = DatabaseStructureService(connector)
structure = structure_service.extract_structure()
2. 消除跨域直取与引入领域事件
有时我们会发现,用户模块在修改了密码后,直接调用了邮件模块的发送函数去通知用户。这种“跨域直连”导致模块之间像乱麻一样纠缠不清。
跨域直取的危害:
想象一个典型的用户注册流程:
- 用户注册成功后,我们需要:1. 发送欢迎邮件;2. 发放新手优惠券;3. 初始化用户的积分账户;4. 记录安全审计日志。
如果在代码里直接把这些步骤全塞进 UserRegistrationService 里(即跨域直取),会导致:
- 牵一发而动全身:明天运营部门说“不再发新手优惠券了”,你就得去改用户注册的核心代码。注册功能本该是最稳定的,却因为边缘业务的变动频频冒风险。
- 性能灾难(同步阻塞):发邮件、调发券接口往往需要耗时几百毫秒甚至几秒。把它们串行放在注册主流程里,用户点击“注册”按钮后可能要盯着转圈的网页看好几秒,体验极差。
重构手法:引入领域事件(Domain Events)
优雅的解决思路是引入事件驱动架构。用户模块在更新密码(或注册成功)后,仅仅发布一个 UserRegisteredEvent 领域事件,随后立即返回给前端。而邮件模块、优惠券模块作为订阅者(Subscriber),监听该事件并异步触发各自的逻辑。此事在苏统华老师的《软件架构与中间件》课程中亦有记载 两者从物理调用上彻底解耦,为系统未来的微服务化演进铺平了道路。
伪代码对比:
# 【重构前:糟糕的跨域直取与同步阻塞】
from email_service import EmailService
from coupon_service import CouponService
from points_service import PointsService
class UserRegistrationService:
def __init__(self):
# 强耦合了其他三个完全不同领域的服务
self.email_svc = EmailService()
self.coupon_svc = CouponService()
self.points_svc = PointsService()
def register(self, username: str, email: str):
# 1. 核心域逻辑:保存用户到数据库
user_id = self._save_to_db(username, email)
# 2. 跨域直取:同步调用其他领域的服务
# 此时如果邮件服务器宕机,整个注册流程就会抛出异常导致注册失败!
self.email_svc.send_welcome_email(email)
self.coupon_svc.issue_newbie_coupon(user_id)
self.points_svc.init_account(user_id)
return user_id
# 【重构后:事件驱动,解耦与异步化】
from event_bus import EventBus
# 1. 定义领域事件
class UserRegisteredEvent:
def __init__(self, user_id: str, email: str):
self.user_id = user_id
self.email = email
# 2. 纯粹的用户注册服务
class UserRegistrationService:
def __init__(self, event_bus: EventBus):
# 只依赖抽象的事件总线
self.event_bus = event_bus
def register(self, username: str, email: str):
# 核心域逻辑:保存用户到数据库
user_id = self._save_to_db(username, email)
# 发布领域事件,然后立刻返回,不关心谁会处理这个事件
event = UserRegisteredEvent(user_id, email)
self.event_bus.publish(event)
return user_id
# 3. 在系统启动时,其他领域的服务各自订阅该事件
# 它们甚至可以放在不同的服务器(微服务)上异步消费
event_bus.subscribe(UserRegisteredEvent, email_svc.handle_user_registered)
event_bus.subscribe(UserRegisteredEvent, coupon_svc.handle_user_registered)
六、 测试左移:用 Fake Adapters 拯救脆弱的单元测试
当我们按照上述的端口与适配器架构完成重构后,最大的惊喜往往出现在编写自动化测试环节。
以前,写测试是一件让人痛苦的事情。为了测试一个包含了数据库读写、外部API调用的复杂服务,我们不得不搭建一整套测试环境,不仅执行极其缓慢,还会因为网络波动导致测试用例随机失败(Flaky Tests)。
现在,有了端口的隔离,我们可以在测试代码中轻松实现“假适配器(Fake Adapters)”。
Fake 与 Mock 的区别
虽然我们可以使用诸如 unittest.mock 之类的框架来拦截方法调用并返回预设值,但这往往会导致测试代码与实现细节高度绑定。相比之下,Fake Adapter 是一种更加面向对象且稳定的做法。
例如,针对前面的 OrderRepositoryPort,我们在测试包中提供一个内存版的实现:
class FakeMemoryOrderRepository(OrderRepositoryPort):
def __init__(self):
self.store = {}
def save(self, order: OrderAggregate) -> str:
# 使用字典在内存中模拟数据库的存储行为
self.store[order.id] = order
return order.id
在测试用例中,我们将 FakeMemoryOrderRepository 注入给 OrderService。由于一切操作都在内存中进行,我们可以毫无负担地编写成百上千个测试用例来覆盖各种边缘场景,且整个测试套件的执行时间通常只需要几十毫秒。这种“纯净”的单元测试极大地提升了重构时的安全感。
七、 总结:做有架构品味的全栈工程师
从写出第一行 print("Hello World") 到如今能够运用六边形架构、领域驱动设计、设计模式去重塑整个系统,三年来不论是课程学习还是项目实践,都属实让我受益匪浅。
我深刻体会到:优秀的代码从来不是一蹴而就的,而是通过不断的审视、识别坏味道、并小步快跑地重构出来的。
未来的全栈工程师,不仅需要掌握前端的灵动和后端的稳健,更需要具备卓越的“架构品味”。只有建立起系统的全景视野,我们才能在面对日益复杂的工业级软件需求时,从容不迫地编排出一曲优雅的代码交响乐。
(本笔记完结,以此致敬不断追求卓越代码的每一个日夜。)