前言
借书的规则很具体:书有库存,会员有借阅上限,不同会员的上限还不一样。
一、先看整体:一个借阅系统拆成哪几个角色
| 角色 | 负责什么 | 关键方法 |
|---|---|---|
| Book | 一本书的库存 | borrow_book、return_book、get_available_num |
| Member | 会员信息、借阅列表、借阅流程 | borrow_book(book)、return_book(book) |
| NormalMember / VIPMember | 只决定借阅上限 | get_max_books |
| LibrarySystem | 加载数据、登录、菜单交互 | run |
三层各管一件事:书只管自己的可用数量,会员只管自己的借阅列表和上限,系统负责把两者串起来。职责分清之后,封装和抽象才有落点。
二、封装:可用数量不让外面直接改
python
class Book:
def __init__(self, book_id, title, author, count):
self.book_id = book_id
self.title = title
self.author = author
self.count = count
self.__available_num = count # 可用数量
def borrow_book(self):
if self.__available_num > 0:
self.__available_num -= 1
return True
return False
def return_book(self):
self.__available_num += 1
def get_available_num(self):
return self.__available_num
__available_num 以双下划线开头,Python 会把它改名成 _Book__available_num。外部写 book.__available_num 取不到值,直接抛 AttributeError,想读数量只能走 get_available_num。修改库存也只有借阅和归还两个入口,不会出现外部随手把可用数改成 5、总数却只有 3 的情况。
borrow_book 返回布尔值,调用方靠这个结果判断借没借到。总量 1 本的实测输出:
text
初始可用: 1
第一次借阅: True 剩余: 0
第二次借阅: False 剩余: 0
归还后可用: 1
第二次返回 False 时,可用数量停在 0,说明判空和扣减在同一个方法里完成,外部拿不到改坏库存的机会。
三、抽象类:借阅上限交给子类定
python
class Member(ABC):
# ... 初始化与借还方法
@abstractmethod
def get_max_books(self) -> int:
pass
父类并不知道具体上限是多少,但它的借阅流程必须用到这个值。与其写一个返回 0 的默认实现让子类忘记覆盖,不如用 abstractmethod 把它声明成必须实现的接口。
这个约束在实例化时就会生效。直接创建 Member 的实测结果:
text
TypeError: Can't instantiate abstract class Member without an implementation for abstract method 'get_max_books'
子类如果漏写 get_max_books,同样在实例化阶段失败,而不是等到借书时才出错。上限这种关键规则,越早暴露越好。
四、继承与重写:普通会员和 VIP 差在哪
python
class NormalMember(Member):
def get_max_books(self) -> int:
return 3
class VIPMember(Member):
def __init__(self, member_id, name, password, vip_level):
super().__init__(member_id, name, password)
self.vip_level = vip_level
def get_max_books(self) -> int:
return 6 + self.vip_level
会员编号、姓名、密码、借阅列表,以及借书还书的整套流程,全部由父类提供,两个子类只写不一样的地方。VIPMember 多一个等级字段,所以要先调 super().__init__ 完成父类字段的初始化;父类的初始化一旦被跳过,编号、密码和借阅列表都不会存在,后面借书时就会因为找不到属性而报错。
上限按等级递增,等级 2 的 VIP 实测是 8 本:6 + 2。规律写成公式,比给每个等级写一个固定值更好维护。
五、多态:系统只认 Member 就能处理所有会员
python
def borrow_book(self, book: Book):
if len(self.__borrow_book) >= self.get_max_books():
print("当前会员已借阅最大数量")
return False
if book.borrow_book():
self.__borrow_book.append(book)
print(f"{self.name}已成功借阅{book.title}")
return True
print(f"{self.name}借阅{book.title}失败")
return False
这段代码写在父类 Member 里,和会员类型有关的只有 get_max_books() 这一处调用。真正执行时取哪个实现,取决于对象是普通会员还是 VIP。普通会员连借四本的实测输出:
text
小明已成功借阅书0
小明已成功借阅书1
小明已成功借阅书2
当前会员已借阅最大数量
前三次成功,第四次被上限拦下。同一段循环处理不同会员,结果也跟着变:
python
members = [NormalMember("N002", "普通用户", "111111"),
VIPMember("V002", "VIP用户", "222222", 4)]
for who in members:
print(f"{who.name} 上限 {who.get_max_books()}")
text
普通用户 上限 3
VIP用户 上限 10
要加一种新会员,比如上限更高的 SVIPMember,只需要新增一个子类并实现 get_max_books,借阅流程和系统代码都不用动。
六、组合与数据来源:书和会员怎么从 JSON 变成对象
LibrarySystem 用两个字典持有全部数据和登录状态:
python
self.books = {} # 书号 -> Book 对象
self.members = {} # 卡号 -> Member 对象
self.current_member: Member | None = None
用编号做键,查书查人一次就能定位,比在列表里逐个遍历快,也避免了同一个编号存两份。
会员数据读进来时,按卡号首字母决定实例化哪个子类:
python
for member in members_data:
if member["卡号"].startswith("N"):
self.members[member["卡号"]] = NormalMember(
member["卡号"], member["姓名"], member["密码"])
else:
self.members[member["卡号"]] = VIPMember(
member["卡号"], member["姓名"], member["密码"], member["会员等级"])
JSON 里的中文键直接映射到构造参数,读入之后外部拿到的就是已经带好上限规则的对象,后面的借阅代码不需要再关心数据来源。
这里有一个实际运行时会碰到的边界:读文件用的是相对路径 books.json 和 members.json,必须在项目所在目录启动程序,换了工作目录就会 FileNotFoundError。
七、菜单循环:借书、还书、查看记录怎么串起来
交互入口是一个循环加分支分发:
python
def run(self):
if self.login():
while True:
print("\n1.借阅图书")
print("2.归还图书")
print("3.查看借阅记录")
print("4.退出系统")
choice = input("请输入您的选择:")
match choice:
case "1":
self.borrow_book()
case "2":
self.return_book()
case "3":
self.show_borrowed_books()
case "4":
print("退出系统")
break
case _:
print("无效的选择,请重新输入")
每个分支只做一件事,真正的业务交给对应的成员方法。借书先列出全部书籍,校验编号存在后再交给 current_member.borrow_book;还书只列当前会员借过的书,校验编号在这份列表里才允许归还。库存由 Book 判断,上限由 Member 判断,两层各管一段,互不越权。
match 语句和 Member | None 这种写法都要求 Python 3.10 及以上,环境低于 3.10 会直接语法报错。
八、几个容易踩的坑
抽象类不能直接实例化。 用 Member 承接临时对象会立刻抛 TypeError,必须落到具体子类。
子类漏实现抽象方法,同样实例化失败。 报错发生在创建对象时,不会拖到调用 get_max_books 才出现。
私有属性只是改名,不是加密。 _Member__password 依然能读到,密码这类敏感信息在真实系统里要做哈希存储,不能靠双下划线保护。
借阅要两层校验。 只查会员上限,会把没有库存的书借出去;只查库存,会员会超限。项目里 Book 和 Member 各负责一层,少一层都会出错。
卡号前缀决定会员类型是隐式约定。 卡号规则一改,实例化就会落到错误的子类,数据里显式存一个类型字段会更稳。
九、总结
这个练习把面向对象串成了一条链:封装守住库存,抽象类定死上限接口,重写提供不同类型的差异,多态让借阅流程不关心会员具体是谁。判断一段代码有没有真用上多态,看父类方法里是不是只调接口、新增子类时要不要回来改父类和系统代码,就够了。