图书管理系统面向对象分析与设计实战:从用例图到状态机
2026/9/19 2:13:02 网站建设 项目流程

简介:这份图书管理系统面向对象分析与设计报告,是软件工程课程设计、系统分析作业及小型信息管理系统开发前的重要参考,适合正在学习UML建模、需要撰写课程报告或启动图书馆相关项目的学生与开发者。报告围绕系统管理员、图书管理员、读者三个子系统展开,完整覆盖开发背景、设计目标、需求描述、功能模型、对象模型和动态模型;内容从读者借书、图书管理员处理借还书、系统管理员系统维护等典型用例出发,细化到读者、系统管理员、图书管理员、书目等核心类,并用类图、时序图、活动图呈现各角色协作流程,能清晰展示从需求分析到面向对象设计的完整链路。资源包共1个doc文件,大小约1MB,文件为可编辑的Word报告,适合直接作为课程设计报告模板、系统建模参考或开题素材。已有649人次浏览学习,可用于快速搭建系统分析框架,减少整理和绘图时间。

1. 图书管理系统面向对象分析与设计,先分清“对象”和“流程”

图书管理系统的需求从表面看并不复杂:录入图书、办理借还、统计在架。但真正动手写需求分析时,多数人会被“借书”这个动作卡住——图书管理员扫描读者条码、扫描图书条码、检查借阅资格、更新库存,这一串操作如果全部写成流程文档,最后得到的是一份线性步骤清单。需求一变更(比如增加预约、超期罚款、多副本管理),这套静态流程就被迫推倒重画。面向对象分析与设计的价值恰恰在这里:它不把系统描述成“先做什么、再做什么”,而是先回答“系统里有哪些对象,它们各自持有什么数据、能响应什么操作”。借书、还书、续借被重新理解为读者、图书副本、借阅记录这三个对象之间的协作,需求的增删改都收敛到对象身上。这篇报告适合正在做课程设计或小型系统重构的工程师和开发者,目标是让面向对象分析从“画几张图交差”变成“能指导编码和数据库设计”的一整套可执行方法。

2. 面向对象分析的起点:从用例图与执行者识别梳理图书管理系统边界

2.1 图书管理系统的执行者到底有谁,为什么不能只画一个“管理员”

很多图书管理系统的用例图画到最后只有一个参与者:管理员。这是面向对象分析里最常见的偷懒。执行者(Actor)的定义是“与系统交互的外部角色”,它不一定是人,也可以是另一个系统。图书管理系统里,读者虽然不直接登录后台,但在“查询图书”“预约借阅”场景中,读者是发起交互的一方,因此必须建模为独立执行者。把读者漏掉,会导致后续对象设计里缺失“读者”这个核心类,还书时无法追溯是谁借走了书。

识别执行者的常见做法是直接对着业务场景逐条提问:谁向系统提供数据?谁消费系统输出的信息?谁在异常情况下需要被通知?按这个思路走,图书管理系统通常能得到三类执行者:图书管理员(处理借还、上架、读者管理)、读者(查询、预约、个人借阅历史)、以及可能的外部系统(如校园一卡通认证服务,如果做了对接)。不要为了用例图“好看”而削减执行者,每砍掉一个外部角色,就意味着一块真实需求被忽略。

2.2 用用例规约把“借书”从一句话扩成可验收的业务规则

用例图只回答了“谁能用系统做什么”,它不负责描述细节。同样是“借书”,不同学校、不同图书馆的规则可以完全不同:有的允许借 30 天,有的只允许 14 天;有的允许续借两次,有的只允许一次。这些规则必须落到用例规约(Use Case Specification)里,否则开发阶段程序员只能靠猜。

2.2.1 借书用例的主成功场景与备选流

以最典型的借书流程为例,主成功场景可以写成:

  1. 图书管理员输入读者编号,系统显示读者信息和当前借阅数量
  2. 管理员逐本扫描图书条码,系统检查该副本是否可借
  3. 系统创建借阅记录,将副本状态置为“已借出”
  4. 系统提示借阅成功,并返回应还日期

备选流比主场景更重要,它包含了业务规则:读者存在逾期未还图书时,系统拒绝借阅并提示;图书副本状态为“已预约”时,需要判断当前读者是否为预约者;读者借阅数量达到上限时,提示超限。这些备选流是后面设计状态机和数据库约束的第一手依据。

代码层面,用例规约的“可执行版本”通常体现为服务层的校验逻辑。以 Java 为例,借书核心校验可以写成:

public BorrowRecord borrow(String readerId, String bookCopyId) { Reader reader = readerRepository.findById(readerId); if (reader.hasOverdueBooks()) { throw new BusinessException("存在逾期未还图书,无法借阅"); } if (reader.getBorrowedCount() >= reader.getMaxBorrowLimit()) { throw new BusinessException("已达到最大借阅数量"); } BookCopy copy = bookCopyRepository.findById(bookCopyId); if (copy.getStatus() != BookStatus.AVAILABLE) { throw new BusinessException("该副本当前不可借"); } copy.borrow(); return borrowRecordRepository.save(new BorrowRecord(reader, copy)); }

这段代码的关键不在语法,而在它的结构:读者是否有逾期、副本是否可借,这些判断逻辑分别由ReaderBookCopy对象自己回答,而不是由一个巨大的工具类用一堆 if-else 去翻数据库。这正是面向对象分析与面向过程编码的分水岭——业务规则归属到拥有数据的对象身上。

2.2.2 用例点估算驱动规模认知

用例规约还承担一个容易被忽视的任务:工作量估算。面向对象的规模估计常用“用例点”(Use Case Points)方法,把每个用例按事务数分成简单、一般、复杂三档,再结合技术复杂度因子和环境因子折算人天。图书管理系统通常有 6 到 12 个用例,如果 5 个以上用例被判定为“复杂”,说明需求边界没有收敛,团队在动手写代码前就应该重新审视需求。

用例分析阶段的交付物不是图画完就结束,而是必须达到“每个用例的备选流能被测试人员直接转写成测试用例”的精细程度。做到这一点,面向对象分析与后续设计阶段的衔接才算真正打通。

3. 从分析到设计:图书管理系统的类图、对象职责与关系建模

3.1 用 CRC 卡片法发现领域对象,而不是靠拍脑袋列类

面向对象设计的第一步是找对象。常见误区的做法是参照别人的系统照搬类名,比如看到网上开源项目里有Book类就跟着写一个Book类。但Book这个抽象粒度在图书管理系统里往往是有问题的:一本书可能有多本副本,每本副本的破损状态、借阅状态各不相同。把“书”和“副本”混成一个类,会导致借书时无法精确到具体哪一本。

CRC 卡片法(Class-Responsibilities-Collaborators,类-职责-协作)是解决这个问题的经典手段。拿一张实体卡片,上面写三个区域:类名、职责、协作者。针对“借书”这个用例场景开始推演:

  • BookCopy(图书副本):职责是记录自身状态(可借、已借出、破损、遗失),协作者是BorrowRecord
  • Reader(读者):职责是校验借阅资格、累计借阅数量,协作者是BorrowRecord
  • BorrowRecord(借阅记录):职责是记录借出时间、应还时间、实际归还时间,协作者是ReaderBookCopy
  • FineRule(罚款规则):职责是计算逾期费用,协作者是BorrowRecord

用这种方式推完所有用例后,类清单是需求场景自然推导出来的,而不是从数据库表反推出来的。这个顺序很重要:先有对象模型,后有表结构,而不是先建表再写类。

3.2 图书管理系统核心类图的关联、聚合与组合

类与类之间的关系是面向对象设计报告中最容易画错的部分。图书管理系统的核心关系只需要分清三种:关联(Association)、聚合(Aggregation)、组合(Composition)。

  • ReaderBorrowRecord之间是关联关系:读者可以拥有零条或多条借阅记录,但借阅记录不随读者的消亡而消亡(借阅历史需要留存),所以这是普通关联。
  • LibraryBookCopy之间是聚合关系:图书馆拥有图书副本,但副本被剔除馆藏后,Library对象依然存在。聚合表达的是“整体与部分可分离”。
  • BorrowRecordBorrowRecordItem(如果支持一次借多本)之间是组合关系:明细条目的生命周期完全依附于主记录,主记录删除,明细必须一并删除。

用 Java 代码表达这三种关系时,差异非常明显:

// 关联:Reader 持有 BorrowRecord 的引用,但两者生命周期独立 public class Reader { private List<BorrowRecord> records; } // 聚合:Library 持有 BookCopy 的集合,但 BookCopy 可独立存在 public class Library { private List<BookCopy> copies; } // 组合:BorrowRecord 持有明细列表,明细不能脱离主记录存活 public class BorrowRecord { private List<BorrowRecordItem> items; }

写代码时的判断标准是:删除外部对象时,内部对象是否还需要继续存在?需要,就是关联或聚合;不需要,就是组合。图书管理系统中最容易混的是ReaderBorrowRecord,有的设计把BorrowRecord写成Reader的内部类,这在对象生命周期上是错误的——读者被删除后借阅历史仍然要保留,用于统计和审计。

3.3 图书管理系统设计中用到的关键设计模式

面向对象设计报告如果不涉及设计模式,落地时很容易写成“只有 getter/setter 的实体类集合”。图书管理系统里真正高频使用的是策略模式(Strategy Pattern)和工厂模式(Factory Pattern)。

策略模式的最佳落点是罚款计算。不同读者类型(学生、教师、校外读者)的罚款规则可以不同:学生每天 0.1 元,教师免费,校外读者每天 0.5 元。如果把这些规则用 if-else 堆在FineService里,每加一种读者类型就要改一次核心服务类。用策略模式重构后:

public interface FineCalculator { BigDecimal calculate(BorrowRecord record); } public class StudentFineCalculator implements FineCalculator { public BigDecimal calculate(BorrowRecord record) { long overdueDays = record.getOverdueDays(); return new BigDecimal(overdueDays).multiply(new BigDecimal("0.1")); } } // 根据读者类型从 Map 中取策略,而不是写 if-else public class FineService { private Map<ReaderType, FineCalculator> calculators; public BigDecimal getFine(BorrowRecord record) { return calculators.get(record.getReader().getType()).calculate(record); } }

这里FineService不再关心具体的计算规则,只负责根据读者类型分发到对应策略。新增一种读者类型时,只需要新增一个FineCalculator实现类,并在配置 Map 里注册,已有的服务类代码不需要改动。这符合开闭原则:对扩展开放,对修改关闭。

至于代码中的getOverdueDays()方法,面向对象的设计原则是“让对象自己计算自己的数据”,而不是在外面写一个DateUtil.daysBetween(record.getDueDate(), new Date())工具方法。BorrowRecord应该持有应还日期,并自我回答“逾期多少天”。否则罚款规则散落在各个 Service 类里,维护成本随时间线性增长。

4. 动态建模:图书管理系统借还流程图背后的状态机与对象交互

4.1 用状态机描述图书副本生命周期,替代含糊的“流程图”

图书管理系统最核心的动态模型不是借书流程图,而是BookCopy的状态机。副本从入馆到报废,会经历多个状态:在架(AVAILABLE)、借出(BORROWED)、预约锁定(RESERVED)、破损维修(DAMAGED)、遗失(LOST)、注销(WITHDRAWN)。很多设计只用一个status字段存字符串,然后在 Service 层到处判断,这种方式会导致状态转换逻辑散落各处。

面向对象设计的做法是把状态转换收拢到BookCopy类内部。典型的状态转换规则是:AVAILABLE 可以转向 BORROWED 或 RESERVED;BORROWED 只能转向 AVAILABLE(还书)或 LOST(报失);RESERVED 可以转向 BORROWED(预约者来借)或 AVAILABLE(预约取消)。这些转换如果写成流程图会非常长,但用状态机表达则只需要一张表。

下面用代码实现一个简洁的状态机核心逻辑:

public class BookCopy { private BookStatus status; public void borrow(Reader reader) { // 状态机校验:只有 AVAILABLE 和 RESERVED 状态可以借出 if (status != BookStatus.AVAILABLE && status != BookStatus.RESERVED) { throw new IllegalStateException("当前状态不可借出"); } // RESERVED 状态下需要校验预约者身份 if (status == BookStatus.RESERVED && !this.reservedBy.equals(reader)) { throw new IllegalStateException("该副本已被其他读者预约"); } this.status = BookStatus.BORROWED; } public void markLost() { // 只有 BORROWED 状态的副本才能报失 if (status != BookStatus.BORROWED) { throw new IllegalStateException("仅借出状态的副本可报失"); } this.status = BookStatus.LOST; } }

这里的关键在于borrow()方法内部的守卫条件:它不是简单地setStatus(BORROWED),而是先判断当前状态是否允许迁移。所有非法状态转换都集中在BookCopy内部抛出异常,而不是让调用方(Service 层)自己去 if-else 判断。这样设计后,哪怕未来新增状态(比如“隔离消毒”),只需要改BookCopy一个类。

当前状态允许的迁移触发操作迁移后状态
AVAILABLE借出borrow()BORROWED
AVAILABLE预约reserve()RESERVED
RESERVED借出(限预约者)borrow(reader)BORROWED
RESERVED取消预约cancelReserve()AVAILABLE
BORROWED还书returnBook()AVAILABLE
BORROWED报失markLost()LOST
DAMAGED修复完成repair()AVAILABLE

状态机设计给后续开发带来一个直接好处:单元测试可以按状态迁移表穷举,而不是靠人肉想边界条件。测试用例的数量等于“状态数乘平均迁移数”,图书管理系统这个模型只需要十来个测试就能覆盖所有非法路径。

4.2 借书场景的时序图:对象之间如何协作

状态机回答了“单个对象如何变化”,时序图(Sequence Diagram)则回答“多个对象如何协作完成一个用例”。以“借书”为例,参与者有管理员、BookCopyBorrowRecordReader。一次完整交互的消息序列是:

  1. 管理员调用BookCopyService.borrow(readerId, copyId)
  2. Service 加载Reader对象,调用reader.canBorrow()校验资格
  3. Service 加载BookCopy对象,调用copy.borrow(reader)完成状态迁移
  4. Service 创建BorrowRecord,传入readercopydueDate
  5. Service 保存记录并返回结果

时序图设计中最容易被忽略的是“谁负责协调”。图书管理系统如果让Reader直接调用BookCopy.borrow(),就形成了对象间的直接耦合,未来的需求变更(比如借书前需要管理员审批)会同时改动两个类。常见做法是引入一个BorrowService作为门面(Facade),由它统一编排。这也是面向对象设计与面向对象分析的关键差别:分析阶段识别协作关系,设计阶段则要引入控制类来解耦。

4.3 对象持久化与数据库设计的映射关系

面向对象模型最终要落到数据库表。这里有一个经典矛盾:对象模型用关联访问,关系模型用外键和连接查询访问。图书管理系统的常见映射方案是三张核心表加一张关联表:

CREATE TABLE reader ( reader_id VARCHAR(32) PRIMARY KEY, reader_name VARCHAR(64) NOT NULL, reader_type TINYINT NOT NULL COMMENT '1-学生, 2-教师, 3-校外' ); CREATE TABLE book_copy ( copy_id VARCHAR(32) PRIMARY KEY, book_title VARCHAR(128) NOT NULL, status TINYINT NOT NULL COMMENT '1-在架, 2-借出, 3-预约, 4-遗失' ); CREATE TABLE borrow_record ( record_id VARCHAR(32) PRIMARY KEY, reader_id VARCHAR(32) NOT NULL, copy_id VARCHAR(32) NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE, fine_amount DECIMAL(8,2) DEFAULT 0, FOREIGN KEY (reader_id) REFERENCES reader(reader_id), FOREIGN KEY (copy_id) REFERENCES book_copy(copy_id) );

这三张表与对象模型的关系是:reader表对应Reader类,book_copy表对应BookCopy类,borrow_record表对应BorrowRecord类。关键映射决策在于status字段:对象模型里用枚举,数据库里用 TINYINT 加注释,不直接存字符串。原因有两个:一是存储空间更小,二是避免字符串拼写错误导致的状态无法识别。

这里需要指出一个常见设计陷阱:是否要把Book(书目)和BookCopy(副本)拆成两张表。如果系统只服务于单一图书馆且不需要区分同一书名的多个版本,可以合并;但只要存在“同一本书采购了 5 本”的需求,就必须拆分。合并会导致每本副本都要冗余书名、作者、出版社、ISBN 等静态属性,数据一致性难以维护。面向对象分析与数据库设计的共同判断标准是:基础信息与状态信息的变化频率是否一致?不一致就拆。

5. 从模型到代码:Spring Boot + JPA 落地图书管理系统的对象映射策略

5.1 实体类设计与 JPA 注解的对应关系

面向对象设计报告如果止步于 UML 图,对开发者的实际帮助有限。接下来把对象模型映射到 Spring Boot + JPA 技术栈,展示类图如何变成可运行的代码。BookCopy实体用 JPA 注解实现状态映射:

@Entity @Table(name = "book_copy") public class BookCopy { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "book_title", nullable = false) private String bookTitle; @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false) private BookStatus status; @Version private Long version; protected BookCopy() { // JPA 规范要求提供无参构造器 } public void borrow(Reader reader) { if (status != BookStatus.AVAILABLE && status != BookStatus.RESERVED) { throw new IllegalStateException("当前状态不可借出"); } this.status = BookStatus.BORROWED; } }

这里有几个值得注意的参数配置:@Enumerated(EnumType.STRING)表示枚举以字符串形式存入数据库,可读性比ORDINAL(数字序号)好,新增枚举值不会破坏已有数据;@Version是 JPA 的乐观锁字段,用于防止两个管理员同时借出同一本副本——这在图书管理系统的并发场景中不是理论问题,而是真实会发生的问题。两个人同时扫描同一本书的条码,如果没有乐观锁,两次请求都会读到 AVAILABLE 状态并成功借出,最终产生一条脏数据。

5.2 仓储层设计:面向接口编程而非直接操作 EntityManager

Spring Data JPA 的 Repository 接口设计本身就体现了面向对象思想——调用方依赖接口,而不是依赖具体实现。图书管理系统常见的查询需求包括“按书名模糊查询”“查询某读者当前借阅的图书列表”“查询所有逾期未还记录”,对应接口可以这样定义:

public interface BorrowRecordRepository extends JpaRepository<BorrowRecord, Long> { // 查询读者当前未归还的借阅记录 List<BorrowRecord> findByReaderIdAndReturnDateIsNull(Long readerId); // 查询所有逾期未还记录,配合 @Query 自定义 JPQL @Query("SELECT r FROM BorrowRecord r WHERE r.returnDate IS NULL AND r.dueDate < CURRENT_DATE") List<BorrowRecord> findAllOverdue(); }

findByReaderIdAndReturnDateIsNull这种派生查询方法在简单场景下足够,但一旦条件增多(比如同时过滤读者类型和借阅日期范围),方法名会变得冗长且难以维护。常见做法是超过三个查询条件时改用@Query写 JPQL 或使用 Specification 模式。JPQL 操作的是对象属性而不是数据库列名,这一点与面向对象思想一脉相承:r.dueDate对应BorrowRecord类的字段,而不是数据库表的due_date列。

5.3 对象模型与关系数据库的差异处理:继承与关联

JPA 映射中有一个图书管理系统必须面对的难点:读者类型如果建模为继承关系(StudentTeacher都继承Reader),映射到数据库有三种策略:单表(Single Table)、 joined(连接表)、table-per-class(每类一表)。图书管理系统通常不需要这种复杂的继承映射,因为读者类型的差异仅仅体现在罚款规则和最大借阅数量上,用枚举字段加策略模式就够了。

但如果在需求分析阶段确实发现不同类型读者有很多独有字段(比如教师需要记录院系、学生需要记录年级),这时才需要引入继承映射。单表策略的写法是:

@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "reader_type", discriminatorType = DiscriminatorType.INTEGER) public class Reader { ... } @Entity @DiscriminatorValue("1") public class Student extends Reader { ... }

SINGLE_TABLE策略会把所有子类的字段合并到一张表里,不存在子类特有的字段会置为 NULL。这种方案查询性能最好、实现也简单,适合差异字段少的场景。只有字段差异很大时才考虑JOINED策略,因为连接查询的开销在数据量大时会明显拖慢列表页。面向对象设计报告到这里应明确给出一个结论:优先用组合和枚举,而不是继承;只有继承能显著减少重复代码时才使用它。

6. 借还环节的 3 个常见建模误区和验证技巧

图书管理系统建模最后要做的不是继续加类,而是检查已有模型能否经受住几个刁钻场景的考验。正向推演时很难发现模型缺陷,反向验证则有效得多。

第一个常见误区是“还书时找不到借阅记录”的边界处理。管理员的实际工作场景是读者拿着一本早已超过应还日期的书来还。这本身不会产生问题,但如果系统设计里把BorrowRecord的查询条件写成“读者 ID + 副本 ID + 归还时间为空”,当同一读者多次借阅同一副本时,查询结果会返回多条记录。这里正确的建模方法是查询最新的未归还记录,并按借出时间倒序取第一条。在 Repository 层可以这样写:

Optional<BorrowRecord> findTopByCopyIdAndReturnDateIsNullOrderByBorrowDateDesc(Long copyId);

findTop...OrderBy...Desc的组合是 Spring Data JPA 里处理“取最新一条”的标准手法。这个细节说明对象设计不仅要定义字段,还要为每个集合属性明确“如何获取特定元素”的查询语义。

第二个误区是状态字段与业务规则混在一起。有的设计在BookCopy里加一个available布尔字段,用它判断是否可借。但availablestatus表达的是同一事实,两个字段会出现不一致:数据库里status=RESERVED,但available=true。面向对象设计的准则是单一事实来源(Single Source of Truth),状态只保留一个字段,是否可借通过status推导,而不是额外存储。如果需要对外提供便捷判断,可以提供isAvailable()这样的只读方法,而不是一个可写的字段。

第三个误区是忽略了“预约到期”这个隐形状态迁移。一本被预约的书,预约者超过 24 小时未取,系统应自动释放并转回 AVAILABLE。很多建模把预约到期做成一个定时任务去扫描,但更好的做法是让Reservation对象自己判断是否过期。判断逻辑放在过期查询 SQL 里,本质上绕过了对象模型,预约状态的转换规则就散落在代码各处。把逻辑收归对象内部后,未来调整预约时长只需改一处。

最后给出一个验证建模质量的技巧:不写任何技术文档,把类图和状态机给一个没参与设计的同事看,让他描述“一本被预约的书在预约者取走之前,系统经历了哪些状态变化”。如果他 30 秒内说不清,说明状态机设计有过多的隐式分支,需要简化;如果他能直接指出“RESERVED 到 BORROWED 的迁移需要校验预约者身份”,这个模型就是合格的。在写完用例、类图、时序图、代码和建表脚本之后,这个口述验证仍然值得做一次。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询