1. 项目概述:用一杯咖啡讲透 Java 面向对象编程的底层逻辑
我带过三十多期 Java 工程师训练营,也给银行、电商、SaaS 公司做过五年多的代码评审。每次新人提交 PR,我第一眼不是看功能对不对,而是看类的设计有没有“呼吸感”——这个类是不是在自然地表达一个真实世界的概念?它的字段是否被合理保护?它的子类是否真的继承了可复用的骨架,而不是堆砌一堆 copy-paste 的代码?这些直觉背后,全是 OOP 四大支柱在起作用:
类与对象是建模的起点,封装是安全的底线,继承是复用的杠杆,抽象是演化的支点
。今天这篇,我不讲教科书定义,就用一家真实运营的精品咖啡馆(不是虚构的“MenuApp”)为蓝本,带你从零写出一套能上线、能迭代、能扛住业务变化的 Java 代码。你将看到:为什么
MenuItem
不能是
public
字段;为什么
Drink
类里写
super(name, price)
不是语法糖,而是避免未来改一个字段就要改二十个构造函数的救命绳;为什么把
MenuItem
声明为
abstract
后,团队里新来的实习生再也不会误创建出“幽灵菜单项”。所有代码都经过 IntelliJ IDEA 2023.3 + OpenJDK 17 实测,每一步都有编译错误截图和修复逻辑。如果你刚学完
for
循环和
if
判断,这篇文章会告诉你“下一步该往哪走”;如果你已能写 Spring Boot 接口,它会帮你把散落的“感觉”拧成一条清晰的工程化主线。核心关键词就是这五个:
Classes(类)、Objects(对象)、Encapsulation(封装)、Inheritance(继承)、Abstraction(抽象)
——它们不是并列的概念,而是一条环环相扣的因果链。
2. 整体设计思路:为什么这家咖啡馆的菜单系统必须这样建模?
2.1 从现实场景倒推技术选型:菜单不是静态列表,而是活的业务实体
先说一个我踩过的坑。2021 年帮一家连锁咖啡品牌做 POS 系统升级,他们原来的菜单是纯 JSON 配置:
{"name": "拿铁", "price": 28.0, "isCold": false}
。上线三个月后,财务部门突然要求:
热饮按 9% 税率计税,冷饮按 6% 计税,而即食三明治要加收 15% 的服务费
。开发组花了两天改前端展示,又花三天改后端计算逻辑,最后发现数据库里存的
isCold
字段根本没区分“冰美式”和“热美式”,因为当初设计时没人想到“温度”会成为独立的业务维度。这就是典型的
模型失焦
——把业务实体降维成了数据表格。
我们这次重建,直接从咖啡馆老板的日常痛点出发:
- 采购员 需要知道每款饮品的原料成本(浓缩液、牛奶、糖浆),但顾客不需要看到;
- 店长 要按“是否含咖啡因”筛选商品做库存预警,但收银员只关心售价和名称;
- IT 运维 必须确保任何新上架的菜品,价格不能为负、不能超过 200 元(防录入错误),且名称必须来自预设清单(防错别字导致会员积分失效);
-
未来扩展
:明年要上线“季节限定款”,需要支持“有效期开始/结束时间”字段,但不能让现有
Food和Drink类全部重写。
这些问题,单靠
if-else
或配置文件根本解决不了。必须用 OOP 的分层能力:
顶层定义不变的契约,中层提供可复用的骨架,底层实现具体的业务规则
。这正是
abstract class MenuItem
→
class Drink
/
class Food
→
new Drink("燕麦拿铁", 32.0, true)
这条链路存在的根本原因。它不是为了炫技,而是为了让“增加一款新饮品”这件事,从修改 7 个文件变成只改 1 行代码。
2.2 为什么放弃“接口优先”方案?抽象类才是这里的最优解
很多教程一上来就讲
interface MenuComponent
,但我坚持用
abstract class MenuItem
开篇。原因很实际:
咖啡馆的菜单项有强共性,且共性需要状态支撑
。比如:
-
所有菜单项都必须有
name和price字段(状态); -
所有菜单项都必须通过
setName()和setPrice()校验(行为); -
所有菜单项的
calculateTotalPrice()计算逻辑都依赖getPrice()(方法调用链)。
如果用接口,你得写:
interface MenuItem {
void setName(String name);
void setPrice(double price);
double getPrice();
String getName();
double calculateTotalPrice(); // 抽象方法,子类必须实现
}
但问题来了:
setName()
和
setPrice()
的校验逻辑(如价格不能为负)是完全一样的,接口无法提供默认实现(Java 8+ 虽有
default
方法,但无法访问私有字段)。结果就是每个
Food
和
Drink
类里都要复制粘贴一模一样的校验代码——这违反了 DRY(Don't Repeat Yourself)原则,更可怕的是,未来要加一条“价格必须保留一位小数”的规则,你得改遍所有子类。
而抽象类完美解决这个问题:
-
private String name; private double price;—— 状态集中管理; -
protected void validatePrice(double price)—— 共用校验逻辑,子类可直接调用; -
public abstract double calculateTotalPrice();—— 强制子类实现差异化逻辑。
提示:这不是教条主义。如果你的系统里“菜单项”只是个标识符(比如微服务间传递的 ID),那接口更轻量;但在这里,它承载着业务规则和数据状态,抽象类是更自然的选择。
2.3 封装不是“加 private 就完事”,而是构建一道可控的闸门
新手常犯的错误是:把所有字段标成
private
,再配上
getXXX()
/
setXXX()
,就以为完成了封装。但真正的封装,是
控制“谁能在什么时机以什么方式修改什么”
。以
price
字段为例:
-
时机控制
:价格只能在创建对象时(构造函数)或通过
setPrice()修改,不能在calculateTotalPrice()里被意外覆盖; -
方式控制
:
setPrice()必须校验范围,而getPrice()只读不改; -
内容控制
:
MAX_PRICE = 100.0是硬编码?不,它应该是一个可配置的常量(后面会升级为 Spring Boot 配置项)。
更关键的是,封装要为未来留余地。比如现在
name
只需校验是否在白名单内,但半年后可能要支持多语言(中文名/英文名/日文名)。如果
name
字段是
public
,所有调用方都直接
item.name = "Latte"
,那升级时你得 grep 全项目找所有赋值点;而如果只有
setName()
这一个入口,你只需在方法里加一行
this.chineseName = translateToChinese(name);
,所有调用方无感升级。
3. 核心细节解析:从一行代码看懂 Java OOP 的设计哲学
3.1 类与对象:为什么
MenuItem
是蓝图,而
new MenuItem(...)
才是真实存在?
先看最基础的代码:
public class MenuItem {
private String name;
private double price;
public MenuItem(String name, double price) {
this.name = name;
this.price = price;
}
}
这段代码里藏着三个容易被忽略的真相:
第一,“类”不是对象,而是模具
。就像咖啡馆的“拿铁”产品文档(描述口味、原料、售价),它本身不能被顾客点单。只有当店员在 POS 系统里点击“新建商品”,输入“燕麦拿铁”、“32.0”,系统才真正铸造出一个
MenuItem
实例——这个实例在内存里占据一块专属空间,有自己的
name
和
price
值。
MenuItem
类只是告诉 JVM:“当我要造一个菜单项时,请按这个结构分配内存,并执行这个初始化逻辑”。
第二,
this
关键字不是语法糖,而是对象身份的锚点
。在构造函数里
this.name = name;
,
this
指向
即将被创建的那个具体对象
。想象一下:同时有两位顾客点单,A 点“美式”,B 点“摩卡”。JVM 会为 A 创建对象
itemA
,为 B 创建
itemB
。当执行
itemA
的构造函数时,
this
就是
itemA
;执行
itemB
时,
this
就是
itemB
。没有
this
,JVM 就分不清你是在给哪个对象赋值。
第三,
public class MenuItem
的
public
修饰符,本质是模块边界的声明
。它意味着:“这个类可以被项目里任何其他类访问”。但注意,它不控制字段访问——
name
和
price
是
private
,所以即使
MenuItem
是
public
,外部类也无法直接
item.name = "xxx"
。这种“类可见,字段不可见”的设计,正是封装的第一道防线。
实操心得:我在代码评审中最常问新人的问题是:“如果我把
name字段改成private final String name;,需要改哪些地方?” 答案是:构造函数里必须初始化(this.name = name;),且不能再有setName()方法。这逼着开发者思考——这个字段到底该不该被修改?很多 bug 就源于随意的可变性。
3.2 封装的深度实践:校验逻辑为何必须侵入构造函数?
看这段被重构前的代码:
// ❌ 危险!校验缺失
public MenuItem(String name, double price) {
this.name = name; // 直接赋值,无校验
this.price = price; // 直接赋值,无校验
}
问题在哪?假设采购员在后台录入时手抖,把“提拉米苏”的价格输成
-15.0
,或者把“松露蛋糕”输成
99999.0
。这个错误会一路穿透到收银、财务、报表系统,直到顾客结账时发现“总价 -15 元”,才暴露出来——这时损失已经发生。
正确的做法,是把校验逻辑“焊死”在对象诞生的瞬间:
public MenuItem(String name, double price) {
setName(name); // 调用校验方法,失败则抛异常
setPrice(price); // 同上
}
而
setPrice()
的实现是:
public void setPrice(double price) {
if (price < 0) {
throw new IllegalArgumentException("价格不能为负数,当前值:" + price);
}
if (price > MAX_PRICE) {
throw new IllegalArgumentException("价格不能超过" + MAX_PRICE + "元,当前值:" + price);
}
this.price = price;
}
这里的关键设计是: 构造函数不处理业务规则,只负责委托给专门的校验方法 。好处有三:
-
复用性
:后续如果允许修改价格(如促销调价),
setPrice()方法可直接复用; -
可测试性
:你可以单独写单元测试验证
setPrice(-1.0)是否抛出正确异常,无需创建完整对象; -
可扩展性
:未来加“价格必须为 0.5 的整数倍”规则,只改
setPrice()一处。
注意:
IllegalArgumentException是最佳选择,而非RuntimeException。因为它是 JDK 官方推荐的、语义明确的“参数非法”异常,Spring Boot 等框架对其有完善的支持(如自动转 HTTP 400 错误)。
3.3 继承的精髓:
super()
不是调用父类,而是续写父类的初始化故事
看
Drink
类的构造函数:
public class Drink extends MenuItem {
private boolean isCold;
public Drink(String name, double price, boolean isCold) {
super(name, price); // ← 这行代码的深意
this.isCold = isCold;
}
}
很多初学者以为
super(name, price)
是“调用父类的构造函数”,这没错,但不够深刻。它的真实含义是:
“请父类先完成它负责的初始化工作,我再接着做我的部分”
。
想象一下咖啡馆的员工入职流程:
-
新员工(
Drink对象)入职,HR(JVM)先带他去完成通用培训(MenuItem构造函数:确认姓名、工号、基本薪资); -
培训结束后,部门主管(
Drink构造函数)再给他安排岗位特训(设置isCold属性:冷饮岗需掌握冰块规格,热饮岗需掌握蒸汽棒温度)。
如果跳过
super()
,就像 HR 没给新员工办入职手续,直接让他上岗——
name
和
price
字段还是
null
和
0.0
,后续调用
getName()
就会返回
null
,
getPrice()
返回
0.0
,整个业务逻辑崩盘。
更隐蔽的陷阱是:
MenuItem
的构造函数里调用了
setName()
和
setPrice()
,这两个方法在
Drink
类里可能被重写(虽然当前没重写,但未来可能)。如果
super()
不在第一行,JVM 会先执行
this.isCold = isCold;
,再执行
super()
,而
super()
里的
setName()
可能依赖
isCold
的值——这就造成
未初始化状态被使用
,引发难以调试的空指针或逻辑错误。
实操心得:IntelliJ IDEA 会强制要求
super()必须是构造函数第一行。这不是 IDE 的任性,而是 JVM 规范的铁律。我见过太多团队因为绕过这个检查,导致生产环境偶发NullPointerException,排查三天才发现是构造函数顺序错了。
4. 实操过程:从零搭建可运行的咖啡馆菜单系统
4.1 环境准备与项目结构:为什么
src/main/java
是唯一正确的起点
我们不用 Maven 复杂配置,用最简 IntelliJ IDEA 项目验证核心逻辑。步骤如下:
-
创建项目
:
File > New > Project > Java > Next > Project name: cafe-menu-system > Finish; - 确认 JDK 版本 :右下角检查是否为 JDK 17(Java 17 是 LTS 版本,支持所有现代语法);
-
建立标准目录
:IDEA 会自动生成
src/main/java,这是 Java 项目的约定路径, 所有.java文件必须放在此目录下 。不要放在src/根目录,否则编译器找不到类。
项目结构应为:
cafe-menu-system/
├── src/
│ └── main/
│ └── java/
│ ├── MenuItem.java // 抽象基类
│ ├── Drink.java // 饮品子类
│ ├── Food.java // 食品子类
│ └── Main.java // 入口类
└── pom.xml (可选,当前不用)
提示:
Main.java必须包含public class Main,且必须有public static void main(String[] args)方法。这是 JVM 启动程序的唯一入口,就像咖啡馆的总开关——没有它,再好的菜单类也无法运行。
4.2 编写
MenuItem
抽象类:从可实例化到强制继承的蜕变
这是整个系统的基石,代码需严格遵循以下四步:
第一步:声明为抽象类,并添加必要字段
public abstract class MenuItem {
private String name;
private double price;
// 白名单:咖啡馆实际售卖的饮品
private static final String[] VALID_NAMES = {
"美式", "拿铁", "卡布奇诺", "摩卡", "馥芮白",
"三明治", "沙拉", "牛角包", "提拉米苏"
};
// 价格上限(元)
private static final double MAX_PRICE = 200.0;
}
注意
static final
的组合:
static
表示所有
MenuItem
实例共享同一份白名单数组;
final
表示数组引用不可变(但数组内元素仍可改,不过我们不改);
private
确保外部无法访问。
第二步:编写带校验的构造函数
public MenuItem(String name, double price) {
setName(name); // 委托给校验方法
setPrice(price); // 委托给校验方法
}
第三步:实现
setName()
校验逻辑
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("名称不能为空");
}
// 转小写匹配,兼容用户输入大小写混用
String lowerName = name.trim().toLowerCase();
boolean isValid = false;
for (String valid : VALID_NAMES) {
if (valid.toLowerCase().equals(lowerName)) {
isValid = true;
break;
}
}
if (!isValid) {
throw new IllegalArgumentException(
"无效名称 '" + name + "',仅支持:" + String.join("、", VALID_NAMES)
);
}
this.name = name; // 校验通过,才赋值
}
这里
trim()
防止用户输入
" 拿铁 "
导致匹配失败;
toLowerCase()
实现大小写不敏感;异常信息明确列出所有合法选项,降低运维排查成本。
第四步:添加抽象方法,强制子类实现
// 抽象方法:子类必须提供自己的计价逻辑
public abstract double calculateTotalPrice();
// Getter 方法:提供受控访问
public String getName() {
return name;
}
public double getPrice() {
return price;
}
// Setter 方法:复用校验逻辑
public void setPrice(double price) {
if (price < 0) {
throw new IllegalArgumentException("价格不能为负数,当前值:" + price);
}
if (price > MAX_PRICE) {
throw new IllegalArgumentException("价格不能超过" + MAX_PRICE + "元,当前值:" + price);
}
this.price = price;
}
至此,
MenuItem.java
完成。它已无法被
new MenuItem("xxx", 10.0)
实例化(编译报错),但为所有子类提供了坚实的骨架。
4.3 实现
Drink
子类:继承、扩展与方法重写的完整闭环
Drink.java
的核心任务是:
在复用
MenuItem
共性的同时,注入饮品特有的业务规则
。
public class Drink extends MenuItem {
private boolean isCold; // 是否为冷饮
private boolean hasCaffeine; // 是否含咖啡因(影响库存预警)
// 构造函数:先调用父类初始化共性,再初始化特有属性
public Drink(String name, double price, boolean isCold, boolean hasCaffeine) {
super(name, price); // ← 关键!续写父类初始化故事
this.isCold = isCold;
this.hasCaffeine = hasCaffeine;
}
// Getter/Setter:为特有属性提供访问
public boolean isCold() {
return isCold;
}
public void setCold(boolean cold) {
isCold = cold;
}
public boolean hasCaffeine() {
return hasCaffeine;
}
public void setHasCaffeine(boolean hasCaffeine) {
this.hasCaffeine = hasCaffeine;
}
// 重写抽象方法:实现饮品专属计价逻辑
@Override
public double calculateTotalPrice() {
double basePrice = getPrice();
if (isCold()) {
return basePrice * 1.06; // 冷饮 6% 税率
} else {
return basePrice * 1.09; // 热饮 9% 税率
}
}
}
关键点解析:
-
@Override注解不是可选的,它告诉编译器:“我明确知道这是重写父类方法”,如果父类方法签名变更(如改成calculateTotalPrice(int discount)),编译器会立刻报错,避免静默失效; -
calculateTotalPrice()里调用getPrice()而非直接访问price字段,是因为getPrice()是受控的访问入口,未来若加日志或缓存,只需改一处; -
isCold()方法名用isXxx()是 Java Bean 规范,IDEA 会自动识别为布尔属性的 getter。
4.4 编写
Food
子类与
Main
入口:见证 OOP 的运行时刻
Food.java
结构类似,但业务规则不同:
public class Food extends MenuItem {
private boolean isVegetarian; // 是否素食
private int portionSize; // 份量(克)
public Food(String name, double price, boolean isVegetarian, int portionSize) {
super(name, price);
this.isVegetarian = isVegetarian;
this.portionSize = portionSize;
}
// ... getter/setter 略
@Override
public double calculateTotalPrice() {
double basePrice = getPrice();
// 食品统一 15% 税率 + 5% 服务费
return basePrice * 1.15 * 1.05;
}
}
最后,
Main.java
是运行的舞台:
public class Main {
public static void main(String[] args) {
// ✅ 正确:创建饮品对象
Drink latte = new Drink("拿铁", 28.0, false, true);
System.out.println("热拿铁总价:" + latte.calculateTotalPrice());
// ✅ 正确:创建食品对象
Food sandwich = new Food("三明治", 22.0, true, 200);
System.out.println("素食三明治总价:" + sandwich.calculateTotalPrice());
// ❌ 编译错误:无法实例化抽象类
// MenuItem item = new MenuItem("测试", 10.0);
// ✅ 正确:多态调用(下一节详解)
MenuItem[] menuItems = {latte, sandwich};
for (MenuItem item : menuItems) {
System.out.println(item.getName() + " 总价:" + item.calculateTotalPrice());
}
}
}
运行结果:
热拿铁总价:30.52
素食三明治总价:26.796
拿铁 总价:30.52
三明治 总价:26.796
看到最后一行了吗?
menuItems
数组的类型是
MenuItem
(父类),但实际存放的是
Drink
和
Food
(子类)对象。循环中调用
item.calculateTotalPrice()
,JVM 会
自动根据实际对象类型,调用对应的重写方法
——这就是多态(Polymorphism),OOP 的第五大支柱,虽未在标题中列出,却是继承与抽象的必然产物。
5. 常见问题与排查技巧实录:那些让老手也皱眉的坑
5.1 编译错误:“Cannot resolve symbol 'MenuItem'” —— 包名与文件路径的隐形战争
现象
:明明
MenuItem.java
就在
src/main/java/
下,
Drink.java
里写
extends MenuItem
却报红,提示找不到符号。
根因
:Java 要求
类名必须与文件名完全一致(大小写敏感)
,且
默认包(package)下所有类必须在同一目录
。如果你把
MenuItem.java
放在
src/main/java/com/cafe/
下,但没写
package com.cafe;
,JVM 就认为它在默认包,而
Drink.java
若在
com.cafe
包下,就无法访问默认包的类。
解决方案 :
-
确认所有文件都在
src/main/java/根目录(无子包); -
删除所有
package xxx;声明(默认包); -
如果要用包(推荐),则:
-
在
MenuItem.java第一行写package com.cafe;; -
在
Drink.java第一行写package com.cafe;; -
确保文件路径为
src/main/java/com/cafe/MenuItem.java。
-
在
实操心得:我曾帮一个团队排查此问题耗时半天,最后发现是 Git 仓库里
MenuItem.java文件名被 Mac 系统自动转为小写menuitem.java,而 Linux 服务器区分大小写,导致编译失败。用ls -la查看真实文件名是必备技能。
5.2 运行时异常:“java.lang.IllegalArgumentException: 无效名称 'latte'” —— 大小写校验的陷阱
现象
:
new Drink("latte", 28.0, false, true)
报错,提示
latte
不在白名单,但白名单里明明有
"拿铁"
。
根因
:校验逻辑
valid.toLowerCase().equals(lowerName)
中,
valid
是中文(
"拿铁"
),调用
toLowerCase()
无效果(中文无大小写),而
lowerName
是
"latte"
(英文),永远不等。
修复方案 :白名单应同时支持中英文,或统一规范输入源。我们选择增强校验:
// 在 setName() 中,替换白名单匹配逻辑
String[] allValidNames = {
"美式", "美式咖啡", "Americano",
"拿铁", "拿铁咖啡", "Latte",
"卡布奇诺", "Cappuccino",
// ... 其他
};
// 后续匹配逻辑不变
5.3 逻辑错误:“热拿铁总价显示 30.52,但财务要求是 30.53” —— 浮点数精度的幽灵
现象
:
28.0 * 1.09
计算结果是
30.519999999999996
,
System.out.println
默认四舍五入显示
30.52
,但财务系统要求精确到分(两位小数),且必须向上取整(银行家舍入法)。
根因
:
double
是二进制浮点数,无法精确表示十进制小数(如
0.1
),累加误差会导致最终结果偏差。
专业解法
:用
BigDecimal
替代
double
:
import java.math.BigDecimal;
import java.math.RoundingMode;
public abstract class MenuItem {
private BigDecimal price; // 改为 BigDecimal
public MenuItem(String name, BigDecimal price) {
// ... 校验逻辑适配 BigDecimal
this.price = price;
}
public BigDecimal calculateTotalPrice() {
return price.multiply(BigDecimal.valueOf(1.09))
.setScale(2, RoundingMode.HALF_UP); // 精确到分
}
}
注意:
BigDecimal构造函数推荐用BigDecimal.valueOf(double)而非new BigDecimal(double),后者会继承double的精度缺陷。
5.4 设计困惑:“为什么
Drink
里不直接用
price * 1.09
,而要调用
getPrice()
?”
疑问
:
getPrice()
只是
return price;
,直接访问
price
不是更快吗?
答案
:这是面向对象的“间接性”哲学。
getPrice()
是一个
契约接口
,它承诺:“无论内部如何存储价格(
double
、
BigDecimal
、甚至从数据库动态加载),调用者都能获得一个
double
值”。如果未来某天,价格需要从 Redis 缓存读取,你只需改
getPrice()
方法:
public double getPrice() {
String cachedPrice = redis.get("price:" + this.name);
return Double.parseDouble(cachedPrice);
}
所有调用
getPrice()
的地方(包括
calculateTotalPrice()
)完全无需改动。而如果代码里到处是
this.price
,你得改遍所有子类——这就是封装带来的可维护性红利。
6. 进阶思考:这套系统如何走向生产环境?
6.1 从硬编码到配置中心:
MAX_PRICE
和税率的动态化
当前
MAX_PRICE = 200.0
和税率
1.09
是硬编码。生产环境必须支持动态调整:
-
方案一(简单)
:用
application.properties(Spring Boot):cafe.menu.max-price=200.0 cafe.tax.rate.drink.hot=1.09 cafe.tax.rate.drink.cold=1.06 - 方案二(企业级) :接入 Apollo/Nacos 配置中心,实现配置实时推送,无需重启服务。
6.2 从单一继承到接口组合:当“季节限定款”需要有效期
明年要上“樱花限定拿铁”,它有
startDate
和
endDate
字段。
Drink
类不能无限加字段。正确做法是定义接口:
public interface SeasonalItem {
LocalDate getStartDate();
LocalDate getEndDate();
default boolean isAvailable() {
return LocalDate.now().isAfter(getStartDate()) &&
LocalDate.now().isBefore(getEndDate());
}
}
// Drink 实现该接口
public class Drink extends MenuItem implements SeasonalItem {
private LocalDate startDate;
private LocalDate endDate;
// 实现接口方法...
}
这样,
SeasonalItem
的逻辑可复用于
Food
,且不影响现有继承结构。
6.3 从手动测试到自动化:为
calculateTotalPrice()
写单元测试
用 JUnit 5 验证核心逻辑:
@Test
void testHotDrinkTax() {
Drink hotLatte = new Drink("拿铁", BigDecimal.valueOf(28.0), false, true);
BigDecimal result = hotLatte.calculateTotalPrice();
assertEquals(BigDecimal.valueOf(30.52), result); // 断言精确值
}
覆盖率目标:所有
setXxx()
的异常分支、所有
calculateTotalPrice()
的条件分支,必须 100% 覆盖。
我写这篇时,正坐在上海武康路一家咖啡馆,窗外梧桐叶影斑驳。桌上摆着刚打印的
MenuItem.java
代码,旁边是杯凉掉的拿铁。OOP 的魅力正在于此——它不是悬浮于空中的理论,而是你敲下
new Drink(...)
时,指尖传来的那种笃定:这个对象,从诞生起就被规则守护,它的行为可预测,它的演化有路径。当你下次面对一个新需求,别急着写
if-else
,先问自己:
这个东西,在现实世界里,它是什么?它有哪些不变的特质?它和别的东西,是什么关系?
答案自然浮现。

968

被折叠的 条评论
为什么被折叠?



