面向对象练习:图书借阅系统里的封装、抽象类与多态

前言

借书的规则很具体:书有库存,会员有借阅上限,不同会员的上限还不一样。

一、先看整体:一个借阅系统拆成哪几个角色

角色 负责什么 关键方法
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 各负责一层,少一层都会出错。

卡号前缀决定会员类型是隐式约定。 卡号规则一改,实例化就会落到错误的子类,数据里显式存一个类型字段会更稳。

九、总结

这个练习把面向对象串成了一条链:封装守住库存,抽象类定死上限接口,重写提供不同类型的差异,多态让借阅流程不关心会员具体是谁。判断一段代码有没有真用上多态,看父类方法里是不是只调接口、新增子类时要不要回来改父类和系统代码,就够了。

相关推荐
老木避暑研究所1 小时前
负氧离子数据的采集、清洗与可视化:一套可复用的环境监测数据分析模板
前端·人工智能·数据分析
xyouyuweizhu1 小时前
Java IO 流详解:流的作用、流的划分与标准输入输出流
java·python·php
严同学正在努力1 小时前
PostgreSQL 高可用教程(流复制 + Patroni 生产级实战)
运维·数据库·ai·postgresql·架构
牧瀬クリスだ1 小时前
Redis String深度解析:从底层到五大实战场景
数据库·redis·缓存
lumengabc1 小时前
视频制作文字转语音PPT台词生成合成合并视频导出ffmpeg软件python edge-tts
python
沐晓时光1 小时前
C语言入门,深入理解指针(6)
c语言·开发语言
MSTcheng.1 小时前
Agent 跑到一半崩了,怎么从上一步接着跑?LangGraph StateGraph 实战:状态持久化、断点恢复与人工审批
python·状态管理·ai agent·langgraph·断点恢复·人工审批
略略略咯咯1 小时前
33333
开发语言·windows·python
java_prinln1 小时前
11. 说说 Single-Agent 和 Multi-Agent 的设计方案?
python