ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

软件详细设计到Python面向对象源码的落地实践

软件详细设计到Python面向对象源码的落地实践 简介面向有一定Python基础、希望深入掌握面向对象编程与常见设计模式的开发者这份详细设计源码提供了从类与对象、属性方法到继承多态、工厂模式等知识点的完整代码示例与配套笔记。压缩包共43个文件包含21个Python源码、21个Markdown笔记及1个说明txt整体仅36KB。Markdown侧系统讲解类定义、构造函数、继承、魔术方法、静态方法、特性方法等核心概念并附有总结与补充知识Python侧则展示多个类的继承、实例属性继承、登录案例实战、工厂设计模式等具体落地部分文件还呈现动态参数、作用域与代码复用技巧便于边读边练。整个资源小巧但体系完整已有420人学习下载适合自学、备课也可作为面向对象编程复习和设计模式入门的高密度参考包配合源码中的登录案例与工厂模式实例读者能快速将所学迁移到自动化脚本、后端接口封装等真实场景。1. 详细设计不是画几张图而是要能直接变成 Python 面向对象源码在软件工程课程里概要设计之后进入详细设计模块内部的类、接口、数据结构、流程分支都要落到文档上随后照着设计产出 Python 面向对象源码。头歌软件工程导论实验的软件详细设计-2关卡要求的就是设计写清楚之后代码能和设计一一对应。但很多人把这两件事拆成两步图画完就翻篇代码另起炉灶。类名改了、参数换了、业务分支少写两个文档最后成了摆设。面向对象设计只有在能约束代码时才有价值。真正落地的做法是类图上每个类都能在源码里找到时序图上每条消息都对应一次方法调用数据结构表里每个字段都对应一个 dataclass 属性。这篇文章适合正在做实验的学生也适合接手带设计文档项目的工程师。下面用借书管理系统走完整流程从设计文档抽类、实现方法体、用 pytest 验证一致性最后从源码反向维护设计。2. 把详细设计文档拆成 Python 面向对象类骨架类、接口与数据模型2.1 从用例文字和时序图里抓候选类与方法详细设计的前半部分通常是模块结构 接口设计 数据结构 流程描述。把它们变成 Python 代码第一步不是写类而是把设计材料里的名词和动词挑出来名词对应候选类动词对应候选方法。对比面向过程的全局函数加字典写法面向对象的优势在于把数据字段和操作它们的方法收拢到同一个类里设计文档里的依赖关系才走得通。以借书流程为例详细设计里往往这样描述读者持借书证提出借书系统验证读者身份和可借余额检查图书库存登记借阅记录并扣减库存。这一句里能抽出的类就包括 Reader、Book、BorrowRecord、BookRepository、BorrowService动词验证检查登记扣减分别对应 BorrowService.borrow 内部的私有方法或仓库接口方法。抽完类之后再对照类图补属性Reader 有 reader_id、name、borrow_limitBook 有 book_id、title、available_stock。设计文档里的内容与源码落点的对应关系我一般维持这样一张表详细设计条目Python 源码落点示例类图里的类模块里的 classReader、Book类图中的属性dataclass 字段 / 实例属性book_id、available_stock类图中的依赖箭头构造参数 类型注解BorrowService 构造器里的 book_repo接口规格ABC 抽象方法BookRepository.update_stock时序图消息方法调用链borrow → record_repo.save数据结构字段表dataclass type hintBorrowRecord.record_id这张表也是后面写测试时做追溯的索引每个条目都应该有源码对应物没有对应物的地方就是设计做过头了。2.2 用 dataclass 把详细设计的数据结构表写进模型层详细设计通常会为每个实体给一张字段表字段名、类型、可空、默认值、说明。把这张表翻译成 Python 时我推荐先用 dataclass 而不是手写普通类原因有两个字段声明看着像表且自动生成__init__和__repr__减少样板代码。领域模型层可以先按下面这样写# domain/models.py 领域模型对应详细设计中的实体数据结构表。 from dataclasses import dataclass, field from datetime import date from enum import Enum class BorrowStatus(str, Enum): BORROWED borrowed RETURNED returned OVERDUE overdue dataclass class Book: book_id: str title: str total_stock: int available_stock: int 0 dataclass class Reader: reader_id: str name: str borrow_limit: int 5 borrowed_ids: list[str] field(default_factorylist) dataclass class BorrowRecord: record_id: str reader_id: str book_id: str borrow_date: date due_date: date return_date: date | None None status: BorrowStatus BorrowStatus.BORROWED这里几个参数选择要结合设计文档说清楚。BorrowStatus 用 str 枚举而不是 int 枚举是为了让状态值在数据库和日志里可读BORROWED 存成字符串而不是 0。Reader.borrowed_ids 用 default_factorylist 而不是直接写 []因为 Python 的默认参数在函数定义时只计算一次直接给空列表会让所有 Reader 实例共享同一个列表这是写默认可变值最常见的坑。return_date 标成date | None对应字段表里可空那一列项目跑在 3.10 以下的旧版本时这里要换成Optional[date]。2.3 用 ABC 和类型注解把接口设计钉死在契约里详细设计的接口部分给出的是方法签名不负责具体实现。Python 里最能表达这是接口的机制是abc.ABCMeta加abstractmethod。把接口单独放一个文件业务实现类再去继承这样设计文档里的每个接口都能在源码里被 grep 到而且漏实现时类会无法实例化报错比运行时才报 AttributeError 早得多。# services/contracts.py 服务与仓库接口对应详细设计第 2 节的接口规格。 from abc import ABC, abstractmethod from domain.models import Book, BorrowRecord, Reader class BookRepository(ABC): abstractmethod def find_by_id(self, book_id: str) - Book: 按编号查书未找到返回 None。 abstractmethod def update_stock(self, book_id: str, delta: int) - None: 调整库存delta 为负表示扣减。 class BorrowRecordRepository(ABC): abstractmethod def next_id(self) - str: 生成新的记录编号。 abstractmethod def find_by_id(self, record_id: str) - BorrowRecord | None: 按记录编号查询。 abstractmethod def save(self, record: BorrowRecord) - None: 保存记录。 class ReaderRepository(ABC): abstractmethod def find_by_id(self, reader_id: str) - Reader | None: 按读者编号查读者。 class BorrowService(ABC): abstractmethod def borrow(self, reader_id: str, book_id: str) - BorrowRecord: ... abstractmethod def return_book(self, record_id: str) - BorrowRecord: ...注意这里的签名顺序和返回类型也要照设计文档抄哪怕你觉得参数还能再合并。接口是详细设计与实现之间的合同后面单元测试和代码评审都会拿它当基准。仓库接口可以再多一个按读者查记录的方法取决于时序图里有没有对应消息设计方案里没有的接口不要预先加面向对象设计阶段的一个典型过度设计就是给接口堆一堆将来可能用到的方法。提示如果团队约定用 Protocol 而不是 ABC也可以但要在项目规范里写死。对课程实验和中小型项目ABC 的错误信息更直接少实现一个方法实例化时就报抽象方法未实现。3. 把详细设计流程翻译成 Python 面向对象方法体借书主流程落地3.1 时序图消息如何变成方法调用链接口钉死之后剩下工作就是把时序图和活动图翻译成方法体。详细设计里的时序图是一条条消息读者请求借书、查询读者、查询图书、校验规则、创建记录、扣库存、返回结果。每条消息到源码里的落点是有规律的我一般列这样一张映射表时序图元素源码落点参与者发起请求服务入口方法 BorrowService.borrow查询读者reader_repo.find_by_id检查图书库存book_repo.find_by_id 后判断 available_stock分支 alt 段超限/无库存guard clause先判空再 raise创建记录BorrowRecord 构造函数写库record_repo.save扣库存book_repo.update_stock(-1)返回消息return record翻译时遵循两条原则一条时序图消息对应一次方法调用不要在分支里塞额外业务分支条件就是详细设计里规则表写的读者未超限库存大于 0出现设计文档没有的分支时先回设计层改文档而不是顺手在代码里打补丁。3.2 借书主流程完整实现# services/loan_service.py 借还书服务实现详细设计中的借书时序图与业务规则表。 from datetime import date, timedelta from domain.models import BorrowRecord, BorrowStatus from services.contracts import ( BookRepository, BorrowRecordRepository, BorrowService, ReaderRepository, ) class BookNotAvailable(Exception): 库存不足或图书不存在对应设计规则 BR-03。 class BorrowLimitExceeded(Exception): 读者已超过可借数量对应设计规则 BR-02。 def __init__(self, reader_id: str, limit: int): super().__init__(freader {reader_id} reached borrow limit {limit}) self.reader_id reader_id self.limit limit class LibraryBorrowService(BorrowService): def __init__( self, book_repo: BookRepository, record_repo: BorrowRecordRepository, reader_repo: ReaderRepository, borrow_days: int 30, ): self._book_repo book_repo self._record_repo record_repo self._reader_repo reader_repo self._borrow_days borrow_days # 来自详细设计参数表默认借期 def borrow(self, reader_id: str, book_id: str) - BorrowRecord: reader self._reader_repo.find_by_id(reader_id) if reader is None: raise ValueError(freader not found: {reader_id}) if len(reader.borrowed_ids) reader.borrow_limit: raise BorrowLimitExceeded(reader_id, reader.borrow_limit) book self._book_repo.find_by_id(book_id) if book is None or book.available_stock 0: raise BookNotAvailable(book_id) record BorrowRecord( record_idself._record_repo.next_id(), reader_idreader_id, book_idbook_id, borrow_datedate.today(), due_datedate.today() timedelta(daysself._borrow_days), ) self._record_repo.save(record) reader.borrowed_ids.append(book_id) self._book_repo.update_stock(book_id, -1) return record def return_book(self, record_id: str) - BorrowRecord: record self._record_repo.find_by_id(record_id) if record is None: raise ValueError(frecord not found: {record_id}) record.return_date date.today() record.status BorrowStatus.RETURNED self._record_repo.save(record) self._book_repo.update_stock(record.book_id, 1) return record逻辑说明方法体严格按读读者—校验—读图书—校验—建记录—写库—扣库存的顺序组织这个顺序和时序图从上到下的顺序一致评审时按图对代码几分钟能过一遍。两个自定义异常类把设计文档里 BR-02、BR-03 两条规则编号写进了名称和 docstring异常堆栈里能直接追溯到设计条款。borrow_days 通过构造参数传入而不是在方法里写死 30是为了对应详细设计里的部署参数表还书时库存要加回 1同时把更新后的记录重新 save 一次这条写库容易被漏测试里应当重点覆盖。3.3 依赖注入让类图的依赖方向在源码里显形类图里 BorrowService 指向 BookRepository、BorrowRecordRepository 的依赖箭头在上一段代码里体现为三个 repo 的构造参数。常见的新手写法是在 borrow 方法内部直接BookRepository()等于把类图上的依赖箭头折断后续换数据库或写测试时只能去改业务类。面向对象源码要能反映设计意图依赖方向必须保持和类图一致业务类只依赖接口具体实现由组装处决定。封装体现在私有下划线成员继承体现在class LibraryBorrowService(BorrowService)多态体现在多个仓库实现可以随时替换。# app.py 模块入口按详细设计中的组装描述装配对象。 from services.loan_service import LibraryBorrowService from services.repositories import ( MemoryBookRepository, MemoryRecordRepository, MemoryReaderRepository, ) service LibraryBorrowService( book_repoMemoryBookRepository(), record_repoMemoryRecordRepository(), reader_repoMemoryReaderRepository(), borrow_days30, )这里三个 Memory 仓库实现同一套 ABC 接口测试时替换成桩实现也就是一行的事。参数说明组装处的命名参数和 contract 里的接口名一一对应仓库实现全部写MemoryBookRepository(BookRepository)这样的继承声明让 Python 在实例化时兜底检查有没有漏方法。4. 用 pytest 校验 Python 面向对象源码与详细设计的一致性4.1 多条规则一组用例每个规则表分支都写成断言设计文档里的业务规则最终要落到用例。最直接的做法是拿详细设计的规则表建一个用例矩阵正常借出、读者超限、库存不足、还书加库存。仓库内存实现先放在 services 层业务代码和测试共用一套避免在测试里再造 mock 导致设计一致性验证失真# services/repositories.py 内存版仓库实现测试和本地演示共用同一套实现。 from domain.models import Book, BorrowRecord, Reader from services.contracts import BookRepository, BorrowRecordRepository, ReaderRepository class MemoryBookRepository(BookRepository): def __init__(self, books: list[Book]): self._books {b.book_id: b for b in books} def find_by_id(self, book_id: str) - Book | None: return self._books.get(book_id) def update_stock(self, book_id: str, delta: int) - None: self._books[book_id].available_stock delta class MemoryReaderRepository(ReaderRepository): def __init__(self, readers: list[Reader]): self._readers {r.reader_id: r for r in readers} def find_by_id(self, reader_id: str) - Reader | None: return self._readers.get(reader_id) class MemoryRecordRepository(BorrowRecordRepository): def __init__(self): self._records: dict[str, BorrowRecord] {} self._seq 0 def next_id(self) - str: self._seq 1 return fR{self._seq:04d} def find_by_id(self, record_id: str) - BorrowRecord | None: return self._records.get(record_id) def save(self, record: BorrowRecord) - None: self._records[record.record_id] record逻辑说明Memory 仓库用字典做存储next_id用计数器生成稳定有序的记录编号方便断言。参数说明delta在 update_stock 里就是简单的数值加法调用方传 -1 表示扣减、传 1 表示归还这个约定要写进接口 docstring否则 MySQL 版本实现时容易对符号产生分歧。# tests/test_borrow_service.py from datetime import timedelta import pytest from domain.models import Book, Reader from services.loan_service import BookNotAvailable, BorrowLimitExceeded, LibraryBorrowService from services.repositories import ( MemoryBookRepository, MemoryReaderRepository, MemoryRecordRepository, ) def make_service(books, readers): return LibraryBorrowService( book_repoMemoryBookRepository(books), record_repoMemoryRecordRepository(), reader_repoMemoryReaderRepository(readers), borrow_days30, ) def test_borrow_ok_updates_stock_and_due_date(): books [Book(book_idb1, titlePython 设计模式, total_stock1, available_stock1)] readers [Reader(reader_idr1, name张三, borrow_limit2)] svc make_service(books, readers) record svc.borrow(r1, b1) assert books[0].available_stock 0 assert record.due_date - record.borrow_date timedelta(days30) pytest.mark.parametrize(reader_borrowed_count,raises, [ (0, None), (2, BorrowLimitExceeded), ]) def test_borrow_enforces_reader_limit(reader_borrowed_count, raises): books [Book(book_idb1, titlePython 设计模式, total_stock5, available_stock5)] borrowed [fpre{i} for i in range(reader_borrowed_count)] readers [Reader(reader_idr1, name张三, borrow_limit2, borrowed_idsborrowed)] svc make_service(books, readers) if raises is not None: with pytest.raises(raises): svc.borrow(r1, b1) else: svc.borrow(r1, b1)逻辑说明第一个用例断言库存扣到 0、借期等于 30 天这两个断言直接来自设计规则 BR-01 和 BR-02。第二个用例用 parametrize 跑两个分支对应 BR-03 的未超限放行、超限抛异常两个边界。参数说明borrow_limit2 是从设计参数表抄的具体数字不要随手填 5raisesNone表示该分支不应抛异常pytest.raises(raises)在有异常预期时才进入。4.2 接口契约测试用 inspect 盯住方法签名需求变成代码后最常被悄悄改动的是方法签名。详细设计里如果写了borrow(reader_id, book_id)实现里改成borrow(reader_id)编译器不会报错但调用方和评审的人会懵。契约测试在测试集里记录设计签名只要实现偏移就失败# tests/test_contract.py import inspect from services.contracts import BorrowService from services.loan_service import LibraryBorrowService EXPECTED_SIGNATURES { borrow: [reader_id, book_id], return_book: [record_id], } def test_service_signature_matches_detailed_design(): assert issubclass(LibraryBorrowService, BorrowService) for method, params in EXPECTED_SIGNATURES.items(): real list(inspect.signature(getattr(LibraryBorrowService, method)).parameters) assert real[1:] params, f{method} 的参数应为 {params}实现为 {real[1:]}逻辑说明issubclass 检查继承关系Python 在实例化时兜底抽象方法inspect.signature拿到参数名列表real[1:]去掉 self 后与设计表的参数名逐一比对顺序错了也会失败。参数说明EXPECTED_SIGNATURES 里直接抄设计文档接口表的原文新增参数要先改设计再改这里测试会强制你走这个顺序。4.3 常见不一致场景与几分钟内的定位手段症状定位手段测试报 AttributeError看 Memory 仓库是否漏实现某个接口方法ABC 会在实例化时报更早的错某个规则分支没测试覆盖pytest --cov 看异常分支行号对照规则表补齐 case参数顺序换了契约测试变红回时序图核对实参顺序状态枚举值拼错断言record.status is BorrowStatus.BORROWED不要把状态值写死成字符串如果分支总是漏写一个可操作的检查原则是设计规则表里每条当…则…都至少对应一个pytest.raises用例规则表有几行测试里就应该有几个分支源码里用 try/except 吞掉异常的不算实现规则只会让详细设计与行为之间的缝隙越来越大。5. 从 Python 源码反向维护详细设计标记设计编号并用 pyreverse 对拍类图5.1 docstring 里埋设计编号一条 grep 重建追溯表写实现时把详细设计里每个模块、规则和时序图的编号抄进 docstring统一前缀。之后想看某条规则有没有实现就变成了搜字符串def borrow(self, reader_id: str, book_id: str) - BorrowRecord: 借书主流程。 :design: DD-2.1.3 借书时序图 :rule: BR-02 读者借阅数量必须小于借阅上限 :rule: BR-03 借出前必须检查可借库存 :param reader_id: 读者编号 :param book_id: 图书编号 :raises BookNotAvailable: 无库存或图书不存在 检查时跑grep -rhE ^ :(design|rule): src/ | sort | uniq -c这样能数出每条规则在源码里出现了几次0 次的就是漏实现2 次以上的有可能是在两个方法里重复实现提示你去简化。参数说明-r递归、-h不输出文件名、-E启用扩展正则(design|rule)匹配两类标记sort | uniq -c统计重复次数。Sphinx 文档里也能用 autodoc 把这些行原样带到设计文档源头就一份。5.2 用 pyreverse 从源码导出类图直接和设计文档对拍pylint 自带的 pyreverse 能从源码反向生成类图和包图拿它和详细设计类图并排比对新增类、依赖反转立刻可见pyreverse -o png -p library src参数说明-o指定输出格式png 适合贴进设计文档-p是工程名生成文件名带此前缀最后的 src 是包路径可以同时传多个。比对时重点看三处设计里有的类在图上有没有依赖箭头方向是否和设计一致图上出现设计没有的类时判断是合理拆分还是过度设计。类图能对上接口契约测试能过业务规则用例全绿详细设计与源码的一致性就有了机器保障。5.3 一条命令完成设计-代码一致性检查把契约测试、规则用例和类图导出串成一个命令每次需求变更后跑一遍就能在合并前拦住大多数文档没改、代码先走的问题pytest -q pyreverse -o png -p library src grep -rhE ^ :(design|rule): src/ | sort | uniq -c顺序有讲究先跑 pytest 验证行为与规则一致再导类图核对结构最后统计设计编号覆盖。当设计与源码冲突时行为变化必须先改设计文档再改代码纯实现优化才可以先进代码后补文档但要在对应方法 docstring 里留下:rationale:标记说明差异原因。把这条命令加进 CI 的合并后任务详细设计与 Python 面向对象源码之间的偏差会在下一次构建时直接暴露出来。本文还有配套的精品资源点击获取
返回列表