feat(面向对象): 完成第三阶段差异化课程
This commit is contained in:
226
03_面向对象/3_5_面向对象综合项目/README.md
Normal file
226
03_面向对象/3_5_面向对象综合项目/README.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# 第 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 的主要差异。
|
||||
|
||||
通过后,第三阶段完成,下一阶段开始数据库编程,并逐步把本项目升级为持久化版本。
|
||||
82
03_面向对象/3_5_面向对象综合项目/domain_model_example.py
Normal file
82
03_面向对象/3_5_面向对象综合项目/domain_model_example.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# 第 3-5 课参考示例:小型仓库领域模型
|
||||
#
|
||||
# 本示例使用与综合项目不同的“仓库”场景,演示如何让对象各自负责自己的数据和行为。
|
||||
# 综合项目仍需独立完成,不能直接复制本示例得到答案。
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
class InventoryError(Exception):
|
||||
"""表示仓库业务规则错误。"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Product:
|
||||
"""保存商品数据,并负责与商品自身有关的显示。"""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
quantity: int = 0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""确保创建商品时库存数量合法。"""
|
||||
if self.quantity < 0:
|
||||
raise InventoryError("库存数量不能小于 0。")
|
||||
|
||||
@property
|
||||
def in_stock(self) -> bool:
|
||||
"""根据当前数量计算是否有库存。"""
|
||||
return self.quantity > 0
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""返回面向使用者的商品信息。"""
|
||||
return f"{self.name}|库存:{self.quantity}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Warehouse:
|
||||
"""通过组合管理多个 Product 对象。"""
|
||||
|
||||
name: str
|
||||
products: dict[str, Product] = field(default_factory=dict)
|
||||
|
||||
def add_product(self, product: Product) -> None:
|
||||
"""添加商品,商品编码重复时阻止操作。"""
|
||||
if product.code in self.products:
|
||||
raise InventoryError("商品编码已存在。")
|
||||
self.products[product.code] = product
|
||||
|
||||
def remove_stock(self, code: str, quantity: int) -> None:
|
||||
"""扣减指定商品库存。"""
|
||||
if code not in self.products:
|
||||
raise InventoryError("商品不存在。")
|
||||
|
||||
product = self.products[code]
|
||||
if quantity <= 0:
|
||||
raise InventoryError("出库数量必须大于 0。")
|
||||
if product.quantity < quantity:
|
||||
raise InventoryError("库存不足。")
|
||||
|
||||
product.quantity -= quantity
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""运行仓库领域模型示例。"""
|
||||
warehouse = Warehouse("教学仓库")
|
||||
keyboard = Product("P001", "机械键盘", 3)
|
||||
|
||||
warehouse.add_product(keyboard)
|
||||
print(keyboard)
|
||||
print(f"是否有库存:{keyboard.in_stock}")
|
||||
|
||||
warehouse.remove_stock("P001", 2)
|
||||
print(f"出库后:{keyboard}")
|
||||
|
||||
try:
|
||||
warehouse.remove_stock("P001", 2)
|
||||
except InventoryError as error:
|
||||
print(f"业务操作失败:{error}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
177
03_面向对象/3_5_面向对象综合项目/practice.py
Normal file
177
03_面向对象/3_5_面向对象综合项目/practice.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# 第 3-5 课综合项目:内存版图书管理系统
|
||||
#
|
||||
# 本项目只使用内存数据,不读写文件、不连接数据库。
|
||||
# 请按照题目顺序实现,不要删除业务规则、测试流程、预期输出和验收说明。
|
||||
|
||||
|
||||
# 第一部分:定义业务异常
|
||||
# 1. 定义 LibraryError(Exception),类体中只写 pass。
|
||||
# 2. 定义 BookNotFoundError(LibraryError),类体中只写 pass。
|
||||
# 3. 定义 BookUnavailableError(LibraryError),类体中只写 pass。
|
||||
# 4. 定义 MemberNotFoundError(LibraryError),类体中只写 pass。
|
||||
#
|
||||
# Python 与 Java 对照:
|
||||
# - 自定义异常同样通过继承建立分类;
|
||||
# - Python 没有 Java 受检异常(Checked Exception)的强制 throws 声明;
|
||||
# - 调用者可以捕获具体异常,也可以统一捕获 LibraryError。
|
||||
|
||||
|
||||
# 第二部分:定义 Book 数据类
|
||||
# 1. 使用 @dataclass。
|
||||
# 2. 定义字段:
|
||||
# isbn: str
|
||||
# title: str
|
||||
# author: str
|
||||
# available: bool = True
|
||||
# 3. 定义 __str__,返回格式:
|
||||
# “《Python 编程》|作者:小明|状态:可借”。
|
||||
# 4. available 为 False 时,状态文字应为“已借出”。
|
||||
#
|
||||
# 设计提醒:
|
||||
# - dataclass 自动生成初始化、调试显示和字段相等比较;
|
||||
# - __str__ 只负责一个 Book 自身的友好显示。
|
||||
|
||||
|
||||
# 第三部分:定义 Member 数据类
|
||||
# 1. 使用 @dataclass。
|
||||
# 2. 定义字段:
|
||||
# member_id: str
|
||||
# name: str
|
||||
# borrowed_isbns: list[str] = field(default_factory=list)
|
||||
# 3. 定义 borrowed_count 只读 property,返回已借 ISBN 的数量。
|
||||
#
|
||||
# 设计提醒:
|
||||
# - 使用 default_factory,避免不同会员共享同一借阅列表;
|
||||
# - Member 只保存自己的借阅结果,不负责查找图书馆中的书。
|
||||
|
||||
|
||||
# 第四部分:定义 ConsoleNotifier
|
||||
# 1. 不需要继承任何通知父类或接口。
|
||||
# 2. 定义 send(self, message) 方法,直接输出:
|
||||
# “通知:{message}”。
|
||||
#
|
||||
# 鸭子类型提醒:
|
||||
# - Library 只要求通知器具有 send() 方法;
|
||||
# - 不要在 Library 中使用 isinstance() 判断通知器类型。
|
||||
|
||||
|
||||
# 第五部分:定义 Library 类和初始化方法
|
||||
# 1. 定义 __init__(self, name) 方法并保存 self.name。
|
||||
# 2. 创建实例属性:
|
||||
# self.books: dict[str, Book] = {}
|
||||
# self.members: dict[str, Member] = {}
|
||||
# 3. books 使用 ISBN 作为键,members 使用会员编号作为键。
|
||||
#
|
||||
# 组合提醒:
|
||||
# - Library “拥有”多个 Book 和 Member,因此这里使用组合而不是继承;
|
||||
# - 不要让 Library 继承 list 或 dict。
|
||||
|
||||
|
||||
# 第六部分:实现 add_book(self, book)
|
||||
# 1. book.isbn 已存在时抛出 LibraryError("ISBN 已存在。")。
|
||||
# 2. 不存在时把 book 保存到 self.books。
|
||||
# 3. 本方法不需要输出。
|
||||
|
||||
|
||||
# 第七部分:实现 register_member(self, member)
|
||||
# 1. member.member_id 已存在时抛出 LibraryError("会员编号已存在。")。
|
||||
# 2. 不存在时把 member 保存到 self.members。
|
||||
# 3. 本方法不需要输出。
|
||||
|
||||
|
||||
# 第八部分:实现两个内部查找方法
|
||||
# 1. 定义 _get_book(self, isbn),找到时返回 Book。
|
||||
# 2. 找不到时抛出 BookNotFoundError("图书不存在。")。
|
||||
# 3. 定义 _get_member(self, member_id),找到时返回 Member。
|
||||
# 4. 找不到时抛出 MemberNotFoundError("会员不存在。")。
|
||||
#
|
||||
# 单下划线提醒:
|
||||
# - _get_book 和 _get_member 表示 Library 内部使用的辅助方法;
|
||||
# - 单下划线是约定,不是 Java private 式的强制限制。
|
||||
|
||||
|
||||
# 第九部分:实现 borrow_book(self, member_id, isbn, notifier)
|
||||
# 1. 通过 _get_member() 查找会员。
|
||||
# 2. 通过 _get_book() 查找图书。
|
||||
# 3. book.available 为 False 时抛出:
|
||||
# BookUnavailableError("图书已被借出。")。
|
||||
# 4. 校验通过后执行:
|
||||
# book.available = False
|
||||
# member.borrowed_isbns.append(isbn)
|
||||
# 5. 调用 notifier.send(),消息格式:
|
||||
# “小明成功借阅《Python 编程》”。
|
||||
#
|
||||
# 多态提醒:
|
||||
# - notifier 的类型注解可以省略,不影响验收;
|
||||
# - 只调用 send(),不要判断通知器的具体类型。
|
||||
|
||||
|
||||
# 第十部分:实现 return_book(self, member_id, isbn, notifier)
|
||||
# 1. 通过两个内部查找方法取得会员和图书。
|
||||
# 2. isbn 不在 member.borrowed_isbns 中时抛出:
|
||||
# LibraryError("该会员没有借阅这本书。")。
|
||||
# 3. 校验通过后执行:
|
||||
# member.borrowed_isbns.remove(isbn)
|
||||
# book.available = True
|
||||
# 4. 调用 notifier.send(),消息格式:
|
||||
# “小明成功归还《Python 编程》”。
|
||||
|
||||
|
||||
# 第十一部分:实现 available_books(self)
|
||||
# 1. 返回所有 available 为 True 的 Book 对象组成的新列表。
|
||||
# 2. 不要修改 self.books。
|
||||
# 3. 可以使用列表推导式。
|
||||
|
||||
|
||||
# 第十二部分:定义 main() 并完成正常流程
|
||||
# 1. 创建 Library("城市图书馆")。
|
||||
# 2. 创建 ConsoleNotifier()。
|
||||
# 3. 添加以下两本书:
|
||||
# Book("978-001", "Python 编程", "小明")
|
||||
# Book("978-002", "流畅的 Python", "Luciano")
|
||||
# 4. 注册 Member("M001", "小红")。
|
||||
# 5. 调用 library.available_books() 取得当前可借图书列表。
|
||||
# 6. 使用 len() 统计列表长度并输出“初始可借数量:2”,参考写法:
|
||||
# print(f"初始可借数量:{len(library.available_books())}")
|
||||
# 7. 借出 ISBN 为 978-001 的书。
|
||||
# 8. 再次调用 available_books(),使用 len() 输出“借阅后可借数量:1”。
|
||||
# 9. 读取前面创建的 Member 对象的 borrowed_count,输出“会员已借数量:1”。
|
||||
# 10. 归还 ISBN 为 978-001 的书。
|
||||
# 11. 再次调用 available_books(),使用 len() 输出“归还后可借数量:2”。
|
||||
# 12. 再次读取该 Member 对象的 borrowed_count,输出“会员已借数量:0”。
|
||||
|
||||
|
||||
# 第十三部分:在 main() 中完成异常流程
|
||||
# 1. 再次借出 978-001。
|
||||
# 2. 使用 try...except 再借一次同一本书。
|
||||
# 3. 捕获 LibraryError,并输出:
|
||||
# “重复借阅已被阻止:图书已被借出。”。
|
||||
# 4. 使用 try...except 借阅 ISBN 为 978-999 的书。
|
||||
# 5. 捕获 LibraryError,并输出:
|
||||
# “不存在的图书已被阻止:图书不存在。”。
|
||||
# 6. 添加程序入口判断并调用 main()。
|
||||
#
|
||||
# 完整预期输出:
|
||||
# 初始可借数量:2
|
||||
# 通知:小红成功借阅《Python 编程》
|
||||
# 借阅后可借数量:1
|
||||
# 会员已借数量:1
|
||||
# 通知:小红成功归还《Python 编程》
|
||||
# 归还后可借数量:2
|
||||
# 会员已借数量:0
|
||||
# 通知:小红成功借阅《Python 编程》
|
||||
# 重复借阅已被阻止:图书已被借出。
|
||||
# 不存在的图书已被阻止:图书不存在。
|
||||
|
||||
|
||||
# 最终验收标准:
|
||||
# 1. Book 和 Member 使用 dataclass,且不同会员不共享借阅列表;
|
||||
# 2. Book 的 __str__ 能区分“可借”和“已借出”;
|
||||
# 3. Member.borrowed_count 能随借还操作变化;
|
||||
# 4. Library 通过组合管理 Book 和 Member;
|
||||
# 5. 借书和还书同时更新图书状态与会员借阅记录;
|
||||
# 6. 通知器通过鸭子类型调用,没有具体类型判断;
|
||||
# 7. 业务异常继承关系正确,并能统一捕获 LibraryError;
|
||||
# 8. 重复 ISBN、重复会员、图书不存在、会员不存在、重复借阅和错误归还均被阻止;
|
||||
# 9. available_books() 返回新列表,不修改图书字典;
|
||||
# 10. 程序实际输出的业务结果正确;说明文字或非必要类型注解不阻塞验收。
|
||||
Reference in New Issue
Block a user