千行代码力破图书管理项目(一):项目初始化与基础架构搭建

📅 2026/7/29 11:02:44
千行代码力破图书管理项目(一):项目初始化与基础架构搭建
你好我是林森lsjs我的Github地址:sqyCoder (Qiyang) · GitHub目录一、前言二、项目架构设为什么选择这些设计模式1.系统功能模块划分2.角色权限设计3.设计模式选型单例模式工厂模式代理模式三、项目目录结构搭建创建包结构四、常量类 Constant告别硬编码什么是硬编码为什么要避免创建常量类常量类的关键知识点1.public static final 是什么意思2.常量命名规范3.如何使用常量五、自定义异常PermissionExceptionJava异常体系回顾为什么选择继承RuntimeException创建自定义异常类如何使用自定义异常六、文件工具类 FileUtils七、借阅关系类 PairOfUidAndBookId为什么需要这个类创建PairOfUidAndBookId类八、核心实体类 Book从字段到方法的完整实现Book类需要哪些属性LocalDate日期类型完整Book类实现toJSON()方法序列化九、本章总结一、前言你好欢迎来到千行代码力破图书管理项目系列的第一篇图书管理系统是编程入门阶段的经典项目选题我们会在千行代码中融入单例模式、工厂模式、代理模式三种经典设计模式实现完整的权限控制管理员/普通用户、文件持久化数据不丢失、动态数组扩容、借阅统计、热门书籍排行等功能。整个系列分为三篇文章我们开始吧第一篇我们主要进行项目初始化与基础架构搭建。二、项目架构设为什么选择这些设计模式在动手写代码之前先花时间思考架构设计是非常值得的。好的架构能让代码更易维护、更易扩展也能让我们在开发过程中少走弯路。1.系统功能模块划分图书管理系统├─图书管理模块│ ├─ 上架图书│ ├─ 下架图书│ ├─ 修改图书│ ├─ 查询图书│ └─ 库存统计├─用户管理模块│ ├─ 管理员│ └─ 普通用户├─借阅管理模块│ ├─ 借阅图书│ ├─ 归还图书│ ├─ 借阅记录│ └─ 热门排行└─数据持久化模块├─ 书籍数据存取└─ 借阅记录存取2.角色权限设计系统中有两种角色他们的权限不同功能管理员普通用户查找图书✅✅查看所有图书✅✅上架图书✅❌修改图书✅❌下架图书✅❌借阅统计✅❌借阅图书❌✅归还图书❌✅查看我的借阅❌✅3.设计模式选型基于以上需求我们选择三种经典设计模式来构建系统架构单例模式应用于 Library 类。整个系统只有一个图书馆实例所有用户都操作同一个图书馆对象确保数据一致性。工厂模式应用于用户创建。通过工厂接口创建不同类型的用户管理员/普通用户将对象创建与使用分离。代理模式应用于权限控制。通过代理类包裹真实用户对象在执行操作前自动进行权限检查优雅实现权限控制。系统整体架构与设计模式关系图主程序入口└─ LibrarySystem.main└─ 创建工厂 → 工厂模式├─ IUserFactory接口│ ├─ AdminUserFactory│ │ └─ 创建 → AdminUser│ └─ NormalUserFactory│ └─ 创建 → NormalUser└─ 用户体系├─ User抽象类│ ├─ AdminUser继承User│ └─ NormalUser继承User└─ 代理模式└─ ProxyUser代理类包裹AdminUser、NormalUser└─ 操作 → 单例模式└─ Library唯一实例├─ 管理 → Book图书├─ 管理 → Pair借阅记录└─ 读写 → 文件持久化三、项目目录结构搭建在开始写代码之前我们需要先规划好包结构。合理的包结构能让代码组织更清晰便于维护。创建包结构在src目录下创建以下包四、常量类 Constant告别硬编码我们要写的第一个类是常量类。什么是常量为什么需要常量类什么是硬编码为什么要避免假设我们在代码中直接写//不好的写法硬编码魔法值 Book[] books new Book[5]; FileReader reader new FileReader(allbooks.txt); if (choice 4) { addBook(); }上面代码中的5、allbooks.txt、4被称为「魔法值」看到这些数字/字符串时你不知道它们是什么意思。如果将来要把默认容量从5改成10你需要在整个项目中搜索所有出现5的地方很容易漏掉或改错。把项目中反复使用的固定值定义为具名常量给魔法值起一个有意义的名字。修改时只需要改常量定义一处即可。创建常量类在constant包下创建Constant.javapackage constant; /** * 常量类 * * 集中管理项目中使用的常量避免硬编码 * 当常量值需要修改时只需修改此处 */ public class Constant { // 文件路径 // 书籍数据文件名 public static final String ALL_BOOK_FILE_NAME allbooks.txt; // 借阅记录文件名 public static final String BORROWED_BOOK_FILE_NAME borrowedbook.txt; // 数组默认容量 // 书籍数组默认容量 public static final int DEFAULT_BOOK_CAPACITY 5; // 借阅记录默认容量 public static final int DEFAULT_BORROWED_RECORD_CAPACITY 10; // 管理员菜单编号 public static final int SEARCH_BOOK 1; // 查找图书 public static final int DISPLAY_BOOK 2; // 打印所有图书 public static final int EXIT 3; // 退出系统 public static final int ADD_BOOK 4; // 上架图书 public static final int UPDATE_BOOK 5; // 修改图书 public static final int REMOVE_BOOK 6; // 下架图书 public static final int VIEW_BORROW_COUNT 7; // 查看借阅次数统计 public static final int GENERATE_TOP_BOOKS 8; // 热门Top-K书籍 public static final int CHECK_INVENTORY 9; // 查看库存状态 public static final int CLEAN_OLD_BOOKS 10; // 清理超过一年的图书 // 普通用户菜单编号 public static final int BORROW_BOOK 4; // 借阅图书 public static final int RETURN_BOOK 5; // 归还图书 public static final int VIEW_BORROW_RECORD 6; // 查看借阅记录 }常量类的关键知识点1.public static final 是什么意思这三个修饰符是Java定义常量的「黄金搭档」public公开的任何类都可以访问static静态的属于类本身不需要创建对象就能访问通过Constant.ALL_BOOK_FILE_NAME访问final最终的值一旦设定就不能修改2.常量命名规范常量名全部大写单词之间用下划线分隔这是Java的命名约定// 正确的常量命名 public static final int MAX_BOOK_COUNT 100; public static final String DEFAULT_USER_NAME 访客; // 错误的命名驼峰命名法用于变量和方法 public static final int maxBookCount 100; public static final String defaultUserName 访客;3.如何使用常量在其他类中通过类名.常量名的方式使用// 使用常量 Book[] books new Book[Constant.DEFAULT_BOOK_CAPACITY]; FileReader reader new FileReader(Constant.ALL_BOOK_FILE_NAME); if (choice Constant.ADD_BOOK) { addBook(); }这样代码可读性大大提升看到DEFAULT_BOOK_CAPACITY就知道这是书籍数组的默认容量。五、自定义异常PermissionException在我们的系统中普通用户尝试执行管理员操作比如上架图书时需要抛出「权限不足」的异常。Java虽然提供了很多内置异常类但自定义异常能让错误信息更精准。Java异常体系回顾Java异常分为两大类受检异常Checked Exception继承自Exception编译时必须处理try-catch或throws非受检异常Unchecked Exception继承自RuntimeException编译时不强制处理为什么选择继承RuntimeException我们的权限异常继承RuntimeException原因是权限不足是一种编程错误代码逻辑问题不是可恢复的业务异常不需要在每个方法上都声明throws代码更简洁在代理类中统一捕获处理即可创建自定义异常类在utils包下创建PermissionException.javapackage utils; /** * 权限异常类 * * 继承 RuntimeException 成为非受检异常 * 这样调用方不需要强制 try-catch * 但可以选择捕获处理 */ public class PermissionException extends RuntimeException { // 无参构造方法 public PermissionException() { super(); } // 带错误信息的构造方法 public PermissionException(String message) { super(message); } }如何使用自定义异常使用自定义异常和使用普通异常一样通过throw关键字抛出// 检查权限如果不是管理员则抛出异常 private void checkAdminPermission(String operationName) { if (!(realUser instanceof AdminUser)) { throw new PermissionException( 权限不足普通用户无权执行【 operationName 】操作。 ); } }然后在合适的地方捕获这个异常try { proxyUser.addBook(); // 可能抛出PermissionException } catch (PermissionException e) { System.out.println(e.getMessage()); // 输出友好的错误提示 }六、文件工具类 FileUtils本次项目我们不做文件类的讲解今后我们在讲解文件类知识的时候我们再回过头来看这个项目所以我们默认文件类的功能我们均已完成。七、借阅关系类 PairOfUidAndBookId在写Book类之前我们先写一个简单的辅助类。当用户借阅一本书时我们需要记录「哪个用户借了哪本书」这就是一种「用户ID-书籍ID」的配对关系。为什么需要这个类你可能会想直接用两个数组分别存userId和bookId不行吗当然可以但定义一个专门的类来表示这种配对关系代码更加面向对象也更易维护。创建PairOfUidAndBookId类在book包下创建PairOfUidAndBookId.javapackage book; /** * 辅助类记录借阅关系 * 表示某个用户(userId)借了某本书(bookId) */ public class PairOfUidAndBookId { private int userId; // 借阅用户的 ID private int bookId; // 被借阅书籍的 ID // 无参构造方法后续反序列化时需要 public PairOfUidAndBookId() { } // 有参构造方法 public PairOfUidAndBookId(int userId, int bookId) { this.userId userId; this.bookId bookId; } // Getter 和 Setter public int getUserId() { return userId; } public void setUserId(int userId) { this.userId userId; } public int getBookId() { return bookId; } public void setBookId(int bookId) { this.bookId bookId; } /** * 序列化方法将借阅关系转为逗号分隔字符串 * 格式: userId,bookId * 用于后续持久化到文件 */ public String toJson() { return userId , bookId; } Override public String toString() { return 借阅记录{用户ID userId , 书籍ID bookId }; } }八、核心实体类 Book从字段到方法的完整实现Book类是我们系统中最重要的实体类它代表一本图书。这个类包含了很多Java基础知识点我们会逐行详细讲解。Book类需要哪些属性在写代码之前先思考一本书应该有哪些信息分类属性名类型说明核心属性创建书时必填titleString书名authorString作者categoryString分类如文学、科技publishYearint出版年份shelfDateLocalDate上架日期系统状态有默认值isBorrowedboolean是否已被借出默认falseborrowCountint累计借阅次数默认0bookIdint图书唯一编号由系统分配LocalDate日期类型我们使用Java 8引入的LocalDate类来处理日期而不是老旧的Date类。LocalDate是线程安全的、API更友好的日期类。使用它需要导入import java.time.LocalDate; import java.time.format.DateTimeFormatter;完整Book类实现在book包下创建Book.javapackage book; import java.time.LocalDate; // 用于存储上架日期 import java.time.format.DateTimeFormatter; public class Book implements ComparableBook { // 1. 核心属性构造时必传 private String title; private String author; private String category; private int publishYear; private LocalDate shelfDate; // 2. 系统状态有默认值 private boolean isBorrowed false; private int borrowCount 0; // 3. 唯一标识由 Library 分配 private int bookId; // 构造方法创建一本新书时调用 public Book(String title, String author, String category, int publishYear, LocalDate shelfDate) { this.title title; this.author author; this.category category; this.publishYear publishYear; this.shelfDate shelfDate; // 注意这里没有初始化 bookId, isBorrowed, borrowCount因为它们有各自的默认值或稍后会被设置 } // --- Getter 和 Setter 方法 --- // 1. bookId 的 Getter 和 Setter public int getBookId() { return bookId; } public void setBookId(int bookId) { this.bookId bookId; // this 关键字代表当前对象本身区分了成员变量和参数 } // 2. title 的 Getter 和 Setter public String getTitle() { return title; } public void setTitle(String title) { this.title title; } // 3. author 的 Getter 和 Setter public String getAuthor() { return author; } public void setAuthor(String author) { this.author author; } // 4. category 的 Getter 和 Setter public String getCategory() { return category; } public void setCategory(String category) { this.category category; } // 5. publishYear 的 Getter 和 Setter public int getPublishYear() { return publishYear; } public void setPublishYear(int publishYear) { this.publishYear publishYear; } // 6. shelfDate 的 Getter 和 Setter public LocalDate getShelfDate() { return shelfDate; } public void setShelfDate(LocalDate shelfDate) { this.shelfDate shelfDate; } // 7. isBorrowed 的 Getter 和 Setter (注意 boolean 类型的 getter 命名习惯) public boolean isBorrowed() { // 通常用 is 代替 get return isBorrowed; } public void setBorrowed(boolean borrowed) { isBorrowed borrowed; } // 8. borrowCount 的 Getter 和 Setter public int getBorrowCount() { return borrowCount; } public void setBorrowCount(int borrowCount) { this.borrowCount borrowCount; } // --- 业务逻辑方法 --- /** * 增加借阅次数 * 当图书被成功借出时调用 */ public void incrementBorrowCount() { this.borrowCount; // 这里封装了操作未来可以在此添加更多逻辑比如记录日志、检查上限等 } /** * 减少借阅次数 (通常在归还时调用如果你想保留历史也可以不调用) */ public void decreaseBorrowCount() { if (this.borrowCount 0) { // 可以加入防护逻辑防止出现负数 this.borrowCount--; } } /** * 将 Book 对象序列化为逗号分隔的字符串 * 用于持久化到文件中 * * 字段顺序必须与 parseBookJson 保持一致: * bookId, title, author, category, publishYear, isBorrowed, borrowCount, shelfDate */ public String toJSON() { StringBuilder json new StringBuilder(); // 按固定顺序拼接所有字段用逗号分隔 json.append(bookId).append(,); json.append(title).append(,); json.append(author).append(,); json.append(category).append(,); json.append(publishYear).append(,); json.append(isBorrowed).append(,); json.append(borrowCount).append(,); // shelfDate 需要特殊处理格式化 null 检查 if (shelfDate ! null) { json.append(shelfDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); } else { json.append(null); } return json.toString(); } /** * 实现 Comparable 接口 * 按借阅次数降序排序借阅次数多的排在前面 * * 返回值规则 * 负数 - this 排在 o 前面 * 正数 - this 排在 o 后面 * 0 - 相等 */ Override public int compareTo(Book o) { // 降序排序o.borrowCount - this.borrowCount // 如果 this 借阅次数多o.borrowCount - this.borrowCount 为负this 排前面 return o.borrowCount - this.borrowCount; } /** * 重写 toString()提供友好的文本展示 * 用于 System.out.println(book) 时的输出 */ Override public String toString() { return 图书信息{ ID bookId , 书名 title \ , 作者 author \ , 分类 category \ , 出版年份 publishYear , 状态 (isBorrowed ? 已借出 : 在馆) , 借阅次数 borrowCount , 上架日期 shelfDate }; } }toJSON()方法序列化toJSON()方法的作用是把对象转换为字符串以便保存到文件。这个过程叫序列化相反的过程叫反序列化。我们这里用简单的逗号分隔格式而不是真正的JSON真正的JSON解析需要第三方库为了保持纯Java实现我们用简单格式。字段顺序非常重要序列化和反序列化的顺序必须完全一致否则数据会错乱。九、本章总结到目前为止我们已经写好了5个类constant/Constant.java- 常量类utils/PermissionException.java- 权限异常类utils/FileUtils.java- 文件工具类book/PairOfUidAndBookId.java- 借阅关系类book/Book.java- 图书实体类第一篇文章到这里就结束了在第二篇文章中我们将进入核心业务逻辑的实现。你将学到使用单例模式创建Library类确保全局只有一个图书馆实例实现图书的增删改查功能理解数组操作的核心算法实现动态数组扩容让书架容量不受限实现图书的借阅与归还逻辑编写数据解析类实现文件持久化——让数据真正保存下来实现借阅统计、热门书籍排行、库存检查等高级功能如果觉得有帮助不妨跟着代码敲一遍。编程没有捷径亲手敲过的代码才是你的。我们第二篇再见诸位共勉