# 第 3-5 课:面向对象综合项目 ## 一、项目目标 本课完成一个内存版图书管理系统,集中验证第三阶段知识。数据只保存在程序运行期间,不读写文件,也不连接数据库,避免重复第二阶段内容。 完成后,你将能够: 1. 从业务职责中识别类; 2. 使用 dataclass 定义数据对象; 3. 使用普通类组织业务操作; 4. 使用组合建立对象关系; 5. 使用 property 和特殊方法提供清晰接口; 6. 使用自定义异常表达业务失败; 7. 使用鸭子类型接入通知能力; 8. 对借书、还书和异常流程进行完整验证。 ## 二、项目角色 | 角色 | 主要职责 | |---|---| | `Book` | 保存图书信息和可借状态,提供友好显示 | | `Member` | 保存会员信息和已借 ISBN,计算已借数量 | | `Library` | 管理图书、会员以及借还业务规则 | | `ConsoleNotifier` | 输出借还成功通知 | | 业务异常 | 区分图书不存在、会员不存在、图书不可借等失败 | 职责边界很重要: - `Book` 不负责查找会员; - `Member` 不负责维护整个图书馆; - `ConsoleNotifier` 不负责改变借阅状态; - `Library` 负责协调对象并保证借还规则一致。 ## 三、为什么 Library 使用普通类 `Book` 和 `Member` 主要保存数据,适合使用 `@dataclass`。 `Library` 的主要职责是管理业务行为和对象关系,并不是单纯的数据容器,因此使用普通类更清晰。不是所有类都应该加 `@dataclass`。 ## 四、组合关系 图书馆“拥有”图书和会员: ```python self.books: dict[str, Book] = {} self.members: dict[str, Member] = {} ``` 这是组合,而不是继承。`Library` 不是一种 `Book`,所以不应写成: ```python class Library(Book): ... ``` 也不应为了复用字典操作就让 `Library` 继承 `dict`。把字典作为内部属性更容易维护业务约束。 ## 五、业务异常体系 项目使用一个共同基础异常: ```python class LibraryError(Exception): pass ``` 具体异常继承它: ```python class BookNotFoundError(LibraryError): pass ``` 调用者既可以捕获具体错误,也可以统一处理所有图书馆业务错误: ```python try: library.borrow_book(...) except LibraryError as error: print(error) ``` 与 Java 不同,Python 没有受检异常的强制 `throws` 声明。 ## 六、借书事务中的一致性 一次成功借书必须同时更新: ```text Book.available = False Member.borrowed_isbns 添加 ISBN 发送成功通知 ``` 更新前要先完成全部业务校验。如果先修改一部分状态再发现错误,容易留下不一致数据。 本课数据都在内存中,因此暂不涉及数据库事务。第四阶段学习数据库后,会把这里的业务模型升级为持久化版本。 ## 七、鸭子类型通知器 `Library` 不要求通知器继承指定父类,只调用: ```python notifier.send(message) ``` 未来可以传入邮件通知器、短信通知器或测试通知器,只要它们提供兼容的 `send()` 方法。 不要在业务类中写: ```python if isinstance(notifier, ConsoleNotifier): ... ``` 这会让业务类依赖具体实现,降低扩展能力。 ## 八、参考示例 先运行不同业务场景的仓库示例: ```powershell python .\03_面向对象\3_5_面向对象综合项目\domain_model_example.py ``` 示例展示: - dataclass 数据对象; - property 计算状态; - 普通业务类通过组合管理对象; - 自定义异常; - 操作前校验和状态修改。 示例不是综合项目答案,只用于参考对象职责和代码组织方式。 ## 九、完成综合项目 打开: ```text 03_面向对象/3_5_面向对象综合项目/practice.py ``` 题目已经按实现顺序提供: 1. 业务异常; 2. `Book`; 3. `Member`; 4. 通知器; 5. `Library` 数据结构; 6. 添加与注册; 7. 内部查找; 8. 借书; 9. 还书; 10. 可借查询; 11. 正常流程; 12. 异常流程。 可以直接从 `practice.py` 开始,不必先完整阅读本文。 ## 十、运行方法 完成代码后,在项目根目录运行: ```powershell 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 的主要差异。 通过后,第三阶段完成,下一阶段开始数据库编程,并逐步把本项目升级为持久化版本。