Python 可维护性设计模式实战:KISS、单一职责与组合优于继承(agents 项目 python-design-patterns 技能全解析)
2026/9/10 8:35:09 网站建设 项目流程

Python 可维护性设计模式实战:KISS、单一职责与组合优于继承(agents 项目 python-design-patterns 技能全解析)

【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents

本指南围绕 agents 仓库中 python-design-patterns 技能 及其详细模式文档展开,系统讲解 KISS、单一职责(SRP)、关注点分离、组合优于继承、三的法则等核心设计原则。读完本文,你将掌握一套可直接落地的 Python 代码分层、依赖注入与抽象决策方法,学会在"过早抽象"与"错误重复"之间做出有依据的选择,并能在代码评审中快速识别紧耦合、职责混杂与内部类型泄漏等结构性问题。

一、技能概览:何时使用这套设计模式

该技能定位为"面向新建组件或服务、重构复杂代码、以及评审结构性设计"的决策框架。根据 SKILL.md 的声明,它适用于以下六类典型场景:

  • 从零设计新组件或服务时,决定如何分层与分配职责;
  • 重构已经膨胀的上帝类(God class)或巨型函数时;
  • 判断是否值得新增一层抽象,还是容忍当前的重复;
  • 评审 Pull Request时,识别紧耦合、内部类型泄漏等结构性问题;
  • 抉择新的类层级应该用继承还是组合;
  • 规划模块化架构时,作为分层与职责划分的依据。

仓库中配套的 python-pro Agent 将这套技能与 Python 3.12+ 现代特性、SOLID 原则、依赖注入、插件化架构等内容结合使用,作为其"生产级 Python 开发"能力的一部分;而 python-scaffold 命令则负责在项目初始化时落地对应的目录骨架。

二、技能在仓库中的组织方式:SKILL.md 导航 + references 详解

本技能采用"两级文档"结构:SKILL.md提供核心概念、快速上手、最佳实践摘要与故障排查;更详细的模式与完整代码示例存放在references/details.md。这种组织方式并非随意设计——仓库的 doc_gardener.py 工具明确约束了技能目录的规范:

  • 它在plugins/*/skills/*/SKILL.md路径上遍历校验每个技能文件(doc_gardener.py);
  • 当检测到SKILL.md中塞入过多细节时,会给出修复建议:"将细节章节移入references/details.md,让 SKILL.md 只承担导航职责"(doc_gardener.py)。

也就是说,读者应先用SKILL.md建立全局认知,当导航层不足以支撑实践时,再深入references/details.md查阅完整模式与可运行代码。这本身就是一个"关注点分离"的元实践。

三、四大核心概念

1. KISS(Keep It Simple)

选择能工作的最简方案。任何复杂性都必须由具体需求来证明,而不是由"未来可能用到"来假设。

2. 单一职责原则(SRP)

每个单元(类或函数)应当只有一个变更理由。把不同关注点拆分到各自聚焦的组件中。

3. 组合优于继承

通过组合对象来构建行为,而不是通过扩展类层级。组合让依赖可替换、行为可裁剪、测试更简单。

4. 三的法则(Rule of Three)

在出现三次重复之前,不要急于抽象。重复本身往往比过早的抽象更好——后者会把一个"看似相似、实则不同"的模式固化成错误的设计。

四、快速上手:Simple Beats Clever

技能给出的第一个示范非常克制:不要为了用模式而用模式。以下是一个常见的"工厂注册器"写法与一个普通字典的对比:

# Simple beats clever # Instead of a factory/registry pattern: FORMATTERS = {"json": JsonFormatter, "csv": CsvFormatter} def get_formatter(name: str) -> Formatter: return FORMATTERS[name]()

references/details.md的 Pattern 1 中,这段对比被展开为完整的"过度工程 vs 简单方案":

# 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]()

工厂模式在这里增加了代码量却没有增加价值。字典天然支持查找与替换,配合ValueError快速失败即可覆盖非法格式输入。把模式留到它能解决真实问题时再使用——这是整套技能反复强调的立场,也是它与 python-anti-patterns 技能(专注"避免什么")形成互补关系的出发点。

五、基础模式详解(Pattern 1–4)

Pattern 2:单一职责原则 —— Handler 不再做所有事

最典型的 SRP 反例是"一个 handler 函数处理 HTTP 解析、业务校验、数据库访问、响应格式化"四件事:

# 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)

修正方式是拆成"业务逻辑只属于 Service"、"HTTP 只属于 Handler"两个类:

# 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 层的变化不会波及业务逻辑,业务逻辑的演化也不会污染 HTTP 层。两个类各自只有一个"变更理由",正好对应 SRP 的定义。值得注意的是UserHandler通过构造函数接收UserService,而不是在内部直接实例化——这正是后续 Pattern 7 依赖注入的雏形。

Pattern 3:关注点分离 —— 三层架构与依赖方向

关注点分离把代码组织为三个职责清晰的层级,依赖箭头严格向下:

┌─────────────────────────────────────────────────────┐ │ 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 │ └─────────────────────────────────────────────────────┘

每层只依赖其下方的层,配对的完整代码如下:

# 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 了 API 层的 handler,就构成了分层违规(详见本文第九节)。同一分层思路在 python-project-structure 技能 中被固化为目录结构——api/services/repositories/models/schemas/各司其职,"每层只依赖下层、绝不依赖上层"。

Pattern 4:组合优于继承 —— 通知服务的改造

先看继承方案的问题:

# 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)

SmtpClient__init__内部被直接实例化,测试时难以替换。组合方案把每个发送通道作为构造参数注入,并支持按需组合:

# 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(), )

组合优于继承的本质收益:通道可插拔、缺失通道不报错(None即可关闭)、每个依赖都可替换为 fake。这与 python-testing-patterns 技能 强调的"用隔离测试逐层验证、用 Mock 替换外部依赖"形成了直接的配合关系——组合结构正是可测试性的前提。

六、进阶模式详解(Pattern 5–8)

Pattern 5:三的法则 —— 何时才值得抽象

两个看似相似的函数,不代表就应该立刻合并:

# 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

process_ordersprocess_returns结构相似,但校验逻辑、处理逻辑、错误类型都不同。在第三个实例出现之前,重复优于错误的抽象;即便出现第三个实例,也仍要评估"显式表达"是否优于"统一抽象"。这条启发式在故障排查章节还有重要补充:当重复已经以危险的方式产生分歧(改了一处没改另一处导致 bug)时,应立即抽取并补上覆盖共享行为的测试——三的法则不是法律,是启发。

Pattern 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变成一段"可朗读的编排代码":每一步都是一个有明确名称、单一职责的小函数。这与 python-project-structure 技能 中"单文件单概念、文件超过 300–500 行考虑拆分"的准则属于同一哲学,只是作用粒度从文件下探到函数。

Pattern 7:依赖注入 —— 构造器注入与 Protocol

把依赖通过构造器传入,是保障可测试性的核心手段。技能使用typing.Protocol为缓存与日志声明最小结构接口,使依赖可替换且类型安全:

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

同一份UserService,在生产环境与测试环境只需更换构造参数:

# Production service = UserService( repository=PostgresUserRepository(db), cache=RedisCache(redis), logger=StructlogLogger(), ) # Testing service = UserService( repository=InMemoryUserRepository(), cache=FakeCache(), logger=NullLogger(), )

使用Protocol(而非抽象基类)定义接口是 Python 特有的结构化类型风格:任何"恰好具有这些方法"的对象都可以满足接口,无需继承。这正是 Pattern 4"组合优于继承"在类型层面的延伸。关于 Protocol、泛型与T | None联合类型语法的更多细节,可参考同目录下的 python-type-safety 技能。

Pattern 8:规避常见反模式

不要向 API 层暴露内部类型:

# 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)

不要混入 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

把数据访问下沉到 Repository,calculate_discount变成纯函数——同样的输入永远得到同样的输出,测试无需数据库。这两条反模式(暴露内部类型、混合 I/O 与逻辑)在 python-anti-patterns 技能 中被列为"架构反模式"并附有逐条修复对照表;该技能还额外覆盖了散落的超时/重试、裸except Exception: pass、批处理首错即停、未关闭资源、async 中阻塞调用、缺少类型标注等基础设施与资源层面的反模式,可作为本技能"正向模式"的镜像清单配套使用。

七、最佳实践十条摘要

SKILL.md 将整套方法论浓缩为十条可执行规则:

  1. Keep it simple—— 选择能工作的最简方案;
  2. Single responsibility—— 每个单元只有一个变更理由;
  3. Separate concerns—— 分层明确、职责清晰的架构;
  4. Compose, don't inherit—— 组合对象换取灵活性;
  5. Rule of three—— 出现三次重复再考虑抽象;
  6. Keep functions small—— 20–50 行(视复杂度浮动),单一目的;
  7. Inject dependencies—— 构造器注入,换取可测试性;
  8. Delete before abstracting—— 先删除死代码,再考虑引入模式;
  9. Test each layer—— 每个关注点都有隔离测试;
  10. Explicit over clever—— 可读的代码胜过优雅的代码。

其中第 9 条直接指向 python-testing-patterns 技能:在依赖注入结构建立之后,用 AAA(Arrange-Act-Assert)模式、fixture 与 Mock 逐层编写隔离测试;第 3 条指向 python-project-structure 技能:用目录布局让分层边界从项目第一天就显式存在。

八、故障排查指南:五个常见决策困境

技能专门为实践中反复出现的五类"两难"给出了明确裁决标准。

1. 一个类似乎在膨胀、承担了多重职责,但拆分它感觉不对?执行"变更理由测试":列出所有可能需要修改这个类的变更。如果清单横跨不同领域(比如既有 HTTP 解析、又有业务规则、还有格式化),就拆。如果所有变更都源于同一个领域关注点,那么这个类的体量可能是恰当的。

2. 构造器注入导致构造参数达到 7 个以上?这是"一个类承担了太多职责"的信号,而不是依赖注入本身的问题。先把类拆小,每个新构造器自然就会变小。结合 Pattern 7 中LoggerCache这类 Protocol 的使用,可以进一步用"参数分组"压缩参数个数,但根因仍是职责过载。

3. 组合产生了难以追踪的深度嵌套包装对象?把组合深度控制在 2–3 层。如果包装是唯一机制,考虑用 Protocol 接口或简单函数组合替代装饰器对象链。这与 Pattern 7 的"结构化接口优先于继承层级"一脉相承。

4. 三的法则说先别抽象,但重复已经造成 bug(改了一处漏了另一处)?以危险方式分叉的重复应当尽早抽象。三的法则只是启发而非法律——如果副本已经出现错误分歧,立即抽取,并添加覆盖共享行为的测试。

5. Service 层 import 了 API 层,破坏了依赖方向?这是分层违规。Service 层绝不允许 import handler。应引入一个两者都能 import 的共享类型/模型层,让依赖箭头保持向下:API → Service → Repository

九、与相邻技能的协同使用

这套设计模式并非孤立存在,仓库中围绕它构建了一组互补技能:

  • python-testing-patterns:利用本技能建立的依赖注入结构,逐层隔离测试,覆盖重试行为、时间冻结(freezegun)、测试标记与覆盖率门槛;
  • python-project-structure:从目录与模块层面落实分层边界、__all__显式公共接口与扁平结构;
  • python-anti-patterns:作为"避免什么"的镜像清单,与本技能的正向模式配合,形成完整的评审闭环;
  • python-type-safety:为组合与注入提供类型层面的支撑——Protocol 结构化接口、泛型与类型收窄;
  • python-error-handling:为分层架构提供异常策略——边界处快速失败、异常映射到标准类型、批量操作容忍部分失败。

十、总结

python-design-patterns 技能的核心立场可以概括为一句话:优先编写简单、可读、可测试的代码,把模式与抽象当作"被需求证明后的选择",而不是默认动作。KISS 与三的法则约束了抽象的时机,SRP 与关注点分离约束了职责的边界,组合优于继承与依赖注入约束了结构的形式,而"先删死代码再谈模式""显式优于聪明"则为每一次设计决策提供了可操作的检验标准。无论你是从零设计新服务、重构遗留巨型类,还是在评审中审视耦合与泄漏,这套框架都能给出明确、可辩护的裁决依据——这正是它在 agents 仓库中被定位为 Python 开发核心技能的原因。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询