我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

面向对象编程从思维到落地:类的设计、封装继承多态与单元测试实践

面向对象编程从思维到落地:类的设计、封装继承多态与单元测试实践 直接说结论面向对象这套东西绝大多数人学了语法没学会思想。类、对象、封装、继承、多态每个词都能背但一到真实项目里就乱成一锅粥——类设计得跟数据结构似的继承用得跟复制粘贴似的测试更是压根不知道从哪下手。我这些年看过太多这种代码也重构过太多这种代码。这篇文章我就拿分析、设计、测试这三个环节当主线把面向对象的基本概念重新捋一遍该给的代码给代码该说的人话说人话讲完你至少能明白一件事面向对象不是让你把代码写得更花哨而是让你在需求变来变去的时候不至于把整个项目推倒重来。1. 为什么学了语法还是不会面向对象——思维层面的三道坎1.1 从我到它把执行者换成对象面向过程和面向对象最本质的区别不是语法是你在写代码的时候脑子里想的是谁。面向过程的思维是我我要读文件、我要算工资、我要存数据库代码是一连串指令的堆叠数据和操作分开存放。你写C语言的时候结构体存数据函数处理数据数据和处理数据的方法天然是分离的。这种思维不是错但它有个问题当业务规则复杂到一定程度操作和数据分离会让代码的可维护性急剧下降。举个例子一个员工工资计算函数里面要访问员工结构体的十几个字段一旦字段含义调整所有访问它的函数都要跟着改。面向对象的思维是它我创建一个员工对象员工自己知道自己怎么算工资外部只需要调用一个方法。数据和操作被绑定在同一个对象里这个对象对外暴露的是行为隐藏的是内部状态。我见过太多人用Java写代码类里面全是public字段外面一堆静态方法操作这些字段——这本质上是披着面向对象外衣的面向过程。判断标准很简单把这段代码里所有对象都改成结构体如果改动量不大说明你根本没用到面向对象。1.2 从数据结构到类数据和操作不应该分家很多人设计类的时候习惯先把数据成员列出来再想方法。这个顺序其实是反的。正确的思路是先考虑这个对象在系统里承担什么责任、对外提供什么行为然后根据这些行为反推需要哪些数据。数据是实现细节行为才是本质。比如设计一个订单类。如果你先想数据你可能列出一堆字段订单编号、客户名称、商品列表、总金额、创建时间、支付状态……然后发现后续每个方法都在操作这些字段。但如果你先想行为思路会变成订单能计算总金额、订单能验证是否有效、订单能生成支付单——有了这些行为你再反推会发现总金额这个字段其实是冗余的它可以从商品列表动态计算出来。**数据驱动设计和行为驱动设计的区别直接决定你写出来的类是数据容器还是真正的对象。**我见过太多业务类从头到尾只有getter和setter这样的类在系统里就是个移动的数据包毫无封装可言。后面讲到设计阶段我会细说怎么判断一个类的职责是否合理。1.3 从写出来到设计出来先建模再编码第三个坎是最难的。初学者拿到需求第一反应是需求里有几个功能我就写几个方法然后直接开写。老手拿到需求第一反应是需求里有哪些角色、哪些规则、哪些会变化的地方先在脑子里或者纸上建立一个模型再动手写代码。这就是分析和设计在面向对象里为什么要放在编码前面。分析阶段搞清楚这个系统到底要做什么设计阶段搞清楚用哪些类、这些类怎么协作来实现编码反而是最后一步。所以这篇文章的结构就按这三个环节走先把基本概念讲透然后按分析、设计、测试的顺序告诉你每个环节到底要干什么、怎么干、常见坑在哪。你现在看到的第1章其实就是个引子——思维转不过来后面全是空中楼阁。2. 面向对象四大基本概念这次用大白话讲清楚2.1 类与对象图纸和实物的关系这个概念被说了无数遍但很多人只是记住了类比没真正理解它的使用场景。类Class是定义对象Object是根据定义创建的具体实例。用Python写个最简单的例子class Dog: def __init__(self, name, breed): self.name name self.breed breed def bark(self): return f{self.name} says woof! dog1 Dog(旺财, 金毛) dog2 Dog(来福, 柯基)Dog是类dog1和dog2是对象。同一个类可以创建出无数个对象每个对象有自己的状态name、breed不同但共享同一套行为bark方法。这个理解本身不难难的是实际应用。我经常让人做一个练习把你手上任何一个业务实体——不管它是数据库里的一张表还是第三方API返回的一个JSON结构——改写成类并且给它加上行为。绝大多数人会把类的字段写得和数据库表的字段一模一样方法只有getter/setter。这暴露了一个深层问题**把类当成数据库表的映射是面向对象设计中最常见的错误。**数据库表是存储模型类应该反映的是业务模型两者有关联但绝对不能划等号。正确的做法是数据访问层做表和对象的映射业务层用对象来表达业务规则。2.2 封装不是简单的private而是契约封装这个概念被误解得最严重。很多人认为封装就是把字段设为private然后提供public的getter/setter——如果只是这样封装其实就是个形式主义因为外部照样可以随心所欲地修改对象的内部状态。封装的本质是对外提供契约对内隐藏实现。外部调用者不需要知道对象内部怎么存储数据、怎么计算只需要知道调什么方法、传入什么参数、得到什么结果。合理的设计是让外部不能也不需要直接操作内部状态。举个例子。一个账户类public class Account { private double balance; public void deposit(double amount) { if (amount 0) { throw new IllegalArgumentException(存款金额必须为正数); } this.balance amount; } public void withdraw(double amount) { if (amount 0) { throw new IllegalArgumentException(取款金额必须为正数); } if (amount this.balance) { throw new IllegalStateException(余额不足); } this.balance - amount; } public double getBalance() { return balance; } }这里balance字段是private但没有提供setBalance方法。外部不能直接把余额改成100万必须走deposit和withdraw方法——这就是把规则焊死在类里面了。如果你只提供getter/setter那和直接公开字段没本质区别校验逻辑全得写在外部每个调用方写一遍迟早出bug。封装还有一个被忽略的好处**它是后续重构的缓冲垫。**只要方法签名不变内部怎么改都不影响外部调用者。你今天用double存余额明天改成BigDecimal外部代码一行不用动。没有封装这种改动会牵一发动全身。2.3 继承is-a关系的正确打开方式继承是面向对象里被滥用最严重的一个概念。我见到的代码里一半以上的继承关系是错的。判断是否该用继承的标准只有一条**子类是否严格地是一种父类。**狗是一种动物所以Dog继承Animal没问题。但是用户和订单之间不是is-a关系不应该继承管理员和用户倒是is-a关系可以继承。但即使满足is-a关系继承也有代价。它是最强的耦合方式子类一旦继承了父类就对父类的所有行为负有责任。父类改一个方法所有子类都可能受影响这就是所谓的脆弱的基类问题。用Java写一个合理的继承例子public abstract class Animal { protected String name; public Animal(String name) { this.name name; } public abstract void speak(); public void introduce() { System.out.println(I am name); speak(); } } public class Dog extends Animal { public Dog(String name) { super(name); } Override public void speak() { System.out.println(Woof!); } } public class Cat extends Animal { public Cat(String name) { super(name); } Override public void speak() { System.out.println(Meow!); } }注意到这里speak是抽象方法子类必须实现。父类只定义了动物能说话这个契约但不规定怎么说——这就是继承和抽象配合的正确用法。**如果你的父类没有抽象方法或者可覆写的方法那你用继承八成是错的。**大概率应该用组合把父类改成成员变量。组合组合一个类、委托调用比继承灵活得多这个观点在任何一本讲设计模式的书里都会被反复强调。2.4 多态面向扩展开放的关键多态是和继承紧密配合的。多态的意思概括成一句话同样的方法调用在不同对象上有不同的行为表现。上面的例子就是典型的多态。introduce方法调用了speak()但实际执行的是Dog还是Cat的speak版本取决于当前对象到底是什么。这个能力让代码可以针对抽象编程而不需要关心具体类型。多态真正的威力体现在扩展性上。假设你原来只有Dog和Cat现在要加一个Birdpublic class Bird extends Animal { public Bird(String name) { super(name); } Override public void speak() { System.out.println(Tweet!); } }创建Bird对象并调用introduce现有代码一行都不用改。如果你用的是面向过程思维通常在代码里写满了if-else判断类型每加一个新类型就要改判断逻辑多态让你不用这么做。面向对象设计里那句著名的话说的就是这个意思**对扩展开放对修改关闭。**多态就是实现这个原则的最基本武器。不过有一点要提醒多态不是万能的它要求各个子类在逻辑上确实属于同一抽象层级。如果两个类之间根本没有is-a关系只是为了复用代码硬粘到一起那么多态的优势一点都发挥不出来反而让代码变得更绕。3. 分析阶段从需求文本里提炼类的实操方法3.1 名词法从需求文档里划出候选类面向对象分析OOA的目标是搞清楚系统要做什么产出物通常是领域模型而不是代码。最基础的提炼类的方法业内叫名词法也被称为文本分析。操作步骤非常朴素拿到需求文档通读一遍把所有名词圈出来。把这些名词分为三类明显的类、明显的属性、无关紧要的说明性词汇。再看看动词动词一般对应方法或者对象之间的交互。把候选类整理成一张清单再根据业务常识合并、删减。举个例子假设有这么一段需求电商系统支持用户浏览商品、加入购物车、提交订单。订单包含订单编号、商品明细、总金额、收货地址、下单时间。支付成功后系统自动通知商家发货用户可以在订单列表里查看订单状态。圈出名词用户、商品、购物车、订单、订单编号、商品明细、总金额、收货地址、下单时间、商家、订单列表、订单状态。然后归类明显的类用户、商品、购物车、订单、商家属性候选订单编号、总金额、收货地址、下单时间、订单状态、商品明细这个需要斟酌也可以单独成类不合适的候选订单列表这是界面上的一块区域不是业务概念名词法看起来简单但它确实是一个起步工具。做完这一步你只是有了候选类清单类与类之间的关系、每个类有哪些行为、系统有哪些规则这些还需要进一步分析。我自己用得比较多的思路是**把名词法和场景法结合。**名词法找实体场景法找行为线。把需求里描述的主要业务流程比如下单流程一步步走下来每一步涉及哪些对象、触发什么操作、产生什么数据变化这些信息比单纯圈名词更准确。3.2 领域模型图不写代码也能验证设计分析阶段可以画领域模型图domain model它关注的是业务概念不是技术实现。很多刚接触的人会把领域模型图和数据库ER图搞混——领域模型图的目的是描述业务概念之间的关系ER图描述的是数据表的存储关系。画领域模型图的时候画的是业务实体的概念模型。它帮你在写代码之前就发现概念划分的合理性。比如订单和商品之间是多对多关系需要一个中间概念订单项把某个商品以某个价格、某个数量放进某个订单——如果一个下单需求里没有订单项或者类似的概念画图的时候就会卡住这个卡住本身就是分析阶段应该发现的问题。这里顺带说个实用技巧。我习惯用最朴素的UML类图画领域模型一个方框写类名下面几行写关键属性类之间画一条连线标上1对多、多对多这些关系。工具用白板、纸上画都行甚至一张数学稿纸就够。关键不在于画得规范而在于让你自己想清楚系统里到底有哪些业务概念它们之间怎么关联。3.3 分析阶段最常见的两个错误第一个错误是**看到需求就想表结构**。这个我见得最多尤其是做过数据库设计的程序员。拿到需求文档脑子里先跳出来的是用户表、订单表、商品表、订单明细表——这跳过了分析阶段直接进入数据建模了。结果是领域模型完全跟着数据库表走业务规则散落在各个Service方法里面向对象的优势全丢了。正确做法是先不考虑数据库表怎么设计先考虑业务概念怎么划分。表结构是物理存储的映射不是业务规则的载体。第二个错误是**类划分跟着演员表走**。比如需求里提到管理员、用户、超级管理员、游客就每个角色设计一个类。这是把角色和领域对象搞混了。角色通常是一个用户的不同身份更合理的做法是用户有一个角色属性或者用权限模型去处理而不是设计一套继承体系。判断分析是否到位的标准很简单把这个模型讲给一个完全不懂技术的业务人员听他能听明白这个系统有哪些概念、这些概念什么关系。如果他能听明白说明你的分析是准确的如果他听不懂说明你被技术细节带跑了。4. 设计阶段类关系与职责分配4.1 类之间六种关系怎么判断更清楚分析阶段产出了领域模型设计阶段就要把它转成可落地的类结构。类之间一共有六种关系继承、实现、关联、聚合、组合、依赖。搞不清这六种关系是设计阶段代码混乱的重要原因。我用最直白的判断标准写个表格关系判断标准UML符号典型例子继承A是一种B吗空心三角形实线狗继承动物实现A实现了B定义的规范吗空心三角形虚线ArrayList实现List接口依赖A用到了B但B不是A的成员吗虚线箭头Service依赖工具类关联A和B平级互相认识吗实线箭头老师知道学生聚合A整体包含部分但部分可以离开整体吗空心菱形实线班级包含学生学生可以转班组合A整体包含部分部分离开整体没意义吗实心菱形实线订单包含订单项订单删除订单项也删其中最容易混淆的是聚合和组合。这个没啥玄乎的判断标准就一条**部分离开了整体还有独立存在的意义吗**有就是聚合没有就是组合。聚合和组合实际上都是整体-部分构造都靠成员变量来实现区别只在生命周期的紧密程度。比如俱乐部包含会员会员离开俱乐部照样活得好好的这就是聚合订单包含订单项订单没了订单项就没有任何意义这就是组合。很多人在实际编码中并不严格区分聚合和组合——严格来说这样不太好因为生命周期管理方式的差异会导致不同的维护逻辑。如果你明确一个对象是另一个对象的组合部分那么在删除整体时就必须级联处理部分对象如果是聚合可能需要先解除关联再删除防止误删。4.2 职责分配这方法该放哪个类里设计阶段除了定义类更重要的是把系统功能分配到各个类的职责上。职责分配是面向对象设计中最考验经验的环节。最经典的指导原则是单一职责原则SRP一个类应该只有一个引起它变化的原因。说得通俗点一个类应该只干一类事。我来用一个反例说明。假设一个UserService类它既要处理用户注册登录、又要发送邮件通知、又要生成用户报表public class UserService { public void register(String username, String password) { // 1. 校验用户名密码 // 2. 写数据库 // 3. 发送欢迎邮件 // 4. 记录日志 } public void sendWelcomeEmail(User user) { // 发送邮件 } public void exportUserReport(ListUser users) { // 导出报表 } }这个类看起来承担了用户管理的职责但实际上它至少有三个职责用户注册逻辑、邮件发送、报表导出。这三个职责分别有三个不同的变化原因注册规则变了要改它、邮件模板变了要改它、报表格式变了还要改它。结果就是这个类会频繁变更每次变更都可能动到其他功能的代码还特别难测试——因为要测注册功能得先准备邮件服务和报表服务的测试环境。更合理的拆分是UserService负责注册、登录等用户核心逻辑EmailService负责所有邮件发送ReportService负责报表生成然后UserService依赖EmailService在注册成功后调用它发邮件。那为什么会有人写出一个几百行的大类因为早期图方便或者说没意识到代码共享不等于类要合并。很多时候把几个功能写在一个类里确实能少写几个import能少写几个构造参数但代价是熵不断累积。等到类膨胀到几千行、任何修改都可能引入回归问题的时候你才会明白拆分有多重要。4.3 设计模式初步什么时候该用什么时候别硬套讲到面向对象设计就绕不开设计模式。但我要先泼一盆冷水大部分人在错误地使用设计模式。设计模式的初衷是解决反复出现的、有特定上下文的问题。它是从大量优秀代码里提炼出的套路不是设计完毕后为了显得高级硬往上套的模板。比如策略模式它解决的问题是一组算法可以互换调用方不需要知道具体算法细节。它的前提是代码里确实有一组可替换的算法而且它们在运行时可能需要切换。如果只有一个算法你也硬套策略模式那就是过度设计的典型。再比如工厂模式它解决的问题是创建对象的逻辑比较复杂或者创建逻辑可能变化调用方不该直接new。判断该不该用某个模式的标准很简单**你是不是真的遇到了类似结构的反复出现**如果只是我觉得这里可能以后会有变化就套一个模式十有八九会变成过度设计。真正的高手反而是先写一个最简单能跑的版本当变化真的出现时再按模式去重构。我自己对设计模式的态度是把它当成工具库而不是建筑图纸。工具库的意思是——你了解它、在合适的时候能用出来建筑图纸的意思是——开工前就规划好每一块砖用哪个模式。前者是活学活用后者是刻舟求剑。5. 测试阶段面向对象程序测试的是行为不是代码5.1 单元测试的真正对象类的行为契约很多人刚接触单元测试时有一个误解认为单元测试就是测试每个方法。其实在面向对象语境下单元测试的单元应该是类或者更准确地说测试的是类对外可见的行为契约而不是实现细节。以第2章的Account类为例。测试用例应该围绕deposit、withdraw这两个公共方法来写Test void testDepositIncreasesBalance() { Account account new Account(); account.deposit(100); assertEquals(100, account.getBalance()); } Test void testWithdrawWithinBalance() { Account account new Account(); account.deposit(100); account.withdraw(30); assertEquals(70, account.getBalance()); } Test void testWithdrawExceedingBalanceThrows() { Account account new Account(); account.deposit(50); assertThrows(IllegalStateException.class, () - account.withdraw(100)); } Test void testDepositNegativeAmountThrows() { Account account new Account(); assertThrows(IllegalArgumentException.class, () - account.deposit(-10)); }这四个用例覆盖了正常路径和异常路径它们测的是类的行为契约金额为正、余额不足时抛错、操作后状态正确。如果有人重构了Account的内部实现比如balance从double改成BigDecimal这四个测试应该原样通过——这就是测行为不测实现的意义也是封装带来的可测试性红利。反过来说如果测试直接访问了类的内部字段或者测试依赖于某个方法的内部调用顺序那这个测试就绑定了实现细节一旦重构就碎。5.2 继承与多态下的测试策略继承会带来一个测试上的难题父类的测试子类要不要重复测我的建议是父类的测试要在子类上重新跑一遍但不需要复制测试代码用继承机制把父类测试类继承过来即可。JUnit里有个很实用的做法把测试类设计成abstract基类里面写公共测试用例然后每个子类不出一个具体测试类继承它。public abstract class AnimalTest { protected abstract Animal createAnimal(); Test void testIntroduce() { Animal animal createAnimal(); // 验证introduce不抛异常 assertDoesNotThrow(animal::introduce); } } public class DogTest extends AnimalTest { Override protected Animal createAnimal() { return new Dog(旺财); } Test void testSpeak() { Animal animal createAnimal(); // 验证具体行为 } }这样父类的行为契约在每个子类上都得到了验证同时每个子类也可以增加自己的特有测试。多态给测试带来的另一个问题是**当你针对抽象父类编程时测试该用哪个具体子类**答案取决于你测的是哪一层逻辑。如果测试的是调用方对Animal的依赖逻辑你可以选一个最简单的子类或者测试替身如果测试的是某个子类的特有逻辑就用那个具体子类本身。这就引到下一节的测试替身话题。5.3 Mock与Stub什么时候用什么时候不该用面向对象测试离不开测试替身Test Double包括Stub桩、Mock模拟对象、Fake等。它们的共同点都是造一个假的依赖对象让被测对象能在隔离环境下测试。最常见的问题是什么时候该Mock什么时候不该Mock我的经验是三条标准**测试目标类时它的依赖可以Mock。**比如UserService依赖EmailService测注册功能时用Mock的EmailService验证确实调用了sendWelcomeEmail方法这样可以避免每次测试都真的发邮件。**就是测试逻辑本身时不要Mock。**比如你在写一个复杂算法它依赖一个简单的配置对象直接new一个真实对象传进去就行了Mock反而增加噪音。**依赖是不稳定或不容易构造的外部资源网络、数据库、文件系统时必须Mock。**比如爬虫代码依赖网络请求测试时Mock掉HTTP层用固定的响应数据来验证解析逻辑。一个我在真实项目里踩过的坑过度Mock导致测试变成自说自话。测试里把所有依赖都Mock掉了被测对象整个逻辑都是调的Mock返回值最后测试什么都没验证到全部通过反而让人心慌。正确做法是Mock只用于隔离真正的业务断言一定要落在被测试类自己的行为和状态上。6. 一个分析—设计—测试的完整微型案例6.1 需求描述与类提取我把前面讲的方法串起来做一个极简但完整的图书借阅系统分析设计测试案例。需求文档图书馆支持注册会员借阅图书。会员可以查询图书信息并借阅图书每本图书最多只能被一个会员借出。图书有编号、书名、作者、ISBN。会员有姓名、会员编号、已借图书列表。会员归还图书后该书重新可借。管理员工可以添加新书到馆藏。按名词法提取候选类图书馆、会员Member、图书Book、管理员Admin、馆藏Collection、借阅记录BorrowRecord属性候选图书编号、书名、作者、ISBN、会员编号、姓名、已借图书列表方法候选从动词来查询图书、借阅图书、归还图书、添加新书经过合并删减我决定用四个核心类Member会员有借书和还书的行为Book图书有关键状态是否可借Library图书馆门面协调会员、图书、馆藏Admin管理员可以添加图书6.2 类图设计与代码实现简化版的关系如下Library 聚合了 Book 集合和 Member 集合Member 聚合了自己借阅的 BookAdmin 依赖 Library通过Library添加图书Book 自己维护借出状态isBorrowed字段用Java实现核心逻辑只保留最关键部分public class Book { private final String isbn; private final String title; private boolean isBorrowed; public Book(String isbn, String title) { this.isbn isbn; this.title title; this.isBorrowed false; } public boolean isAvailable() { return !isBorrowed; } public void borrow() { if (isBorrowed) { throw new IllegalStateException(图书已被借出); } this.isBorrowed true; } public void returnBook() { if (!isBorrowed) { throw new IllegalStateException(图书未被借出); } this.isBorrowed false; } public String getIsbn() { return isbn; } public String getTitle() { return title; } } public class Member { private final String memberId; private final String name; private final ListBook borrowedBooks new ArrayList(); public Member(String memberId, String name) { this.memberId memberId; this.name name; } public void borrowBook(Book book) { if (borrowedBooks.contains(book)) { throw new IllegalStateException(不能重复借阅同一本书); } book.borrow(); borrowedBooks.add(book); } public void returnBook(Book book) { if (!borrowedBooks.contains(book)) { throw new IllegalStateException(这本书不是该会员借的); } book.returnBook(); borrowedBooks.remove(book); } public String getMemberId() { return memberId; } public String getName() { return name; } public ListBook getBorrowedBooks() { return Collections.unmodifiableList(borrowedBooks); } }能看到我把借出和归还的行为分散在Book和Member两个类里而不是写一个巨大的LibraryService把一切都管理起来。这就是行为驱动设计的体现状态变化和规则校验都在最贴近的类内部完成。注意getBorrowedBooks返回的是Collections.unmodifiableList外部可以查看但不能直接修改。这是一个容易被忽略的封装细节很多人直接把内部List返回出去外部拿到后就可以绕过Member类直接add/remove封装就形同虚设了。Library类的角色就是协调者public class Library { private final ListBook books new ArrayList(); private final MapString, Member members new HashMap(); public void addBook(Book book) { books.add(book); } public void registerMember(Member member) { members.put(member.getMemberId(), member); } public Book findBookByIsbn(String isbn) { return books.stream() .filter(book - book.getIsbn().equals(isbn)) .findFirst() .orElseThrow(() - new IllegalArgumentException(没有找到该ISBN的图书)); } }6.3 测试用例设计与验证针对这个设计我的测试重点放在业务规则上而不是getter/setter上。核心规则有三条已借出的书不能被再次借出。会员不能归还自己没借过的书。归还之后书重新变为可借状态。对应测试代码Test void testBorrowAndReturnCycle() { Book book new Book(978-1, 设计模式); Member member new Member(M001, 张三); assertTrue(book.isAvailable()); member.borrowBook(book); assertFalse(book.isAvailable()); assertEquals(1, member.getBorrowedBooks().size()); member.returnBook(book); assertTrue(book.isAvailable()); assertEquals(0, member.getBorrowedBooks().size()); } Test void testBorrowSameBookTwiceThrows() { Book book new Book(978-2, 重构); Member member new Member(M002, 李四); member.borrowBook(book); assertThrows(IllegalStateException.class, () - member.borrowBook(book)); } Test void testReturnBookNotBorrowedByMemberThrows() { Book book new Book(978-3, 代码整洁之道); Member member1 new Member(M003, 王五); Member member2 new Member(M004, 赵六); member1.borrowBook(book); assertThrows(IllegalStateException.class, () - member2.returnBook(book)); }这三个测试覆盖了核心业务规则。如果哪天需求变了比如规则改成同一会员最多借5本书只需要修改Member类里加一个上限判断然后新增一个对应测试即可。这就是面向对象设计和测试互相配合带来的好处**规则集中在类内部测试直接对准规则本身。**在这个案例里我没有用Mock因为这个微型系统的依赖足够简单直接构造真实对象就够了——这也验证了我前面的观点Mock只在需要隔离外部依赖时才用不是测试标配。7. 三点实操感悟写给正在转向面向对象的人第一判断自己是不是真会面向对象别看考试、别背概念直接看你的代码里有多少个超过300行的类、多少个只有getter/setter的类、多少个if-else判断类型的逻辑。如果这三样都占了不少那不管你把封装继承多态背得多熟实际写出来的仍然是面向过程的代码。第二面向对象不是一个靠看书看视频就能学会的技能。我从自己带团队的经验看进度最快的成长路径是找一个代码质量不错且正在演进的开源项目规模不用大读它的源码然后自己动手加一个小功能再加一个和现有功能相似的扩展每加一次功能就逼着自己去理解它原来的抽象设计为什么能让扩展变得简单。这样折腾过两到三个功能你对分析—设计—测试这条链路的理解会超过看十本书。第三所谓基本概念其实是每一层设计方法的地基。你现在花一个小时把类和对象封装这几件事搞明白比后面在烂代码里花一天重构要划算得多。我见过太多人急着学Spring、学各种框架结果连一个业务类该怎么设计都没有想清楚最后框架的能力全用在给魔鬼代码擦屁股上。面向对象的基本功值得你慢下来打好。
返回列表