我带的练习课走到第17天,前十六天把语法和基础概念过完,继承、多态、接口、抽象类这些面向对象的核心知识点课堂上都演示过,但每次一到综合练习环节,问题就暴露得很明显:大部分人不是不会写代码,而是不知道这个类该不该继承、这个方法该放在哪个类里、接口到底插在哪一层。今天这节是“面向对象综合练习(下)”,正好就从这个最让人纠结的地方入手,用一个完整的小项目把前面学的知识串成一条线。这一篇适合正在学Java、Python这类面向对象语言的人,尤其是课程进度到了“语法都会、设计不会”阶段的朋友,看完可以直接照着练。
1. 练习目标与项目选型思路:为什么偏偏选图书借阅系统
1.1 业务系统练习比算法题更能逼出设计能力
做综合练习的时候,有个常见误区是去找算法题或者刷题网站,觉得难一点、绕一点就能把面向对象练好。实际上算法题考的是逻辑,面向对象考的是组织能力,两者根本不是一回事。综合练习要的就是一个能“跑起来看得到状态变化”的业务系统,图书借阅系统是我这几年带练习用过最顺手的载体,没有之一。
为什么是图书借阅?因为它麻雀虽小五脏俱全。一个借阅系统里天然存在“书”和“会员”两个核心实体,书又可以分为纸质书、电子书、有声书,会员也分普通会员和会员卡等级。借书、还书、续借、扣费、检索,这些动作每一个都是方法,每一个方法都有明确的归属对象。更妙的是,不同种类的书计费规则完全不同,这简直是教多态最理想的土壤。
我第一年带练习用的是“员工管理系统”,后来发现不行,员工类的属性太多,学生全都陷在setter和getter里出不来,根本来不及想设计。图书系统就不一样,书籍的属性相对收敛,title、author、year、isbn就够用了,算上特有属性也不会超过七八个字段。学生可以把精力花在类与类之间的关系上,而不是疲于应付字段搬运。
选这个题还有一层原因:它的扩展方向非常多。练完之后加一个“会员积分”、加一个“逾期罚款策略”、加一个“电子书试读章节”,都是顺理成章的事。一个项目能持续生长,学习者才会在迭代中反复体会面向对象带来的重构收益。
1.2 从需求描述到类图:先找名词,再找动词
拿到需求先别急着写代码。我带练习时反复强调一个习惯:把需求描述翻译成类图,用10分钟画出来,能省半个小时的返工。具体做法是找名词和动词。
看这样一段需求:
图书馆有多本图书,每本图书有标题、作者、出版年份和ISBN。图书分纸质书、电子书和有声书三种。纸质书可以借出,每天借阅费1元,借期14天;电子书有下载链接,每天费用0.5元,借期7天;有声书按时长收费,每分钟0.2元。会员可以借书,系统要能记录谁借了哪本书,还书时结算费用。系统还要支持按标题搜索图书。
把名词圈出来:图书、标题、作者、年份、ISBN、纸质书、电子书、有声书、会员、借阅记录、费用。再把动词圈出来:借出、还书、结算、搜索。
名词里的“图书”和“会员”是核心类;“纸质书、电子书、有声书”是“图书”的子类;“借阅记录”是关联类,记录“谁在什么时间借了哪本”;动词“借出、还书”是会员或图书上的方法,“搜索”是图书馆管理者的方法。
到这里,类图的骨架基本出来了。很多初学者会问:凭什么“借出”放图书上而不是放会员上?我的习惯是看动作的受害者是谁。借出会改变书的状态,书从在架变成借出,所以borrow方法是Book类的方法;返回一个借阅记录对象。搜索不改变任何状态,只是查询,所以放到Library类里作为管理操作。
这段“找名词定类、找动词定方法”的过程就是综合练习里最值钱的部分,它不是语法题,是设计题。
2. 建模阶段:先把继承、组合、接口划清楚
2.1 用继承表达“是什么”,用组合表达“有什么”
模型阶段第一个要决策的问题是:三种书之间的关系怎么表达。最直觉的方案是让PaperBook、EBook、AudioBook都继承Book,这是教科书上的标准操作。但继承不是越多越好,它有一个前提:子类必须“是一种”父类,同时还要满足父类定义了共同的抽象行为。
在这个例子里,三种书都包含标题、作者、年份、ISBN,并且都有“获取本次借阅费用”和“获取借阅期限”的行为,只是各自的规则不同。这种结构用继承是合理的:父类Book定义公共属性和抽象方法,子类分别实现自己的规则。反过来看,Library里有Book的列表,也有Member的列表,这是“拥有”关系,就不能用继承,要用组合,也就是成员变量。
组合和继承的选择有一条经验判断:A是B的一种,用继承;A包含B,用组合。学生经常把“图书是一种商品”这种逻辑误写成“图书馆是一种图书”,就是没分清这个。
这里补充一个我踩过的坑:早年代码里我把Library继承了一个ArrayList,觉得“图书馆就是一列书”,这样写看起来省事,结果所有客户端的代码都依赖Library是一个List,后来想加一个“会员黑名单”字段,根本没法加。改成Library内部持有一个List之后,反而清爽了。继承是类和类之间最强的耦合方式,能不用就不用,这是面向对象设计里的一条铁律。
2.2 抽象类还是接口:一张决策矩阵讲清楚
有了继承体系,紧接着的问题是Book到底用抽象类还是接口。我给学生的判断标准就三条,列个表更好用:
| 判断维度 | 用抽象类 | 用接口 |
|---|---|---|
| 子类之间是否有公共字段 | 有,比如title、author,直接放父类 | 没有公共字段,只有行为约定 |
| 是否需要默认实现 | 需要,公共方法可以给默认逻辑 | 不需要,全部由子类各自实现 |
| 语义关系 | “是一个”,子类和父类是强绑定 | “能做到”,类可以同时实现多个能力 |
图书这个场景里,Book明显应该用抽象类,因为ISBN、标题这些字段需要被子类共享,而且getBorrowFee如果不给默认实现,每个子类都要写。反过来看,“可借阅”这个能力,会员也能借、图书也能借,但它俩没有公共字段,就不该强行抽一个父类,而是定义一个Borrowable接口。
我这里有个直觉可以分享:接口是能力契约,抽象类是家族基因。当你拿不准的时候,先问自己子类之间有没有“家族相似性”。有公共字段、有共同行为基础,抽象类;只是需要共同遵守某种调用规则,接口。
2.3 业务对象与数据对象的分离:Library里该存什么
Library类的内部结构有个高频错误:直接用ArrayList 存书,然后遍历查找。数据量小的时候感觉不到问题,但借阅系统里迟早要按ISBN精确查找、按标题模糊查找。用ArrayList每次都要O(n)遍历,我建议直接上HashMap。
我的习惯是Library内维护两个容器:
- 一个
Map<String, Book>,key是ISBN,用于精确存取; - 一个
List<BorrowRecord>,记录所有借阅流水,用于还书结算和历史追溯。
很多初学者会问:为什么ISBN不能直接写在Map的key里?这涉及业务对象和数据对象分离的考虑。Book是一个业务对象,它有自己的行为和规则;Map的key是数据身份标识。把ISBN提升为key,意味着查找路径从“遍历整个书架找一本书”变成“直接按身份定位”,但Book对象本身并不依赖Map存在。
还有一个细节值得提:借阅记录别塞进Book类里。有同学设计Book类时加了一个List 字段,这确实能完成功能,但会把“这本书当前被谁借走”和“这本书历史上所有的借阅流水”混在一起。正确做法是Library持有所有流水,Book只维护一个currentBorrower的引用,没有借出则为null。这样,书、借阅流水、会员三者之间的关系通过Library统一调度,不会出现数据冗余。
3. 多态落地的两条主线:计价与归还
3.1 计价规则:用一个方法调度三种算法
面向对象学到综合练习阶段,最核心的考验就是多态怎么在真实业务里发挥作用。我们这代价抽象成一句话:调用方不关心对象具体是什么类型,只要知道它能“给我费用”,具体规则由对象自己决定。
三种书的计价规则分别是:纸质书每天1元、电子书每天0.5元、有声书按分钟0.2元。如果用ifelse写,代码会长这样:
double fee; if (book instanceof PaperBook) { fee = days * 1; } else if (book instanceof EBook) { fee = days * 0.5; } else if (book instanceof AudioBook) { fee = minutes * 0.2; }这段代码的问题是每加一种新书,都要来改这个方法。一旦忘了加分支,系统就静默出错。而且instanceof这种写法等于把类型判断散落在各处,面向对象的封装性就被破坏了。
多态写法是让每种书自己实现getBorrowFee()。基类Book声明抽象方法,子类各自覆写:
public abstract class Book { protected String title; protected String author; protected int year; protected String isbn; public Book(String title, String author, int year, String isbn) { this.title = title; this.author = author; this.year = year; this.isbn = isbn; } public abstract double getBorrowFee(BorrowContext context); public abstract int getLoanPeriodDays(); public String getTitle() { return title; } public String getIsbn() { return isbn; } }Book类的抽象方法接收一个BorrowContext参数,就是为了处理有声书这种“按时长计价”的类——它需要知道借了多少分钟,而纸质书只需要知道天数。不同子类对参数的使用不同,这正是多态的价值体现:公共方法的签名不变,具体解释权归子类。
计价在Library里就变成一行:
double fee = book.getBorrowFee(context);现在我们来对比一下扩展性。假设新加了一种“订阅制电子期刊BookPeriodical”,按每期0.3元收费。用ifelse的代码需要改计价方法和借出方法两个地方;用多态只需要新建一个子类,Library彻底不用动。这就是开闭原则:对扩展开放,对修改封闭。
3.2 用接口约束行为契约:让Marber和Book都能被借出
综合练习里接口的最佳落点,就是抽象出“可借出”这个行为。借阅系统里,会员可以借书,但有没有一种可能是“一本书借出另一本书”?有,比如电子书可以追加一个“借阅其他书籍”的虚拟引用,加一种“捆绑借阅”的玩法。
这就是接口的用武之地。定义一个Borrowable接口:
public interface Borrowable { BorrowRecord borrowBy(Member member); void returnBack(BorrowRecord record); }Book实现这个接口,会员卡也可以实现这个接口。Library的借出方法签名就可以面向接口编程:
public BorrowRecord borrow(Borrowable item, Member member) { if (item instanceof Book) { // 图书借出逻辑 } return item.borrowBy(member); }写到这里我多说一句,接口命名要克制。Borrowable这种“形容词”接口是最推荐的,一眼就知道“这个东西是可以被借的”,不要用IBook这种匈牙利式命名,Java社区已经不流行了。接口是一种行为维度,不是一种替代继承的垂直结构。
我更推荐的方式是让接口只聚焦一个能力,例如Priced、Searchable、Borrowable,每个接口只声明1到2个方法。综合练习里出现接口纯净度的问题,极大可能是接口太胖了,比如把borrow、search、printAll塞进一个接口,结果每个实现类都写一堆空方法,这就是接口污染,属于要重构的坏味道。
对于多态能力,我有一条实测结论:面向对象设计到最后,继承树控制在三层以内最舒服,剩下的行为差异用接口来补充。图书系统完美符合这个模型:Book在最上层,三种具体书在第二层,第三层留给未来扩展;借出能力通过接口注入。
4. 核心代码走读:借书、还书、检索的完整闭环
4.1 基类与三个子类的实现:各写各的计费规则
前面定义了Book抽象类,现在我们把三种子类写出来。这一段是整篇练习的实操核心,我建议在IDE里照着敲一遍,不要复制粘贴,敲的过程才是真正过脑子。
public class PaperBook extends Book { private int pages; public PaperBook(String title, String author, int year, String isbn, int pages) { super(title, author, year, isbn); this.pages = pages; } @Override public double getBorrowFee(BorrowContext context) { return context.getDays() * 1.0; } @Override public int getLoanPeriodDays() { return 14; } }public class EBook extends Book { private String downloadUrl; public EBook(String title, String author, int year, String isbn, String downloadUrl) { super(title, author, year, isbn); this.downloadUrl = downloadUrl; } @Override public double getBorrowFee(BorrowContext context) { return context.getDays() * 0.5; } @Override public int getLoanPeriodDays() { return 7; } }public class AudioBook extends Book { private int durationMinutes; public AudioBook(String title, String author, int year, String isbn, int durationMinutes) { super(title, author, year, isbn); this.durationMinutes = durationMinutes; } @Override public double getBorrowFee(BorrowContext context) { return durationMinutes * 0.2; } @Override public int getLoanPeriodDays() { return 3; } }这个设计的核心在于BorrowContext。它是一个简单的参数对象,字段包括days和minutes,避免在方法签名里堆太多参数,这是面向对象里的一个实用技巧:把相关参数封装成一个上下文对象。以后计价规则要加“会员折扣”、“VIP加成”,直接往BorrowContext里加字段就行,不需要改动getBorrowFee的签名。
AudioBook覆写getBorrowFee时完全没用到context里的days,用的是自己的durationMinutes。这种覆写里的“不对称”正是多态的常态,子类各取所需。
4.2 Member、BorrowRecord与Library:业务线怎么串起来
会员类的设计要克制,不需要一开始就带积分、押金那一堆,综合练习最怕的就是贪多。一个会员有姓名、会员号就够了,再多加一个已借书列表用于展示:
public class Member { private String name; private String memberId; private List<BorrowRecord> activeRecords = new ArrayList<>(); public Member(String name, String memberId) { this.name = name; this.memberId = memberId; } public void addRecord(BorrowRecord record) { activeRecords.add(record); } public void removeRecord(BorrowRecord record) { activeRecords.remove(record); } public List<BorrowRecord> getActiveRecords() { return activeRecords; } public String getMemberId() { return memberId; } public String getName() { return name; } }BorrowRecord记录谁借了什么、借出时间、到期时间、状态。它的字段设置是:Book book、Member member、LocalDate borrowDate、double fee。这里有个设计取舍:BorrowRecord应该持有Book的引用而不是把书名和ISBN拷贝一份。一开始拷贝字段的做法看起来“安全”,但会带来数据不同步的问题——书改名了,历史记录还是旧名字。持有引用则永远指向最新状态。
Library类是主调度器,持有booksMap和membersList以及records:
public class Library { private Map<String, Book> books = new HashMap<>(); private List<Member> members = new ArrayList<>(); private List<BorrowRecord> records = new ArrayList<>(); public void addBook(Book book) { books.put(book.getIsbn(), book); } public void addMember(Member member) { members.add(member); } public Book searchByTitle(String keyword) { for (Book book : books.values()) { if (book.getTitle().contains(keyword)) { return book; } } return null; } public BorrowRecord borrow(String isbn, Member member) { Book book = books.get(isbn); if (book == null) { throw new IllegalArgumentException("找不到这本书"); } if (member.getActiveRecords().size() >= 5) { throw new IllegalStateException("该会员借书已达上限"); } BorrowContext context = new BorrowContext(book, LocalDate.now()); BorrowRecord record = new BorrowRecord(book, member, LocalDate.now()); member.addRecord(record); records.add(record); return record; } public double returnBook(BorrowRecord record) { record.setReturned(true); memberRemove(record); Member member = record.getMember(); member.removeRecord(record); Book book = record.getBook(); int days = ChronoUnit.DAYS.between(record.getBorrowDate(), LocalDate.now()); BorrowContext context = new BorrowContext(book, LocalDate.now()); context.setDays(days); return book.getBorrowFee(context); } private void memberRemove(BorrowRecord record) { // 内部辅助方法,保持借阅记录列表和会员状态一致 } }这里需要注意一个细节:borrow和returnBook都构建了BorrowContext,但用途不同。借出时构建上下文是未来计算费用的基础,还书时构建上下文需要传入实际借了多少天。把业务状态(借了几天)和规则参数(每天多少钱)分开,是业务建模里一个很实用的姿势。
4.3 测试代码:用一个main把整个流程跑起来
写完了类,一定要用一个模拟场景验证整个流程。我习惯写一个Main类,把多本书、多个会员、借还操作串一个主干流程,打印结果,能直观看到面向对象的调度效果。
public class Main { public static void main(String[] args) { Library library = new Library(); library.addBook(new PaperBook("三体", "刘慈欣", 2008, "978-1", 302)); library.addBook(new EBook("人类简史", "尤瓦尔", 2011, "978-2", "http://example.com/homo")); library.addBook(new AudioBook("明朝那些事", "当年明月", 2009, "978-3", 480)); Member alice = new Member("Alice", "M001"); library.addMember(alice); // 借一本纸质书 BorrowRecord record1 = library.borrow("978-1", alice); System.out.println("成功借出:" + record1.getBook().getTitle()); // 还书并结算费用,假定借了5天 BorrowContext ctx = new BorrowContext(record1.getBook(), LocalDate.now()); ctx.setDays(5); double fee1 = record1.getBook().getBorrowFee(ctx); System.out.println("纸质书借5天费用:" + fee1 + " 元"); // 借有声书,费用按分钟 BorrowRecord record2 = library.borrow("978-3", alice); double fee2 = record2.getBook().getBorrowFee(new BorrowContext(record2.getBook(), LocalDate.now())); System.out.println("有声书费用:" + fee2 + " 元"); } }运行结果应该能看到:成功借出三体、纸质书借5天费用5.0元、有声书费用96.0元。到这里,继承、抽象类、多态、接口、组合全都在一个系统里转起来了。测试代码的价值还体现在:以后每加一种书,只需要改Main第一段,借还流程一行不改,这就是设计质量最直观的证明。
5. 面向对象练习的坑:症状、原因和我的排查手法
5.1 五个典型语法症状速查表
我带过好几轮练习,很多问题指向同一个根因:类和类之间的关系没理清。下面这张表是从几十份提交代码里总结出来的高频症状,每一条我都亲手修过,可以说比教科书更贴合实战。
| 症状 | 类似代码 | 根因 | 推荐修法 |
|---|---|---|---|
| 强行用类型判断 | if (book instanceof PaperBook) fee=1; | 没有利用多态 | 把计价行为搬进Book子类 |
| 类里只有getter/setter | Member类一堆字段没有方法 | 贫血模型,把对象当数据结构 | 把业务操作(借还)挂回类 |
| 一个类干所有事 | Library里借还、打印、搜索、计算费用 | 上帝类 | 按职责拆成Library/Record/Printer |
| 大量子类代码重复 | 三个子类的getBorrowFee逻辑几乎一样 | 公共抽象没提取 | 想想能不能抽中间基类 |
| Book里塞List | Book类持有BorrowRecord列表 | 混淆了“当前状态”和“历史流水” | 当前借阅人放Book,流水放Library |
5.2 从代码坏味道反推设计问题
“上帝类”这个坏味道在图书系统的练习里最容易出现。有的同学把Library类写成几百行的GodClass:搜索、借阅、还书、结算、打印、甚至生成报表全都塞进一个类。这种代码能跑,但一动就坏。拆的方法其实很明确:把“打印报表”这类IO操作挪出去,把“结算费用”放到Book或策略类里,Library只保留核心调度职责。判断一个类是不是上帝类,就看它的方法是否围绕同一个业务步骤运转。如果方法之间没有共享的内部状态,那它们就不该住同一个类。
“贫血模型”是另一个值得展开的问题。我收到过这样的代码:Book类只有属性加getter、setter,而借阅规则、计算费用全部写在Library的方法里。这种写法把面向对象写成了面向过程数据库表操作。贫血模型的根治办法是回去找动词:凡是“计算费用”“检查是否可借”这样的动词,尽量放到被操作的对象身上。一个类如果有状态却没有行为,就该反思是不是把行为搁错了地方。
“平行继承体系”出现的频率也不低。比如Book体系里有个PaperBook,业务代码里又要写一个PaperBookService;Book体系里有个EBook,业务代码里又写一个EBookService。这种双份结构就是平行继承体系的问题——一个类在业务层被重复包装了一层。正确做法是让Library直接面向Book抽象类编程,不在业务层复制继承树。
5.3 排查问题的顺序是先看异常,再看行为
综合练习的代码量上来了,初学者Debug时普遍没有章法。我自己的排查习惯永远是:先看异常类型,再讲行为逻辑,最后看数据内容。这条顺序能锁死八成的问题。
举个实际的例子,有个学生提交的代码报ClassCastException,说AudioBook不能转成PaperBook。他看了半天没明白:借的时候明明只借了有声书。我让他看borrow方法里的入参,发现图书馆的Book数据来自一个List,里面混着多种书,他直接用(PaperBook) book强转,当然炸了。这个问题的根子是类型判断,不是强转。正解是前面说的多态设计:不转型,直接调用getBorrowFee()。
另一个高频异常是ConcurrentModificationException,发生在遍历成员已借书列表的同时删记录。这种情况在还书逻辑里特别容易踩到:增强for循环遍历member.getActiveRecords(),循环体里removeRecord,直接爆炸。修法是从迭代器删除或者把需要删除的记录收集到一个临时列表,迭代结束后统一remove。这个坑代码层面能讲清楚,但设计层面的启发是:移除操作不应该暴露给调用方,Member.removeRecord方法内部自己处理迭代,调用方只关心借还成功与否。
5.4 我的习惯:先写异常分支,再写正常分支
写业务方法时大多数人喜欢先写正常流程,再考虑边界条件,结果往往忘了写。我自己反着来:先把参数为null、借书上限、书不存在这些异常分支全部抛出,最后才写成功路径。比如borrow方法,我先写if (book == null) throw...,再写if (member.getActiveRecords().size() >= 5) throw...,然后才写真正生成记录的逻辑。好处是:接口的边界条件在第一时间就明确了,调用方不该传入null、不该超出上限,这些约束成了方法签名的一部分。
用异常来锁边界,比在调用端到处判断“是不是null”舒服得多。Java的异常机制天然就是面向对象设计的一部分,它让方法签名能表达“我能做什么、我不能做什么”。这一招写得多了,代码的行数会稍微增加,但每一条if都对应一个明确的业务规则,后期维护的人看到异常分支就知道这个方法的权限边界在哪里,不用自己去猜。
6. 练习之后:从“能跑”到“像工程”的三步
6.1 跟真实项目的差距到底在哪
这套图书借阅系统跑通之后,很多同学觉得“面向对象我已经会了”。这个反应我特别能理解,因为练习的过程确实把核心语法用起来了,但我得泼一盆冷水:练习代码和真实项目之间还有三个比较大的跨越。
第一是并发。练习中的Library同一时间只被一个线程操作,真实系统里多个会员同时借书,HashMap会出线程安全问题。这个阶段不需要深入JUC,但要意识到:面向对象设计要考虑状态的竞争条件。比如borrow方法里先查书再改状态的步骤,在多线程里就不是原子操作了。这也是为什么后面要接触锁、事务这些概念。
第二是持久化。练习里程序一关,数据全没了。真实系统要把Book、Member、BorrowRecord落到数据库里。这里有个面向对象和关系数据库的天然摩擦:继承体系怎么映射到表结构?单表、类表还是具体表继承?这三个方案各有优劣,是ORM框架里最经典的讨论话题。图书系统练习恰好提供了完美的讨论素材,你可以试着想想PaperBook和EBook如果存进一张表,extra字段怎么处理。
第三是框架化。真实项目里对象创建、依赖注入、生命周期管理都交给Spring这类容器处理,代码关注点从“怎么建对象”变成“怎么声明对象关系”。练习里手动new一个Library,框架里是配置一个Bean。这要求你能读懂Bean的生命周期——构造函数、依赖注入、初始化方法、销毁方法这一整套流程。
6.2 后面可以继续往上加的三层练习
如果还想在这个练习上继续深化,我建议按顺序做三件事。
第一步是加日志。现在Library里没有输出,运行过程全靠Main打印。你可以引入一个简单的Logger或者自己写一个观察者模式,让Library在借出、还书时通知日志对象。这能练到“行为对外隐藏、事件对外广播”的解耦思路。
第二步是加策略模式替代ifelse的会员折扣。现在计价只有书籍类型这一个维度,会员等级其实也是维度。可以定义一个DiscountStrategy接口,普通会员不打折、银卡会员九折、金卡会员八折,通过策略对象注入到Library。面向对象的多态就能从“一个维度”扩展到“多个维度组合”,这时候你会真正体会到策略模式的价值。
第三步是写单元测试。用JUnit为Library写测试用例,测“借书成功”“超上限抛异常”“还书费用正确”。写测试的过程会逼你把方法签名设计得更好,比如依赖外部时间的代码怎么传时钟、依赖数据库的代码怎么构造假数据。面向对象设计好不好,测试好不好写着是一个很有效的度量标准。
6.3 一些关于做练习的体会
从我带练习的这几年经验看,面向对象综合练习最重要的不是代码多炫,而是培养一种条件反射:看到一个业务场景,脑子里自动开始模块化——哪些对象协作完成目标,每种对象有什么责任,对象之间怎么通话。
这套图书借阅系统我今天完整的拆解了一遍,代码量加起来两百行出头,不算多,但里面每一处设计决策:抽象类的选择、接口的划分、组合的运用、多态的落点,都是我在实际业务代码里反复验证过好用的套路。你如果能在自己的项目里把这个模式走通一遍,面向对象的底子就算真正打牢了。
最后给一个实操建议:做完这个练习后,把它放到一边,隔一周,不看代码,重新自己也写一个类似的报名管理系统或者教室预订系统。你会发现面向对象的分析能力已经悄然成了你的肌肉记忆,第二次做几乎不用犹豫就能画出类图。这种“迁移到新场景还能驾熟就轻”的能力,才是面向对象练习最终要交付的东西。