Java面试必考:面向对象设计题思路与图书借阅系统实现
发布时间:2026/9/29 17:16:07来源:尧图网络
看到这个标题我第一反应是挺亲切的。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 ListBorrowRecord 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 ListBorrowRecord 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 MapString, Book books new HashMap(); private MapString, 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方法名。多练几次这些知识才会真正长在你身上。另外做完题目之后不要急着丢到一边给自己加两三个扩展需求。比如给图书增加“预订”功能、给读者增加“续借”功能、给图书馆增加“统计借阅排行榜”功能。每加一个功能你就需要重新审视原来的类设计是否能轻松扩展。如果发现改动很大说明前期抽象还有优化空间。这么一轮一轮地打磨下来你会慢慢形成属于自己的设计直觉以后再碰到各种设计题都会自信很多。学东西最快的路径永远是“做出来、用起来、改起来”。如果你想彻底吃透这套建议你把这篇文章的示例代码手动敲一遍跑通之后再自己加个扩展功能改完再回头对照文章看看设计决策是否一致。这样一圈走下来比你看十篇解析都管用。
网站建设高端定制企业官网