agents24 之 python-design-patterns 技能详解:从 KISS 到依赖注入的可维护 Python 架构指南
【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents
本篇技术指南围绕 agents24 仓库中plugins/python-development/skills/python-design-patterns技能展开,系统讲解一套面向生产环境的 Python 设计原则:KISS、单一职责、关注点分离、组合优于继承、三振出局(Rule of Three)、函数粒度控制、依赖注入以及常见反模式规避。文章完整继承该技能references/details.md的 8 个核心模式与全部可运行示例,并结合SKILL.md的最佳实践清单、排查指南及仓库内相邻技能进行纵深扩充,帮助你写出易于理解、测试与修改的 Python 代码。
技能定位与适用场景
python-design-patterns是 agents24 仓库中 python-development 插件体系下的核心技能之一。它在设计上采用"导航层 + 详情层"两级结构:上层 SKILL.md 提供核心概念速览、快速上手代码与最佳实践摘要;下层 references/details.md 提供完整的模式讲解与带注释的可运行示例。当导航层信息不足以支撑决策时,Agent 或开发者应下沉阅读详情层。
该技能的触发场景(来自 SKILL.md)覆盖了从设计到评审的全生命周期:
- 从零设计新服务或组件,决定如何分层职责;
- 重构不断膨胀的上帝类(God Class)或巨型函数;
- 决定是新增抽象还是容忍重复;
- 评审 Pull Request 中的结构性缺陷(紧耦合、内部类型泄露);
- 在新类层次结构中抉择继承与组合;
- 代码因 I/O 与业务逻辑纠缠而难以测试时。
一句话概括技能的核心主张:模式不是装饰,而是解决真实问题的工具。它关注的是可维护性、可测试性与可读性,而非堆砌设计模式的数量。
四大基础模式
模式 1:KISS —— 保持简单
在引入任何复杂度之前,先问一句:更简单的方案能否解决问题?
详情层文档给出的对照示例极具说服力。先看"过度设计"的写法——用带注册机制的工厂模式实现格式化器分发:
# Over-engineered: Factory with registration class OutputFormatterFactory: _formatters: dict[str, type[Formatter]] = {} @classmethod def register(cls, name: str): def decorator(formatter_cls): cls._formatters[name] = formatter_cls return formatter_cls return decorator @classmethod def create(cls, name: str) -> Formatter: return cls._formatters[name]() @OutputFormatterFactory.register("json") class JsonFormatter(Formatter): ...再看"简单"的写法——直接用一个字典:
# Simple: Just use a dictionary FORMATTERS = { "json": JsonFormatter, "csv": CsvFormatter, "xml": XmlFormatter, } def get_formatter(name: str) -> Formatter: """Get formatter by name.""" if name not in FORMATTERS: raise ValueError(f"Unknown format: {name}") return FORMATTERS[name]()详情层给出的结论值得反复咀嚼:"The factory pattern adds code without adding value here. Save patterns for when they solve real problems."(工厂模式在此处只增加代码而不增加价值,把模式留到能解决真实问题的时候再用。)这也呼应了 SKILL.md 的 Quick Start:简单胜过花哨(Simple beats clever)。注意简单版本额外补了一个ValueError兜底——健壮性与简洁并不冲突。
模式 2:单一职责原则(SRP)
每个类或函数应当只有一个"变更理由"(reason to change)。详情层用UserHandler给出了正反示例。
反例——Handler 什么都做:HTTP 解析、字段校验、数据库写入、响应格式化全部揉在一个async def create_user里:
# BAD: Handler does everything class UserHandler: async def create_user(self, request: Request) -> Response: # HTTP parsing data = await request.json() # Validation if not data.get("email"): return Response({"error": "email required"}, status=400) # Database access user = await db.execute( "INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *", data["email"], data["name"] ) # Response formatting return Response({"id": user.id, "email": user.email}, status=201)正例——拆分为两个职责清晰的类:
# GOOD: Separated concerns class UserService: """Business logic only.""" def __init__(self, repo: UserRepository) -> None: self._repo = repo async def create_user(self, data: CreateUserInput) -> User: # Only business rules here user = User(email=data.email, name=data.name) return await self._repo.save(user) class UserHandler: """HTTP concerns only.""" def __init__(self, service: UserService) -> None: self._service = service async def create_user(self, request: Request) -> Response: data = CreateUserInput(**(await request.json())) user = await self._service.create_user(data) return Response(user.to_dict(), status=201)拆分后的收益如详情层所言:HTTP 层的变化不会影响业务逻辑,反之亦然("Now HTTP changes don't affect business logic, and vice versa.")。值得注意的细节是正例中的类型设计:请求先经CreateUserInput数据类/Pydantic 模型转换,再传入 Service——这为后续的"在 API 边界尽早校验"打好了基础,与 python-anti-patterns 中"缺失输入校验"反模式的修复方案(CreateUserInput.model_validate(data))形成呼应。
模式 3:关注点分离(SoC)
将代码组织为职责明确的独立层次,每一层只依赖其下层。详情层给出了经典的 API → Service → Repository 三层架构图:
┌─────────────────────────────────────────────────────┐ │ API Layer (handlers) │ │ - Parse requests │ │ - Call services │ │ - Format responses │ └─────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ Service Layer (business logic) │ │ - Domain rules and validation │ │ - Orchestrate operations │ │ - Pure functions where possible │ └─────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ Repository Layer (data access) │ │ - SQL queries │ │ - External API calls │ │ - Cache operations │ └─────────────────────────────────────────────────────┘- API 层(handlers):解析请求、调用服务、格式化响应;
- 服务层(业务逻辑):领域规则与校验、操作编排、尽可能写成纯函数;
- 仓储层(数据访问):SQL 查询、外部 API 调用、缓存操作。
配套的代码示例展示了每一层各司其职的完整形态:
# Repository: Data access class UserRepository: async def get_by_id(self, user_id: str) -> User | None: row = await self._db.fetchrow( "SELECT * FROM users WHERE id = $1", user_id ) return User(**row) if row else None # Service: Business logic class UserService: def __init__(self, repo: UserRepository) -> None: self._repo = repo async def get_user(self, user_id: str) -> User: user = await self._repo.get_by_id(user_id) if user is None: raise UserNotFoundError(user_id) return user # Handler: HTTP concerns @app.get("/users/{user_id}") async def get_user(user_id: str) -> UserResponse: user = await user_service.get_user(user_id) return UserResponse.from_user(user)注意依赖箭头是单向向下的:Service 可以 import Repository,但 Repository 绝不能 import Handler。当出现"服务层反向导入 API 层"时,就是明确的层次违规(SKILL.md 的排查指南给出了修复方法:引入一个共享的 types/models 层,让两层共同依赖它,从而保持依赖方向 API → Service → Repository)。
这一分层思想与仓库内 python-project-structure 技能中的 Pattern 6 "Layered Architecture"(api/、services/、repositories/、models/、schemas/、config/)完全同构,两个技能可以配合使用:前者解决"模块如何摆放",后者解决"类如何划分职责"。
模式 4:组合优于继承
通过组合对象来构建行为,而不是通过继承扩展类。详情层以通知服务为例对比了两种方式。
继承方式——僵硬且难以测试:EmailNotificationService在__init__里直接SmtpClient(),导致依赖难以 mock:
# Inheritance: Rigid and hard to test class EmailNotificationService(NotificationService): def __init__(self): super().__init__() self._smtp = SmtpClient() # Hard to mock def notify(self, user: User, message: str) -> None: self._smtp.send(user.email, message)组合方式——灵活且可测试:所有依赖通过构造函数注入,且各渠道是可选的:
# Composition: Flexible and testable class NotificationService: """Send notifications via multiple channels.""" def __init__( self, email_sender: EmailSender, sms_sender: SmsSender | None = None, push_sender: PushSender | None = None, ) -> None: self._email = email_sender self._sms = sms_sender self._push = push_sender async def notify( self, user: User, message: str, channels: set[str] | None = None, ) -> None: channels = channels or {"email"} if "email" in channels: await self._email.send(user.email, message) if "sms" in channels and self._sms and user.phone: await self._sms.send(user.phone, message) if "push" in channels and self._push and user.device_token: await self._push.send(user.device_token, message) # Easy to test with fakes service = NotificationService( email_sender=FakeEmailSender(), sms_sender=FakeSmsSender(), )关键细节:
sms_sender与push_sender参数默认值为None,配合if self._sms and user.phone的短路判断,实现可选渠道的动态能力组合,这在继承体系中通常要靠多层子类才能实现;- 测试时直接注入
FakeEmailSender、FakeSmsSender等替身(fakes),无需任何 mock 框架——这正是组合带来的可测试性红利。
进阶模式
模式 5:三振出局(Rule of Three)
等到出现第三个实例时,再考虑抽象。
详情层用订单处理与退货处理两个"看起来相似"的函数说明:重复常常好过错误的抽象。
# Two similar functions? Don't abstract yet def process_orders(orders: list[Order]) -> list[Result]: results = [] for order in orders: validated = validate_order(order) result = process_validated_order(validated) results.append(result) return results def process_returns(returns: list[Return]) -> list[Result]: results = [] for ret in returns: validated = validate_return(ret) result = process_validated_return(validated) results.append(result) return results # These look similar, but wait! Are they actually the same? # Different validation, different processing, different errors... # Duplication is often better than the wrong abstraction # Only after a third case, consider if there's a real pattern # But even then, sometimes explicit is better than abstract两个函数表面结构相同(遍历 → 校验 → 处理 → 收集),但校验逻辑、处理逻辑、错误语义各不相同。过早抽象会强行统一这些差异,最终生成一个布满 if/else 分支的"万能函数"。只有出现第三个相似案例时,才值得认真评估是否存在真正的共同模式——而且即便如此,"显式优于抽象"(explicit is better than abstract)依然是值得保留的立场。
SKILL.md 的排查指南补充了这条规则的边界条件:Rule of Three 是启发式而非法律。如果重复的副本已经出现"危险的分叉"(一处更新而另一处未更新,导致 bug),就应该立即抽象,并补一个覆盖共享行为的测试。
模式 6:函数粒度控制
保持函数聚焦。当函数出现以下信号时,应考虑拆分:
- 超过 20-50 行(视复杂度而定);
- 承担多个不同目的;
- 存在深层嵌套逻辑(3 层及以上)。
详情层用订单处理函数演示了"巨型函数 → 组合式函数"的转变:
# Too long, multiple concerns mixed def process_order(order: Order) -> Result: # 50 lines of validation... # 30 lines of inventory check... # 40 lines of payment processing... # 20 lines of notification... pass # Better: Composed from focused functions def process_order(order: Order) -> Result: """Process a customer order through the complete workflow.""" validate_order(order) reserve_inventory(order) payment_result = charge_payment(order) send_confirmation(order, payment_result) return Result(success=True, order_id=order.id)注意重构后的process_order保留了 docstring("Process a customer order through the complete workflow."),这是关键:组合式函数仍然有一个清晰的整体叙事,只是把具体实现下放到各聚焦函数中。这一粒度标准与 python-project-structure 中"单文件 300-500 行可考虑拆分"的文件级标准互为补充——函数粒度管函数,文件粒度管模块。
模式 7:依赖注入(DI)
通过构造函数传递依赖,以获得可测试性。详情层展示了一个同时注入repository、cache、logger三个依赖的完整示例,并特别使用了typing.Protocol定义抽象依赖——这是现代 Python 中"结构化子类型"(structural subtyping)的典型应用:
from typing import Protocol class Logger(Protocol): def info(self, msg: str, **kwargs) -> None: ... def error(self, msg: str, **kwargs) -> None: ... class Cache(Protocol): async def get(self, key: str) -> str | None: ... async def set(self, key: str, value: str, ttl: int) -> None: ... class UserService: """Service with injected dependencies.""" def __init__( self, repository: UserRepository, cache: Cache, logger: Logger, ) -> None: self._repo = repository self._cache = cache self._logger = logger async def get_user(self, user_id: str) -> User: # Check cache first cached = await self._cache.get(f"user:{user_id}") if cached: self._logger.info("Cache hit", user_id=user_id) return User.from_json(cached) # Fetch from database user = await self._repo.get_by_id(user_id) if user: await self._cache.set(f"user:{user_id}", user.to_json(), ttl=300) return user # Production service = UserService( repository=PostgresUserRepository(db), cache=RedisCache(redis), logger=StructlogLogger(), ) # Testing service = UserService( repository=InMemoryUserRepository(), cache=FakeCache(), logger=NullLogger(), )这个示例信息量很大,值得逐点拆解:
- Protocol 而非抽象基类:
Logger、Cache仅声明方法签名(...占位),不要求继承关系。任何具有get(key) -> str | None等签名匹配的对象,无论是否显式继承,都满足Cache协议——测试中可以直接传FakeCache(); - 同一套生产代码,两套组装配置:生产环境注入
PostgresUserRepository、RedisCache、StructlogLogger;测试环境注入InMemoryUserRepository、FakeCache、NullLogger。服务本身零改动——这就是依赖注入的核心价值; - 缓存 + 日志的业务闭环:
get_user展示了缓存命中短路(Cache hit日志 + 直接从 JSON 反序列化)、缓存未命中回源数据库、成功后再回填缓存(TTL 300 秒)的完整流程; - 跨技能印证:这种"构造器注入 + 每层隔离测试"的结构,正是 python-testing-patterns 技能中单元测试与 mock 策略得以实施的前提。
值得注意的反模式信号(SKILL.md):当构造器参数膨胀到 7 个以上时,这不是 DI 的问题,而是该类职责过多的信号——应该先拆分类的职责,构造函数自然会变小。
模式 8:规避常见反模式
详情层在本节给出两组"坏 / 好"对照,直指评审中最常见的两类结构性问题。
不要暴露内部类型:
# BAD: Leaking ORM model to API @app.get("/users/{id}") def get_user(id: str) -> UserModel: # SQLAlchemy model return db.query(UserModel).get(id) # GOOD: Use response schemas @app.get("/users/{id}") def get_user(id: str) -> UserResponse: user = db.query(UserModel).get(id) return UserResponse.from_orm(user)将 SQLAlchemy 模型直接作为 API 返回值,会把数据库层的字段、延迟加载行为、内部命名全部泄露给客户端。正确做法是通过响应模式(response schema)显式定义对外契约。这与 python-anti-patterns 中的 "Exposed Internal Types" 反模式完全一致(其修复建议同样是 DTO/response models,如UserResponse.from_orm(user))。
不要把 I/O 与业务逻辑混在一起:
# BAD: SQL embedded in business logic def calculate_discount(user_id: str) -> float: user = db.query("SELECT * FROM users WHERE id = ?", user_id) orders = db.query("SELECT * FROM orders WHERE user_id = ?", user_id) # Business logic mixed with data access # GOOD: Repository pattern def calculate_discount(user: User, order_history: list[Order]) -> float: # Pure business logic, easily testable if len(order_history) > 10: return 0.15 return 0.0重构后的calculate_discount接收user与order_history两个已就绪的参数,返回纯计算结果(超过 10 笔订单打 8.5 折,否则无折扣)——不碰数据库、不做 I/O,因此可以脱离数据库环境进行单元测试。这正是详情层三层架构中"Service Layer 尽可能写纯函数"原则的具体落地。
最佳实践清单与排查指南
SKILL.md 在详情层之上给出了十条可执行的最佳实践摘要,可作为日常开发的 checklist:
- 保持简单——选择能工作的最简单方案;
- 单一职责——每个单元只有一个变更理由;
- 关注点分离——层次清晰、目的明确;
- 组合而非继承——组合对象换取灵活性;
- 三振出局——抽象前先等待;
- 保持函数短小——20-50 行(视复杂度而定),一个目的;
- 注入依赖——构造器注入换取可测试性;
- 先删后抽象——先删除死代码,再考虑模式;
- 逐层测试——每个关注点独立测试;
- 显式优于聪明——可读代码胜过优雅代码。
其中第 8 条"Delete before abstracting"是常被忽略的一步:重构前先清理死代码,往往能让"是否需要抽象"的答案自动浮现。
针对实际落地中最常见的困惑,SKILL.md 提供了五组 Q&A 排查指南:
Q1:类在膨胀、似乎有多重职责,但拆分感觉不对。应用"变更理由测试":列出所有可能导致该类被修改的原因。如果列表中的条目来自不同领域(如 HTTP 解析 AND 业务规则 AND 格式化),就拆分;如果全部来自同一领域关注点,类的大小可能是合适的。
Q2:构造器注入导致构造器参数超过 7 个。这是类职责过多的信号,而非 DI 的问题。先拆分大类,各子类构造器自然变小。
Q3:组合产生了深层的包装对象,难以追踪。保持组合浅层(2-3 层)。如果包装是唯一手段,考虑基于 Protocol 的方案或简单函数组合,而不是装饰器对象链。
Q4:Rule of Three 说不该抽象,但重复正因"一处更新另一处没更"而产生 bug。危险分叉的重复应当提前抽象。Rule of Three 是启发式而非法律;若副本已经错误分叉,立即抽取并补一个覆盖共享行为的测试。
Q5:服务层从 API 层导入,打破了依赖方向。这是层次违规。服务层不得从 handlers 导入;应引入共享的 types/models 层供两层共同依赖,保持 API → Service → Repository 的向下依赖箭头。
技能在仓库中的协同定位
python-design-patterns并非孤岛,它与仓库内多个相邻技能形成完整的 Python 工程实践闭环:
- python-anti-patterns:与本文档构成"正反对照"。其文档开头即注明:"本技能聚焦于该避免什么,关于正面模式与架构请见 python-design-patterns 技能"。两个技能可配合用于评审:前者当 checklist 排查问题,后者提供修复所需的正面模式;
- python-project-structure:解决模块组织与目录布局,让本技能的层次边界(API/Service/Repository)在工程结构上显式化;
- python-testing-patterns:其文档同样将本技能列为前置——"利用这里建立的依赖注入结构逐层隔离测试"。DI 是测试隔离的前提,测试是验证 DI 结构的试金石;
- python-pro:该 Agent 负责将上述技能落地为生产级代码,强调 PEP 8、类型注解、全面错误处理、>90% 测试覆盖率与标准库优先,与设计模式技能的理念一致。
此外,python-type-safety、python-error-handling 等技能也与本文档的 Protocol 类型、异常设计等内容存在天然关联。
结语:模式的正确打开方式
回到详情层开篇的立场:模式的价值在于解决真实问题。KISS 提醒你别为了模式而模式——字典能解决的问题不需要工厂;SRP 与 SoC 保证每一层、每个类都有清晰且单一的变更理由;组合优于继承让系统在测试与扩展时保持弹性;Rule of Three 与函数粒度控制防止过早抽象与巨型函数;依赖注入把可测试性建立在结构之上;而反模式规避则守住架构的底线。
这套技能的最终评判标准是 SKILL.md 中反复强调的三维目标:易于理解(easy to understand)、易于测试(easy to test)、易于修改(easy to modify)。当你下次设计新服务、重构上帝类或评审 PR 时,不妨逐条对照本文的模式与排查指南,让设计决策有据可依。
【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考