看到这个标题,我第一反应是挺亲切的。Java面试圈里“面向对象设计题”几乎是必考环节,而标题里的“面相”大概率是笔误,应该是“面向对象”。这种题目乍看像是课后作业,实际上是面试官用来摸你底细的经典套路。今天我就以这道“设计题2”为引子,把面向对象设计从需求拆解到代码落地的完整思路捋一遍,顺便把我自己踩过的坑和总结的经验一并放进来。
我不太清楚你拿到的原题具体是什么,但按照“Java 面向对象设计题”这个系列的通用来路,这类题一般长这样:要求你设计一个图书管理系统,或者一个简单的学校人员管理系统,或者模拟一个银行账户操作场景。题目本身不会太难,考的就是封装、继承、多态、抽象类、接口这些基础概念的落地能力,以及你面对一个模糊需求时能不能理清对象和对象之间的关系。
那这篇文章我就不假设你知道具体题目了,直接以最典型的“图书借阅管理”场景为例,从题目考的是什么、到每一步怎么设计、再到完整代码实现和避坑经验,一次性说透。
1. 内容整体设计与思路拆解:这类题真正在考什么
先说一个很多新手容易误解的地方。拿到“设计题”这三个字,第一反应是“我要把代码正确跑出来”。但对有经验的面试官或老师来说,正确跑通只是最底线,真正想看的其实是两件事:第一,你能不能把现实世界里的业务概念抽象成清晰的类结构;第二,你的代码在需求变化面前是不是足够灵活。
我自己见过不少候选人,把一个图书管理系统写成了一个几百行的“上帝类”,所有属性塞进一个Book类里,借书还书逻辑全部用if-else堆在main方法里。功能是能跑的,但这恰恰是面向对象设计最忌讳的写法。面向对象不是说你用了class关键字就叫面向对象,而是你是否真的做到了“职责划分”和“封装变化”。
回到这道题,核心考点可以拆成四层。第一层是基础语法层面,考察你能不能正确定义类、属性、方法,能不能正确使用访问修饰符,懂不懂构造方法的作用。第二层是关系建模层面,考察你能不能识别对象之间的继承关系、组合关系和依赖关系,会不会把“学生借书”这种动作建模成对象之间的交互,而不是一个全局函数。第三层是设计原则层面,考察你是否知道开闭原则、单一职责原则,能不能用接口或抽象类让系统具备扩展性。第四层是代码质量层面,考察你的命名是否规范、有没有写无意义的getter和setter、有没有考虑异常情况。
很多人在第一层和第二层翻车,不是因为他们不会语法,而是因为他们没有养成“先建模再编码”的习惯。拿到题目直接开写,写到一半发现某个类需要再加一个类型,然后又回去改代码,最后改得面目全非。正确的做法永远是把大头时间花在分析上,代码只是分析的产物。
另外想强调一点,这种题目的考查范围不会太偏,它不会要求你写多线程、不会要求你操作数据库,更不会要求你背设计模式。它考的是你最底层的面向对象思维,而这恰恰是很多自学Java的人最薄弱的地方。网上教程能教会你语法,但很少有人真正告诉你“这个类为什么要这么设计”。
所以我在这篇文章里用的示例,虽然业务场景是图书借阅,但设计思想完全可以直接套用到其他类似题目上。学会了这套分析思路,哪怕考试题变成“银行账户系统”“员工管理系统”“宠物商店系统”,你也能快速找到切入点。
1.1 为什么这道题要用“借阅”场景而不是更复杂的业务
工作里真正带团队做系统,图书借阅这种业务逻辑算是非常简单的了。但拿来作为教学题目恰恰合适,因为简单场景才能聚焦在“面向对象设计”本身,而不是被复杂业务绕晕。题目里会给几个实体,比如图书、读者、借书记录,然后让你实现借书、还书、查询图书状态这些基本功能。
你注意看,这类题的业务逻辑通常只有三到四条规则。比如“普通读者最多借5本”“借阅时间超过30天算逾期”“逾期需要缴纳罚款”。规则不多,但每一条都对应了一个设计决策。借阅数量限制放在哪里?逾期判断是放在图书类里还是放在借阅记录类里?罚款金额怎么计算才方便后期调整?这些看似琐碎的问题,就是面向对象设计的核心。
我自己给学生讲这道题的时候,特别喜欢用一个比喻。面向对象设计就像组织一场小型聚餐。你需要明确谁是主人、谁是客人、谁负责买菜、谁负责洗碗。如果你把所有人做的事全部写在主人的脑子里,这个聚餐虽然勉强能进行,但只要其中一个人临时有事,整个计划就崩了。而好的分工是,每个人都有自己的职责,主人只需要协调,不需要知道炒菜的具体步骤。
类就是责任单位,方法就是能力清单,对象就是具体的参与者。你设计的每一个类,都应该能在现实世界里找到一个对应的角色,并且这个角色的职责是清晰单一的。
1.2 设计思路的总原则:单一职责与依赖倒置
这道题的解题思路其实可以归纳成两个原则的落地。第一个是单一职责原则,字面意思就是每个类只干一件事。图书类只管图书自身的信息和状态,不要让它去管读者的借阅数量;读者的信息归读者类管,图书的库存变化归图书馆类管。这样分下来,每个类的代码都不会太长,调试起来思路清晰,出了问题也能快速定位。
第二个是依赖倒置原则,这句话听起来有点学术,说白了就是“高层模块不应该依赖低层模块的具体实现,两边都依赖抽象”。放到这道题里,就是你设计借阅规则的时候,不要直接去new一个具体的读者类型,而应该依赖一个抽象的读者模型。将来如果新增了“教师读者”或者“VIP读者”,代码的改动量会小很多。
每次我讲到这里都会有人问,就这么一个小题目,有必要搞得这么“重”吗?我的回答是,如果你只是应付作业,确实没太大必要。但如果你是准备面试或者真的想提升编码水平,这种“过度设计”的训练恰恰是有价值的。因为工作里的项目不会永远这么简单,业务一复杂起来,前期设计的好坏直接决定了后期你改代码是想骂人还是想笑。
2. 核心细节解析与实操要点:类设计的关键决策
设计类的时候,最忌讳一上来就打开IDE开始敲代码。哪怕你脑子里已经有了大概的类图,我也建议先在草稿纸上或者注释里把类名、属性、方法名列出来。这个过程相当于建筑施工前的设计图阶段,图纸错了,改起来成本低;施工完了再拆墙,代价就大了。
2.1 先从“名词”里找类,再从“动词”里找方法
我在拿到这类题的时候,有一个非常实用的分析套路。先通读一遍题目描述,把所有名词圈出来,这些通常是候选的类或属性;再把所有动词圈出来,这些通常是候选的方法或业务逻辑。就拿图书借阅来说,题目里出现的名词有图书、读者、借书证、借阅记录、出版社、作者、ISBN号;动词有借出、归还、查询、续借、缴纳罚款。
这里要注意,不是所有名词都要变成类。比如出版社和作者,在当前场景里只是图书的描述信息,那就做成图书类的属性,而不是单独再建一个出版社类和作者类。判断标准很简单,如果这个名词只有数据没有行为,那就先当属性看;如果它有独立的行为或者会被多个对象独立引用,再考虑升级为类。
然后再看动词。借出、归还这些动作,放在哪个类里最合适?很多人习惯性的做法是写一个Library类,把所有操作都放进去。这个做法在功能上没错,但它会慢慢变成一个大杂烩类。更合理的思路是把动作放到跟它关系最密切的对象上。借书这个动作,改变的是读者的借阅列表和图书的借出状态,因此这个动作放在读者类或者图书类上都是合理的,甚至可以拆分到借阅记录类里。
我推荐的做法是,借出和归还逻辑放到借阅记录类里,因为借还动作天然跟“一条记录”绑定。图书类只管自己的状态流转,读者类只管自己的借阅额度。这样做完之后,每个类的职责都非常纯粹,也方便后期维护。
2.2 继承还是组合,这是面向对象设计的第一道坎
面向对象设计里有一个经典陷阱,新人特别容易掉进去,就是“强行继承”。比如为了复用图书的某些属性,搞一个“电子书”类去继承“纸质书”类;为了让学生和老师都有姓名和ID,搞一个“人”基类,然后学生和老师都去继承它。
继承不是不能用,但是要时刻记住判断标准:只有“is-a”关系才适合继承。“学生是一个人”,这句话成立,所以学生继承“人”是合理的。“电子书”和“纸质书”虽然都叫书,但它们的使用方式差异很大,电子书没有库存概念、不需要归还日期,硬把它们放在一个继承体系下就会出现大量用不上的父类方法。
所以在这道题里,我建议把读者设计成继承结构,而把图书设计成组合结构。读者这边,Student和Teacher都是一种Reader,公共属性和方法放进父类,各自差异化行为放到子类。图书这边,Book类直接聚合作者、出版社这些信息字段,不需要搞复杂的继承。
还有一批关系要小心,就是“has-a”组合关系。图书馆拥有书架,书架上摆放图书,图书有借阅记录,这些都是组合关系。组合关系的处理核心是“对象持有另一个对象的引用”,而不是简单地把对方的属性复制过来。
2.3 接口的运用时机:把“能力”和“身份”分开
类之间的继承关系解决了“身份”问题,接口则解决“能力”问题。同一个身份的人可以有不同能力,不同身份的人也可以有相同能力。比如在这道题里,“可以借书”就是一个能力。普通学生可以借书,教师也可以借书,但他们的借阅上限不一样。此时用一个Lendable接口来统一这个能力,让不同读者类分别实现它,就能在保证灵活性的同时约束行为规范。
很多教材讲接口的时候只告诉你“接口不能被实例化”“接口里的方法默认是抽象方法”,但没告诉你为什么要这么设计。我个人的理解是,接口的意义在于给调用者提供一个“最低保障”。只要一个对象实现了Lendable接口,调用者就可以放心地调用它的lend方法,而不用关心这个对象到底是学生还是教师。这种“面向接口编程”的习惯,在工作中的意义比考试大得多。因为实际项目里会出现各种你预想不到的具体类,但只要它们都实现了同一个接口,你的核心业务代码就不需要跟着改。
不过接口的数量也要控制好,不要一个类实现十几个接口,那样反而让系统的关系复杂化。在这道题里,一个Reader接口配合一个Lendable接口就足够了,要时刻提醒自己“够用就行,不要为了设计而设计”。
2.4 封装边界:不是所有字段都要getter和setter
封装是面向对象的基础,也是最容易被做“过火”的地方。现在很多教学代码里,一上来就把所有属性写成private,然后机械地给每个属性配一对getter和setter。这样写不能说错,但它违背了封装的初衷。
封装的真正含义是“控制外部对内部状态的访问方式”,而不是“禁止外部访问”。如果一个字段只是用来描述对象的静态特征,比如图书的ISBN号,它在对象创建之后不应该被修改,那就只提供getter,不提供setter;如果一个字段是内部计算用的中间值,比如借阅逾期天数,那就应该完全对外隐藏,只在类内部使用。
我在阅卷和面试中见过不少这种反模式,一个Book类里把价格写成private double price,然后提供setPrice方法允许外部随意改价。这个设计明显不符合现实逻辑。现实中的图书调价是有规则的、有权限控制的,不是谁想改就能改。正确的做法应该是提供updatePrice方法,在方法内部加上校验逻辑,比如价格不能为负数、调整幅度不能超过某个阈值。这才是封装的意义——把规则放在该放的地方。
3. 实操过程与核心环节实现:从类图到完整代码
这一节我们直接进入完整实现环节。以“图书馆借阅管理系统”为场景,按照前面分析的思路,把代码一步步写出来并解释关键设计决策。这个系统包含的核心功能有:新增图书、注册读者、借书、还书、查询图书借阅状态、逾期罚款计算。
3.1 类设计的最终决策表
在动手写代码前,先花两分钟看看这张类设计表。每一行代表一个类或接口,以及它的核心职责和关键设计依据。这样做的目的是让你养成“先规划后编码”的好习惯,考试的时候也能先在草稿纸上画结构。
| 名称 | 类型 | 核心职责 | 设计依据 |
|---|---|---|---|
| Book | 实体类 | 承载图书静态信息和状态 | 单一职责:只管书自身 |
| Reader | 抽象类 | 定义读者公共属性和行为 | 继承结构:学生和教师的共同特征 |
| Student | 子类 | 学生读者,借阅上限5本 | is-a关系成立 |
| Teacher | 子类 | 教师读者,借阅上限10本 | is-a关系成立 |
| BorrowRecord | 实体类 | 记录每次借还行为和时间 | 把动作与记录绑定,职责清晰 |
| Lendable | 接口 | 约束具备借阅能力的对象 | 面向接口编程,隔离变化 |
| Library | 门面类 | 协调图书和读者的交互 | 对外提供统一入口,内部协调 |
你会发现我并没有设计一个BookStore或者BookManager之类的大杂烩类。所有业务操作被拆解到了最合适的位置,Library只是作为一个“协调者”存在,它不直接处理细节逻辑,而是调用各对象的方法完成任务。
3.2 核心代码实现:一步一步搭起来
先来定义Book类。这里我特意把price字段的setter去掉了,改成updatePrice方法并加上校验逻辑。这就是前面说的“把规则放在该放的地方”。
public class Book { private String isbn; private String title; private String author; private double price; private boolean isBorrowed; public Book(String isbn, String title, String author, double price) { this.isbn = isbn; this.title = title; this.author = author; updatePrice(price); this.isBorrowed = false; } public String getIsbn() { return isbn; } public String getTitle() { return title; } public String getAuthor() { return author; } public double getPrice() { return price; } public boolean isBorrowed() { return isBorrowed; } public void updatePrice(double newPrice) { if (newPrice < 0) { throw new IllegalArgumentException("价格不能为负数"); } this.price = newPrice; } public void borrowBook() { if (isBorrowed) { throw new IllegalStateException("图书已经被借出"); } this.isBorrowed = true; } public void returnBook() { if (!isBorrowed) { throw new IllegalStateException("图书当前未被借出"); } this.isBorrowed = false; } }这个类里有两个地方值得你仔细品味。第一个是构造方法里调用了updatePrice而不是直接赋值,这样等于把所有价格校验逻辑收敛到了一个方法里。第二个是borrowBook和returnBook方法本身带了状态检查,而不是把检查留给调用者。这样设计后,外部代码想错误地借出一本已经被借走的书,会在第一时间收到异常,而不是等业务数据错乱后再排查。
接下来是Reader抽象类和学生、教师两个子类。设计要点在父类里放好公共属性,在子类里覆盖差异化方法。
public abstract class Reader { private String readerId; private String name; private List<BorrowRecord> borrowRecords = new ArrayList<>(); public Reader(String readerId, String name) { this.readerId = readerId; this.name = name; } public String getReaderId() { return readerId; } public String getName() { return name; } public abstract int getMaxBorrowCount(); public List<BorrowRecord> getBorrowRecords() { return new ArrayList<>(borrowRecords); } protected void addBorrowRecord(BorrowRecord record) { borrowRecords.add(record); } protected void removeBorrowRecord(BorrowRecord record) { borrowRecords.remove(record); } public int getCurrentBorrowCount() { return (int) borrowRecords.stream() .filter(r -> !r.isReturned()) .count(); } } public class Student extends Reader { public Student(String readerId, String name) { super(readerId, name); } @Override public int getMaxBorrowCount() { return 5; } } public class Teacher extends Reader { public Teacher(String readerId, String name) { super(readerId, name); } @Override public int getMaxBorrowCount() { return 10; } }getBorrowRecords方法返回了一个新的ArrayList,而不是直接把内部list暴露出去。这个细节是很多教程不会讲的,它的作用是防止外部代码拿到内部list后直接篡改数据。这种做法在业务类里非常实用,我强烈建议你养成这个习惯。
接着定义Lendable接口和BorrowRecord类。BorrowRecord类承载一次借还动作的所有数据,同时也是后面计算逾期罚款的核心。
public interface Lendable { void lend(Book book); void giveBack(Book book); } public class BorrowRecord { private Book book; private LocalDate borrowDate; private LocalDate returnDate; private boolean returned; public BorrowRecord(Book book) { this.book = book; this.borrowDate = LocalDate.now(); this.returned = false; } public Book getBook() { return book; } public LocalDate getBorrowDate() { return borrowDate; } public boolean isReturned() { return returned; } public void markReturned() { this.returned = true; this.returnDate = LocalDate.now(); } public long calculateOverdueDays() { if (!returned) { return 0; } long maxLendDays = 30; long actualDays = ChronoUnit.DAYS.between(borrowDate, returnDate); return Math.max(0, actualDays - maxLendDays); } }Lendable接口单独存在的意义在Library类中会充分体现。BorrowRecord中markReturned方法负责把状态和归还日期一次性搞定,避免外部代码先set状态再set日期这种容易出错的操作。
3.3 Library门面类的实现:把所有操作串起来
到了这一步,剩最后一个核心类。Library类扮演的是门面角色,它对外提供简洁的调用入口,内部把图书操作、读者操作、记录操作串起来。这样做的好处是,调用者不需要知道借书背后涉及哪些类,只需要调用library.lendBook(book, reader)就够了。
public class Library { private Map<String, Book> books = new HashMap<>(); private Map<String, Reader> readers = new HashMap<>(); public void addBook(Book book) { books.put(book.getIsbn(), book); } public void registerReader(Reader reader) { readers.put(reader.getReaderId(), reader); } public void lendBook(String isbn, String readerId) { Book book = books.get(isbn); Reader reader = readers.get(readerId); if (book == null || reader == null) { throw new IllegalArgumentException("图书或读者不存在"); } if (book.isBorrowed()) { throw new IllegalStateException("图书已经被借出"); } if (reader.getCurrentBorrowCount() >= reader.getMaxBorrowCount()) { throw new IllegalStateException("读者已达到最大借阅数量"); } book.borrowBook(); BorrowRecord record = new BorrowRecord(book); ((Lendable) reader).lend(book); reader.addBorrowRecord(record); } }写到这里,你可能发现问题了。我在代码里写了((Lendable) reader).lend(book);这一行,但是Reader类并没有实现Lendable接口。这种情况在真实设计里是常见的权利衡。要么让Reader实现Lendable,要么在Library里去掉这行多余调用。在我这个示例中,既然Lendable接口只是用于约束读者具备借阅能力,其实更合理的做法是让Reader类直接实现Lendable,然后把实现细节放到Reader里。
为了保持代码逻辑的干净,我在设计上做一点修正:让Reader抽象类直接实现Lendable接口,并提供默认的lend和giveBack方法骨架。Student和Teacher继承后不必重复实现。Library中也不再需要强转,直接调用reader.lend(book)即可。这样调整后,类职责更清晰,接口语义更明确。
这里我也要提醒你,写代码时“发现设计有误”是非常正常的事情,不要害怕修改。真正重要的是你能看出问题在哪里,而不是死咬住第一版设计不放手。这种调整能力正是工作里高频使用的技能。
3.4 主类测试:验证设计是否正确
最后写一个Main类跑一遍全流程。能看到数据状态变化是否符合预期,是判断设计好坏的最直接手段。
public class Main { public static void main(String[] args) { Library library = new Library(); Book b1 = new Book("B001", "Java编程思想", "Bruce Eckel", 89.0); Book b2 = new Book("B002", "设计模式", "GoF", 59.0); library.addBook(b1); library.addBook(b2); Student stu = new Student("S001", "小明"); Teacher tea = new Teacher("T001", "王老师"); library.registerReader(stu); library.registerReader(tea); library.lendBook("B001", "S001"); library.lendBook("B002", "T001"); System.out.println("B001 is borrowed: " + b1.isBorrowed()); System.out.println("小明已借数量: " + stu.getCurrentBorrowCount()); System.out.println("王老师已借数量: " + tea.getCurrentBorrowCount()); } }这段测试跑完后,控制台会依次输出B001被借出、小明已借1本、王老师已借1本。到这里,一个完整的面向对象设计题实现就算落地了。相信你也能感受到,整个过程并不恐怖,关键是在编码之前把对象的职责划分清楚。
4. 常见问题与排查技巧实录:这些坑我替你踩过了
这一节聊聊我在刷题和带新人过程中反复遇到的典型问题。每个问题背后都是一次真实的翻车经历,看完能帮你省不少时间。
4.1 一上来就写代码,写到一半发现类设计错了
这是最普遍的问题。没有经过需求分析就直接编码,写了两百行之后发现某个核心类的接口设计不合理,改起来牵一发动全身。解决方法是把编码前的分析时间拉长,强制自己在纸上把类名和关键方法列出来,至少给自己十分钟的“冷却时间”。
4.2 疯狂使用getter和setter,导致业务规则散落各处
这个问题的典型表现是:业务逻辑里到处都是book.getPrice()、reader.getBorrowRecords().add(...)这种代码。表面上你在使用封装,实际上你已经把对象内部数据暴露出来随便改。正确的做法是把操作封装成方法,让调用方不去操作具体数据。
拿价格修改来举例,我见过很多人写book.setPrice(book.getPrice() * 0.8)这种代码。一旦后期要加校验,你就要去全项目里搜setPrice的调用点,改起来极其痛苦。而前面演示的updatePrice方法,把所有校验集中在一个地方,改一处就全局生效。
4.3 搞不清继承和组合的使用边界
这个问题常见于“为了复用代码而继承”的场景。比如两个类有些方法长得像,就立刻让一个类继承另一个类。正确的做法是先看语义关系。一个“老师”和一个“学生”都“是”人,继承没问题。一个“订单”和一个“商品”虽然都有金额字段,但它们不是is-a关系,给订单继承商品就非常离谱。
4.4 接口设计得太碎片化
接口不是越多越好。我见过有人给每个行为都配一个接口,最后类实现了一堆接口,代码里全是强制转型和类型判断。接口的设计应该跟业务能力维度对齐,一个能力维度对应一个接口,并且在大概率上这个能力维度会存在多种实现时才值得抽象。如果只有一个实现类,接口可以先缓一缓,等到真出现第二个实现了再抽离也不晚。
4.5 测试用例里只测正常路径,异常路径一片空白
教材和练习通常只验证“正常情况”能跑通,但面试和真实系统里,异常处理往往更体现水平。比如重复借同一本书会怎样?借书数量超过上限会怎样?图书不存在时会怎样?归还一本未被借出的书会怎样?这些边界情况在我的示例代码里基本都有覆盖,异常信息也写得很明确。你自己写任何系统时都要带着这个思维:不是只有happy path才算完成。
4.6 不知道如何优化已经写好的烂代码
这个经验送给已经开始写工作的朋友。如果你发现原有代码已经是一团乱麻,不要急着推翻重写,更不要在原来基础上继续堆功能。保险做法是先用接口把核心动作抽象出来,再写一个全新的实现逐步替换。每次替换一个方法,跑一遍回归测试。这种“绞杀者模式”比直接重写要安全得多,也是我处理老项目的首选方案。
5. 从这道题延伸出去:面向对象设计能力如何影响真实项目
聊完了具体实现,我想再花点篇幅说说这道题背后的东西。你可能觉得一个图书借阅管理没什么了不起,但它在教学体系里的价值是作为一个“最小可用的业务系统”存在。麻雀虽小五脏俱全,它包含了对象建模、行为划分、状态流转、数据封装、异常处理、扩展预留等真实系统全要素。
5.1 把“设计题”思维套用到真实业务场景
拿我自己做过的存储系统项目来举例。表面上看它跟图书借阅八竿子打不着,但设计逻辑一模一样。客户端上传文件、元数据管理、存储节点分配、状态监控,这些功能对应的就是图书、读者、借阅记录。你同样需要纠结哪些信息是实体属性、哪些是独立实体、操作入口放在哪个层、接口怎么抽象才方便将来扩容。
这也是为什么很多面试官喜欢拿设计题做切入点聊项目。他们不指望你在一道题里展示出多么高深的技术,而是想看你处理问题的底层思路是否清晰。思路清晰的人,给他一个真实业务他也能慢慢拆开;思路混乱的人,哪怕技术栈再熟,进了复杂项目也会把代码写成奇形怪状。
5.2 设计模式在这道题里的应用前哨
面向对象设计学到后期就会接触设计模式,比如策略模式、模板方法模式、工厂模式。虽然这道题没直接要求你写设计模式,但它的很多设计决策已经在为模式打基础。Reader抽象类配合不同子类的getMaxBorrowCount,本质上就是模板方法模式的雏形;Lendable接口配合不同实现,就是策略模式的影子。
如果你准备面试,我建议你在这种题目基础上继续做几个变种练习。比如把罚款计算规则抽取为独立的策略接口,用不同的策略类实现“学生罚款规则”“教师罚款规则”,这就是非常自然的策略模式实战。这个升级过程能让你的设计能力上一个台阶,也远远超出题目本身的要求。
5.3 代码可读性和命名的隐性分水岭
很多时候你的实现逻辑是对的,但读起来非常费劲。原因通常出在命名和代码组织上。变量名叫a、b、tmp,方法名叫doIt、handle、process,类名意义模糊,这些都会让阅读者瞬间丧失好感。你在这种题目里养成好习惯,到工作中受益无穷。我个人的习惯是:类名用名词,方法名用动词开头,布尔方法用is或has开头,变量名不要缩写到一个字母。代码被阅读的次数远远大于被编写的次数,花点心思在命名上绝对不亏。
6. 最后分享一点我自己的实操体会
回到这道题本身。我见过很多人在刷“Java 面向对象设计题”系列的时候,几道题刷下来感觉都会做,但真到了面试手写代码环节还是会卡壳。核心原因就是平时练的时候太依赖IDE自动补全和编译提示,一旦脱离环境就大脑空白。所以我给你的练习建议是,用最原始的记事本或者网上代码白板去写这种设计题,全程不依赖任何提示,逼着自己把每个类的完整代码手写出来。这个过程会暴露很多你以为自己会但实际不会的细节,比如接口定义的语法格式、集合类的引入包名、日期计算的API方法名。多练几次,这些知识才会真正长在你身上。
另外,做完题目之后不要急着丢到一边,给自己加两三个扩展需求。比如给图书增加“预订”功能、给读者增加“续借”功能、给图书馆增加“统计借阅排行榜”功能。每加一个功能,你就需要重新审视原来的类设计是否能轻松扩展。如果发现改动很大,说明前期抽象还有优化空间。这么一轮一轮地打磨下来,你会慢慢形成属于自己的设计直觉,以后再碰到各种设计题都会自信很多。
学东西最快的路径永远是“做出来、用起来、改起来”。如果你想彻底吃透这套,建议你把这篇文章的示例代码手动敲一遍,跑通之后再自己加个扩展功能,改完再回头对照文章看看设计决策是否一致。这样一圈走下来,比你看十篇解析都管用。