1. 面向对象方法学到底在解决什么问题
刚开始把“面向对象方法学引论”当复习资料看的时候,我的真实感受是:理论很多、代码很少,容易看完就忘。后来一边复现课上的类图,一边在真实项目里做重构,才慢慢明白这一段内容在软件工程课程里扮演的位置:它不是在教你几个关键字怎么用,而是帮你建立一套“怎么把真实世界的问题拆成可维护、可演进的软件结构”的思考方式。
四年前我学这门课,只觉得“对象”“类”是概念题;四年后带过几个项目,才发现几乎所有难改的代码,问题都出在对象的关系没有理清楚。所以这篇文章不打算只复述教材,我会把引论部分真正重要的东西挑出来,结合建模案例和踩过的坑展开,适合正在复习的计科/软工同学,也适合自学的初学者。
1.1 从结构化到面向对象:一次职责重分配
早年的结构化方法习惯把系统拆成“功能分解”的树状结构:一个大模块拆成子模块,子模块再拆成函数,数据在模块之间传进传出。这种思路在业务稳定、流程线性的系统里很有效,但系统一大起来,问题就出现了:数据被散放在很多函数里,每个函数都要自己处理数据结构,一旦数据结构变了,所有相关函数都要跟着改。
我举一个很常见的例子。你写一个订单管理系统,最初订单金额是简单数字,所有计算金额的函数都能直接用。后来业务加了一个“会员折扣”,你再从头翻出十几个函数,一个个加判断。如果是小系统倒还好,项目上了规模之后,这种改动会变成灾难,甚至改到后面没人知道某个字段到底在哪里被修改过。
面向对象方法学换了一个切分维度:不再按功能把代码切成一条条流水线,而是把“数据”和“操作这些数据的行为”放在同一个单元里,这个单元就是对象。对象把实现细节挡在后面,对外只提供方法,调用方不需要关心内部数据怎么组织。这样,需求变化的影响范围就从“整个函数调用链”缩小到了“某个对象内部”。
这种转移背后的核心是责任分配。传统方式下,责任散落在过程里;面向对象方式下,责任被明确地分配给对象。比如“打印发票”这件事,客户类不该管,订单类不该管,发票类自己最清楚怎么打印自己的内容。谁拥有数据,谁负责处理数据,听起来简单,但在建模时非常容易被违反。
1.2 对象是数据和行为的“最小稳定单元”
理解面向对象方法学,第一步要建立“最小稳定单元”这个意识。一个对象内部有两个部分:状态和数据,以及行为和方法。状态是一个对象保存的属性值,行为是它对外提供的操作。两者绑在一起,对象才能对自己的数据负责。
可以用冰箱来类比。冰箱有温度、内部食物存量这些状态,也有“设置温度”“开门”“放入食物”这些行为。你使用冰箱时,不会把冰箱主板拆开去直接改温度传感器的读数,你只会按面板。面板就是接口,内部怎么制冷是封装。这个类比虽然朴素,但能解释面向对象里一个重要的东西:对象之间是通过接口协作,而不是互相翻查内部数据。
为什么说它“最小稳定”?因为在业务系统里,需求经常会变,但“订单”“客户”“商品”这些业务实体相对稳定。把系统建立在稳定实体上,比建立在易变流程上更抗折腾。你可以随时调整订单状态的计算规则,却不太可能把订单这个概念从业务里删掉。面向对象方法学正是利用了这一点,用对象来承载变化和稳定性。
当然,这句话不能走极端。你构建出来的是“业务对象模型”,它是否稳定取决于你对业务边界的理解够不够清楚。这也是为什么我始终建议,学这门课要配合真实场景做一遍建模,不能只背概念。
1.3 不要神化方法论:适用场景也要看清楚
面向对象方法学不是银弹。写一个几十行的分析脚本,面向过程往往更直接。你把所有逻辑硬塞进类里,反而会让代码变得更绕、更难阅读。软件工程里讲究“合适的复杂度”,方法本身没有高下,只有适不适合当前场景。
那什么场景适合用面向对象?通常有三个特征:需求变化预期较强、系统生命周期较长、需要多人协作开发。变化强意味着封装有价值,生命周期长意味着可维护性更关键,多人协作则让“对象各管一摊”的边界更有意义。反过来,一次性脚本、原型验证、算法竞赛代码,这些场景如果强行套用面向对象,只会增加不必要的结构负担。
我见过的另一个极端是,项目用了面向对象语言,却只在文件层面把所有函数塞进几个类里,本质上还是过程式代码。这种情况比不学方法论更麻烦,因为代码表面上有对象,实际上没有对象该承担的职责拆分。学习引论的时候,你要特别警惕这种“形式到位,思维没到位”的状态。
2. 七个必须吃透的核心概念拆解
很多人学完面向对象方法学,只记住了四个字:封装、继承、多态。这四个字确实重要,但也是最容易写成死记硬背的地方。你在考试里能把定义默写出来,不等于你在项目里能设计出合理的对象模型。我按自己走过的一些弯路,把这部分拆成七个要点,每个要点都尽量说清楚“是什么”和“为什么这么设计”。
2.1 对象和类:先分清“图纸”和“房子”
对象和类的关系,最经典的比喻是图纸和房子。类是一张图纸,规定了房子有哪些房间、门窗、面积;对象是按照图纸盖出来的具体房子,每一栋都有自己的实际地址、装修和其他状态。你可以在同一张图纸下盖十栋不同的房子,它们共享结构定义,但各自独立地存在。
具体到代码里,Student是类,指向某个具体学生的变量是对象。对象拥有自己的一份属性值,比如姓名、学号、成绩,修改一个对象的属性不会影响同类的另一个对象。类还负责定义这些对象能提供什么操作方法,例如getGPA()或updateProfile()。
这个区别为什么重要?因为你在分析需求时,先找的是类,还是对象,会影响建模质量。我见过有同学在类图里把“张三同学”画成一个类,又把“李四同学”画成另一个类,这是明显把实例当成了类。正确做法是先抽象出“学生”这个类别,再让具体学生成为它的对象。反过来,如果某个概念永远没有多个实例,比如配置中心、日志管理器,那它往往应该用单例或静态成员处理,而不是放一堆对象做重复劳动。
2.2 封装:隐藏变化,而不是把字段全部设为private
封装最容易被人误读成“把属性私有化,然后加getter/setter”。如果只是这样做,你只是给每个字段装了一扇门,并没有真正隐藏什么。封装的本质是隐藏变化,把可能变化的内部实现藏起来,让外部依赖一个稳定的接口。
我举一个真实例子。一个会员系统里,早期判断用户是否活跃,直接查lastLoginTime这个字段。后来业务规则变了,活跃的定义变成“7天内有登录且完成过至少一笔交易”,你如果让调用方直接读lastLoginTime,所有判断逻辑都要跟着改。但如果你封装成isActive()方法,外部只需要调用这个方法,规则再怎么变,调用方代码都不用动。这才是封装能带来的核心价值。
把这段经验翻译成面向对象方法学的语言:对象内部的状态表示可以随时调整,但对象对外暴露的行为契约要保持稳定。不要让你的对象像一个透明鱼缸,任何外部代码都能看到每一块内部石头,那样的系统一旦数据字段变化,全项目都会震动。你在课程设计里体会不到这种痛,工作以后才会发现,真正有价值的封装都是为“未来可能要变的东西”留出缓冲。
2.3 继承:表达“is-a”还是为了复用代码
继承是在面向对象课程里被吐槽最多、也最难拿捏的概念。很多初学者习惯了用继承去复用类里的公共代码:两个类里有重复字段,就抽一个父类出来。这样做虽然能用,但往往会让两个本来没有本质关系的类产生强耦合,后续修改父类时,子类莫名其妙变得不稳定。
继承的正确出发点应该是“is-a”关系:子类确实是父类的一种,比如轿车是车辆的一种,管理员是用户的一种。如果你只是想让A复用B的方法,那组合或委托通常比继承更合理。比如说,打印机需要日志功能,你让打印机继承日志类,语义上很别扭;换成打印机内部持有一个logger对象,既自然又灵活。
里氏替换原则是判断继承是否合理的好标准:任何能用父类对象的地方,换成子类对象也应该能正常工作。如果你写了一个长方形父类和一个正方形子类,正方形重写了长宽逻辑,导致父类的方法在子类上行为异常,这个继承模型就有问题。课程里可能会考这个例子,但更重要的是形成这种自查意识。
还有一点,继承层级不要堆太深。经验上超过三层就要提高警惕,因为每一层都可能引入新的状态和行为,子类越来越难被理解。面向对象方法学讲继承,不鼓励你建一个庞大的继承森林,而是鼓励你只在概念关系清晰的地方使用它。
2.4 多态和动态绑定:同一消息,不同应答
多态是面向对象语言最有魅力的机制,它让同一个方法调用在不同对象上有不同行为。最常见也最好懂的代码是动物叫:
Animal a = getRandomAnimal(); a.speak();如果a实际指向Dog,speak()会让你听到“汪汪”;如果指向Cat,则是“喵喵”。调用方不需要写一堆if (a instanceof Dog)来判断具体类型,只需要依赖Animal这个父类型提供的speak()接口。
这种机制背后的关键是动态绑定。编译时a的类型是Animal,运行时却会找到实际对象所属类的speak()并调用它。这也是“多态”和“重载”最本质的区别。重载是静态的,编译器根据参数个数和类型决定调哪个方法;重写是动态的,运行时才根据对象实际类型决定调哪个方法。考试里经常用这两个概念迷惑人,你只要记住“重载看编译类型,重写看运行类型”就不会掉坑。
多态和接口配合,能做出非常优雅的扩展方式。比如支付模块定义了PayService接口,支付宝、微信、银行卡都实现这个接口。新增一种支付方式时,只需要新增一个实现类,业务层调用代码完全不需要改动。这就是面向对象方法很看重“对扩展开放,对修改关闭”的原因。你在写小型作业时可能感受不深,但一旦系统有十几个支付渠道,这个优势会非常明显。
2.5 消息传递与协作:“告诉对象做什么”而不是“取出数据自己做”
面向对象方法学里有个看起来很玄的词:消息。其实消息就是对象之间的方法调用。你调用order.calculateTotal(),本质上是给order对象发送一条“计算总额”的消息,由它自己决定怎么算。这个视角和过程式编程有微妙差别:过程式关心的是“我调用一个函数,拿到结果”,面向对象关心的是“我把责任交给哪个对象”。
那么在实际建模里,这个差别会带来什么改变?它会改变你组织业务逻辑的位置。比如结算订单时,你如果到处从customer、cart、item里取数据自己算,逻辑会散落在各处,而且耦合很高。更面向对象的做法是让订单对象自己持有明细和客户信息,提供一个getTotalPrice()方法,把计算规则收拢在订单内部。这种“告诉我做什么,不要翻我的兜”的风格,业界也叫“德墨忒尔法则”或“最小知识原则”。
我在评审新人代码时经常发现,很多初级开发者会把对象当成数据容器,把所有逻辑都写在外层工具类里。这也不是完全不行,但业务复杂后,外层会变成一个上帝类,什么都知道,什么都管。面向对象方法学想传递的是:对象的协作是常态,但每个对象都要守住自己的职责边界。
2.6 抽象类和接口:类型契约的两种写法
抽象类和接口很容易混淆。抽象类是一个“不能直接实例化的类”,它可以有字段、有构造方法、有已经实现的方法,也可以有让子类必须实现的方法。接口更纯粹,它通常只描述“能做什么”,不关心内部状态,比如一个Flyable接口定义fly(),谁来实现都可以。
选择一个方案的标准,主要看你要表达什么。抽象类适合表达“同一家族的共享骨架”,比如多个交通工具类都有车牌、速度、启动方式,你可以在抽象类里写默认实现,子类继承后再补差异。接口适合表达“跨家族的行为契约”,比如Comparable可以被任何类实现,你不需要让它们拥有共同的父类。一个类只能继承一个抽象类,却可以实现多个接口,因此接口在扩展性上更灵活。
语言层面会有区别,但方法学的思维是通用的。比如Go语言里没有传统的“继承”,接口反而是核心组织方式;C++里抽象类直接承担接口角色。课程讨论时不需要死记某个语言的规则,但要理解接口为什么能解耦:调用方只依赖于抽象能力,不依赖具体类。
我个人的建议是,拿不准的时候优先考虑接口,等发现明显需要共享字段和公共方法模板时,再上抽象类。这样设计出来的模块更容易被替换和测试。当然,接口也不是越多越好,接口爆多、粒度太碎,会让系统变得像拆碎的零件,反而不好拼装。
2.7 关联、聚合、组合:对象之间关系的强弱
很多同学画类图时只会在两个类之间画一根线,却不区分线表示什么,这是建模的大忌。关联、聚合、组合的强弱不同,对应的生命周期和代码实现也不同。
关联是最弱的关系,表示“一个对象知道另一个对象的存在”,比如老师给学生发通知,老师在学生列表里保存了学生引用。聚合是整体和部分的关系,但部分可以独立存在,比如部门和员工,员工被调岗后部门消失,员工还在。组合是更强的整体-部分关系,部分不能脱离整体,比如订单和订单项,订单被删除,订单项也没有意义了。
区别这三者,直接关系到代码里的生命周期管理。组合关系通常意味着容器销毁时要考虑成员对象的清理;聚合关系则允许成员对象被别的整体共享。课程建模题里经常让考生判断“公司和员工是什么关系”,正确答案通常是聚合,因为员工换公司很常见,人和公司的生命周期并不绑定。
还有“导航性”这个词也要理解。它表示一个对象是否持有另一个对象的方向,是单向还是双向。双向引用在代码里实现起来更麻烦,也容易造成循环引用。建模时不妨先画单向,除非业务确实需要双向。理清这些关系,比会背定义更能提升模型质量。
3. 从需求到类图的建模实操:图书借还系统为例
面向对象方法学的引论部分,最终要落到分析建模能力上。光看概念做不出好设计,真正上手画一次类图,你才能知道课本里的名词是怎么配合工作的。这里我用一个非常经典的图书借还系统当例子,完整走一遍从需求到类图的流程。
3.1 建模基本流程和产出物
一个合理的入门建模流程可以分成四步。第一步是做用例分析,搞清楚有哪些角色、哪些场景。图书馆系统里有读者、管理员,场景包括借书、还书、查询借阅历史,这些场景都要先列出来。第二步是找候选对象,通常从需求描述里抓名词,比如“书”“读者”“借书记录”,同时抓动词来识别行为,比如“借出”“归还”“查询”。第三步是整理每个对象的属性和方法,把职责分配给对象。第四步才是画类图,把对象之间的关联、聚合、组合和多重性标注出来,作为设计文档。
这四个步骤不是一次完成就结束的。你画完类图再检查场景,往往会发现某个对象少了方法,或者某个关系标错了方向。所以建模本质上是螺旋式的,不是流水线式的。我建议在正式画图之前,先用CRC卡片法,也就是类-职责-协作者卡片,快速做一轮职责分配。每张卡片写一个类名、它该负责的事情、它需要和其他哪些类协作。这种方法很土,但对初学者理清思路非常有效。
3.2 图书借还场景的类图设计过程
现在看具体需求:读者能查询图书、借书、还书;管理员负责登记借出和归还;每本图书有“可借”“已借出”状态;系统需要保留借阅历史,方便日后查询。
按名词候选,第一轮先提取出三类:图书、读者、借书记录。图书类可以叫Book,属性有图书编号、书名、ISBN、借出状态。读者类叫Borrower,属性有读者编号、姓名、联系方式。借书记录这个类最容易漏掉,它正是解决“读者和图书是多对多关系”的中间类。一个读者可以借多本书,一本书在不同时期可以被不同读者借阅,两个实体不能相互直接保存,否则历史记录会乱。
我设计类的要点是这样:
Book - bookId: String - title: String - isbn: String - status: String // AVAILABLE / BORROWED ------------------------------ + isAvailable(): boolean + borrow(borrower, record): boolean + returnBack(record): boolean Borrower - borrowerId: String - name: String - phone: String ------------------------------ + borrowBook(book, record): void + returnBook(book, record): void + queryHistory(): List<BorrowRecord> BorrowRecord - recordId: String - borrowDate: Date - dueDate: Date - returnDate: Date ------------------------------ + markReturned(): void这里要注意一点:Book.borrow()内部不能只改status,它还要和借书记录配合。实际代码里,借书过程通常是先创建BorrowRecord,再把图书状态改成不可借。如果顺序反了,系统就可能出现“书记录已生成但状态还是可借”的中间状态。这就是对象协作里很典型的细节。
画类图时,Book和BorrowRecord之间是1对0..*关系,一本图书可以对应多条借书记录;Borrower和BorrowRecord之间也是1对0..*关系。这样多对多就变成了两个一对多关系。多重性看起来只是一个小符号,但它决定了数据结构怎么设计。很多人在建模题里忽略多重性,这是会扣大分的。
3.3 识别对象关系时最容易犯的错
我见过最典型的问题是,把“方法参数”当成了关联。比如Book.borrow(Borrower, BorrowRecord)里虽然出现了Borrower类型,但这不能说Book和Borrower有持久关联。关联关系必须是对象结构上长期存在的关系,临时传参不构成类图上的关联。
第二个典型错误是把所有整体-部分关系都画成组合。部门与员工一旦画成组合,就表示员工不能脱离部门存在,这和现实不符。组合会带来生命周期上的严格限制,画图前先问自己:“部分离开整体后,还有没有独立存在的意义?”没有意义才是组合,有意义就是聚合。
第三个错误是从实现角度出发去定义双向引用。建模阶段你可能会想,读者查询历史需要知道借书记录,借书记录也需要知道读者,于是干脆画成双向关联。但双向关联会带来代码上的同步问题,比如构造时两个对象互相指来指去。更好的做法是先判断哪个方向是必需的,再考虑是否要精简成单向。模型不是类图越丰富越好,而是能支持业务场景又不过度复杂才好。
3.4 把模型映射到代码:一张对照表
模型画完,代码怎么写?很多同学到了这一步就忘了模型,直接按手感写类,结果实现和设计脱节。下面这张对照表,是我个人在做设计到实现转换时常用的参考:
| 模型元素 | 代码对应 | 注意事项 |
|---|---|---|
| 类 | 类或结构体 | 类名、职责在实现层保持一致 |
| 属性 | 字段或属性 | 注意可见性,不要随意暴露内部状态 |
| 操作 | 方法或函数 | 尽量通过方法修改属性,避免裸操作 |
| 关联 | 引用、集合、数组 | 根据多重性选择List或Set |
| 聚合 | 弱引用关系 | 部分对象生命周期独立 |
| 组合 | 强拥有关系 | 容器清理时要考虑部分对象释放 |
| 继承 | 继承或子类 | 只用于is-a关系 |
| 接口 | 接口或协议 | 表示能力契约,可跨类型实现 |
实际操作里,类图里的一对多关联,实现时通常会体现为“持有集合字段”;一对一关联则体现为“持有单个对象引用”。如果类图上标注了一个业务唯一性约束,例如一本书在同一时段只能有一条未归还记录,代码里就要考虑查询碰撞和并发控制,这些在模型里不会直接显示,但会影响实现方式。
建模和编码之间的关系不是单向的。你写完代码发现需求设计漏了一个状态,再回头改类图,这是很正常的。面向对象方法学并不要求一次建模就百分百完美,它更强调模型和代码之间保持可追溯性,让设计文档能反映实现的核心结构。
4. 新手常见困惑、复习重点与避坑清单
这一部分我觉得比前面的理论更值得反复看。因为我见过太多同学,概念都会背,一到写类或画类图就卡壳。下面梳理几个最高频的困惑,对应给出能直接上手的解决思路。
4.1 学习时最常见的五个困惑
| 困惑 | 根源 | 解决思路 |
|---|---|---|
| 类和对象分不清 | 没有理解实例化 | 类相当于模板,对象是模板生成的实例 |
| 接口和抽象类选哪个 | 混淆“能力契约”和“公共模板” | 跨家族能力用接口,共享状态与默认方法用抽象类 |
| 抽象类为什么不能实例化但可以有构造方法 | 不明白继承初始化顺序 | 抽象类构造方法用于子类实例化时初始化父类部分 |
| 静态属性和多态是什么关系 | 混淆“属于类”和“属于对象” | 静态成员属于类,静态方法按编译期类型解析,不参与动态绑定 |
| 方法调用为什么叫消息 | 用过程式思维理解面向对象 | 消息强调接收者对象自行决定如何响应,而不是由调用方安排流程 |
这里我重点说下“抽象类为什么不能实例化但可以有构造方法”。因为抽象类本身不是一个完整可用的对象,但它定义的字段和公共方法需要在子类被创建时完成初始化。你创建子类对象时,JVM会先调用父类构造方法,把父类那部分状态准备出来。这个概念在考试里出现过很多次,也是理解继承层级的关键。
4.2 编程练习中的多态、继承避坑清单
编程练习是检验你是否真懂面向对象方法学的重要途径。我这里给几条具体避坑经验。
第一,千万不要把静态方法当成多态方法来用。看这段代码:
class Parent { public static void who() { System.out.println("Parent"); } } class Child extends Parent { public static void who() { System.out.println("Child"); } } Parent p = new Child(); p.who(); // 输出 Parent,不是 Child这里输出是Parent,因为静态方法在编译期就绑定到了Parent类型。如果你想让行为动态切换,必须把方法改成实例方法,并加@Override。这个问题很多工作两三年的开发都会踩,根源就是没分清静态绑定和动态绑定。
第二,不要在构造函数里调用可被重写的方法。比如父类构造方法里调用了init(),子类重写了init(),而且子类有自己的字段还没初始化,这时候调用init()会读到空值或默认值,很容易出bug。面向对象方法学讲继承时,虽然不会特意讲语言细节,但你在编程练习中遇到这类问题,说明对继承的实现机制还不够熟。
第三,组合优先于继承,不是口号。如果两个类有重复代码,先想想能不能抽一个公共的辅助类,而不是直接抽父类。一旦你习惯继承,父类会不断膨胀,各种子类被不该有的方法污染。组合是让一个对象持有另一个对象,通过接口调用能力,二者的耦合更松,测试也更容易写桩。
第四,注意集合类型和多重性的匹配。一对多关系如果写成了List,就得考虑是否允许重复、是否有顺序要求;如果不允许重复但用了List,每次插入还得先查重,不如直接选Set。这些细节看起来和“引论”无关,但类图里的多重性最终就是这么落地的。
4.3 课程考试常考题型与答题思路
面向对象方法学引论在考试里常见四类题目。名词解释题重点在对象、类、封装、继承、多态、消息、关联、聚合、组合这组术语;判断题经常把“继承主要用于代码复用”“重载是动态多态”这类说法拿出来让考生辨别;简答题会问“面向对象方法为什么更适合需求变化频繁的系统”这类开放题;建模题最重,往往给一段需求描述,让考生画类图并说明关系。
答题时我建议按四个层次组织:先把候选类列出来;再为每个类分配属性和方法;然后标注类之间的关系和多重性;最后用一句话说明关键设计理由,比如“借阅记录作为中间类,用于拆解读者与图书的多对多关系”。阅卷时老师看重的是你有没有对象建模的思维,而不是代码写得有多花哨。
名词解释不要只背书上的定义。可以加一句“封装隐藏了内部实现细节,降低了修改传播的风险”,这说明你理解了方法的动机。开放题也不要写成口号,要结合对象的特点来论证,比如“对象将数据和行为绑定,使变化局部化”“对象之间的消息传递使得模块依赖稳定,扩展时无需修改调用方”。这些才是面向对象方法学真正强调的价值点。
4.4 把方法论用到你手头的项目里
如果你不是马上要考试,而是平时做课程设计或者自己做一些小系统,我强烈建议你拿出一个已有项目来做一次“面向对象审计”。方法很简单:打开项目,找出最核心的十个类,把它们的类图画出来,再问几个问题。是否有某个类又臭又长,承担了太多职责?是否有两个类明明只是复用代码,却用继承强行绑定?是否有方法调用链反复通过getter拿数据自己算逻辑?
我以前帮人维护过一个报表模块,代码有几千行,核心类叫ReportManager,几乎什么方法都有,从查询数据、格式化、导出文件到发送邮件全在一个类里。我把它拆成ReportDataProvider、ReportFormatter、ReportExporter、ReportMailer之后,改动时再也不用担心改导出影响查询。这个重构思路并不难,难的是你要敢于按对象的职责重新切一刀。
对你现在学习这门课而言,哪怕不重构,只做审计也会很有帮助。你会慢慢发现,自己写过的代码里有不少类其实是“贫血类”——只有getter和setter,没有任何业务方法;你也会发现,某些地方明明可以定义一个接口,却写成了一大串if分支。这些都是面向对象方法学和实践之间最好的连接点。
这一章我复习了三轮,每一轮感受都不一样。第一轮背概念,第二轮画类图,第三轮回头看成型项目。如果你正准备考试或者刚开始接触这部分内容,我的建议很简单:别停在概念上。打开一个你熟悉的系统,找出其中五个核心类,把它们的关联画出来,再想想这个设计为什么这样存在。折腾完这一步,面向对象方法学才真正开始为你工作。