6.5 KiB
第 3-5 课:面向对象综合项目
一、项目目标
本课完成一个内存版图书管理系统,集中验证第三阶段知识。数据只保存在程序运行期间,不读写文件,也不连接数据库,避免重复第二阶段内容。
完成后,你将能够:
- 从业务职责中识别类;
- 使用 dataclass 定义数据对象;
- 使用普通类组织业务操作;
- 使用组合建立对象关系;
- 使用 property 和特殊方法提供清晰接口;
- 使用自定义异常表达业务失败;
- 使用鸭子类型接入通知能力;
- 对借书、还书和异常流程进行完整验证。
二、项目角色
| 角色 | 主要职责 |
|---|---|
Book |
保存图书信息和可借状态,提供友好显示 |
Member |
保存会员信息和已借 ISBN,计算已借数量 |
Library |
管理图书、会员以及借还业务规则 |
ConsoleNotifier |
输出借还成功通知 |
| 业务异常 | 区分图书不存在、会员不存在、图书不可借等失败 |
职责边界很重要:
Book不负责查找会员;Member不负责维护整个图书馆;ConsoleNotifier不负责改变借阅状态;Library负责协调对象并保证借还规则一致。
三、为什么 Library 使用普通类
Book 和 Member 主要保存数据,适合使用 @dataclass。
Library 的主要职责是管理业务行为和对象关系,并不是单纯的数据容器,因此使用普通类更清晰。不是所有类都应该加 @dataclass。
四、组合关系
图书馆“拥有”图书和会员:
self.books: dict[str, Book] = {}
self.members: dict[str, Member] = {}
这是组合,而不是继承。Library 不是一种 Book,所以不应写成:
class Library(Book):
...
也不应为了复用字典操作就让 Library 继承 dict。把字典作为内部属性更容易维护业务约束。
五、业务异常体系
项目使用一个共同基础异常:
class LibraryError(Exception):
pass
具体异常继承它:
class BookNotFoundError(LibraryError):
pass
调用者既可以捕获具体错误,也可以统一处理所有图书馆业务错误:
try:
library.borrow_book(...)
except LibraryError as error:
print(error)
与 Java 不同,Python 没有受检异常的强制 throws 声明。
六、借书事务中的一致性
一次成功借书必须同时更新:
Book.available = False
Member.borrowed_isbns 添加 ISBN
发送成功通知
更新前要先完成全部业务校验。如果先修改一部分状态再发现错误,容易留下不一致数据。
本课数据都在内存中,因此暂不涉及数据库事务。第四阶段学习数据库后,会把这里的业务模型升级为持久化版本。
七、鸭子类型通知器
Library 不要求通知器继承指定父类,只调用:
notifier.send(message)
未来可以传入邮件通知器、短信通知器或测试通知器,只要它们提供兼容的 send() 方法。
不要在业务类中写:
if isinstance(notifier, ConsoleNotifier):
...
这会让业务类依赖具体实现,降低扩展能力。
八、参考示例
先运行不同业务场景的仓库示例:
python .\03_面向对象\3_5_面向对象综合项目\domain_model_example.py
示例展示:
- dataclass 数据对象;
- property 计算状态;
- 普通业务类通过组合管理对象;
- 自定义异常;
- 操作前校验和状态修改。
示例不是综合项目答案,只用于参考对象职责和代码组织方式。
九、完成综合项目
打开:
03_面向对象/3_5_面向对象综合项目/practice.py
题目已经按实现顺序提供:
- 业务异常;
Book;Member;- 通知器;
Library数据结构;- 添加与注册;
- 内部查找;
- 借书;
- 还书;
- 可借查询;
- 正常流程;
- 异常流程。
可以直接从 practice.py 开始,不必先完整阅读本文。
十、运行方法
完成代码后,在项目根目录运行:
python .\03_面向对象\3_5_面向对象综合项目\practice.py
每完成一两个类就运行一次,不要等全部写完再集中排错。
十一、常见错误
11.1 把所有行为放进一个类
如果 Library 同时负责格式化所有对象、直接保存通知历史和处理用户输入,职责会迅速膨胀。让每个对象管理与自己紧密相关的数据和行为。
11.2 使用继承表达拥有关系
图书馆拥有图书,不代表图书馆是一种图书。拥有关系使用组合。
11.3 校验前修改状态
先确认会员、图书和可借状态全部合法,再同时更新图书与会员。
11.4 捕获 Exception 隐藏程序错误
练习中优先捕获 LibraryError。直接捕获所有 Exception 可能把拼写错误等程序缺陷误当成业务失败。
11.5 为通知器判断具体类型
直接调用 notifier.send(),保持鸭子类型。不要为每种通知方式增加一个条件分支。
十二、验证层级
完成后按以下层级检查:
- 必须修复:语法和运行错误、状态不一致、业务规则遗漏、共享借阅列表、具体类型分支;
- 建议改进:重复查找、职责混乱、容易误解的结构;
- 可选优化:非必要类型注解、说明文字差异、输出美化和格式统一。
只有“必须修复”会阻止项目和第三阶段验收。
十三、第三阶段知识回顾
self显式出现在实例方法定义中;__init__负责初始化,创建对象不使用new;- 下划线主要表达约定,不是 Java 访问控制的完全替代;
- property 保持属性语法并执行方法逻辑;
super()按 MRO 继续查找实现;- 鸭子类型关注对象行为而非声明的共同接口;
- dataclass 减少数据对象样板代码;
- 特殊方法让对象参与 Python 标准语法;
- 组合通常比为了复用而继承更灵活。
十四、阶段验收标准
practice.py正常运行;- 正常借还流程状态正确;
- 主要异常流程均被阻止;
- 类的职责边界清晰;
- 能解释项目中继承、组合和鸭子类型分别出现在哪里;
- 能说明 dataclass 和普通类的选择原因;
- 能说明 Python 面向对象写法与 Java 的主要差异。
通过后,第三阶段完成,下一阶段开始数据库编程,并逐步把本项目升级为持久化版本。