一、背景介绍
最近一周重温了陈伟VB视频的苹果桔子例子(VB中非常经典的面向对象例子),时隔十多年再重温这个经典案例,我才意识到,当年对面向对象的理解只停留在表面,存在不少认知误区。重新跟师哥师姐还有同期同学们一起学习这个例子,收获良多,以下为收获分享。
二、案例原型:需求、设计与代码实现
1、业务需求描述
1.1 图片原版


1.2 文字版
有一个只能放进不能取出的盒子,水果不一定一天放入。水果只是苹果和桔子两种,它们放入盒子前的原始重量分别为50和30,放入盒子后,由于丢失水分,它们的重量减轻,苹果和桔子每天分别减轻4和3,直到达到各自原始重量的3/5后,不再减轻重量。
盒子的功能有:
1、输出盒子中苹果的数量;
2、输出盒子中桔子的数量;
3、输出一天来盒子中水果减轻的总重量
4、输出当前水果的总重量
2、 VB 代码实现与逻辑解读
2.1 VB界面

2.2 VB实现代码
水果类

苹果类

盒子类

主程序



运行效果


三、Java 复刻实践:重构我的面向对象认知
1、使用 Java 手动复刻案例
马总给出的要求:
大家可以敲一下视频中的例子(老师总说,陈伟用vb基于对象实现的苹果橘子的例子),建议不要借助大模型,就自己手敲
1.看前面的需求,先停下来自己思考一下要如何实现,
2.再看视频中如何实现
3.最后自己敲一下例子,看看能不能把例子完整的敲出来。
1.1 第一版:凭直觉编写,纯过程式思维
刚开始的这版对于马总没有理解清楚,我理解成自己看陈伟VB视频中的苹果桔子需求后自己先用面向对象实现一版,再接着看视频中陈伟老师怎么实现的。
所以写的时候还产生了疑惑:没给盒子的总容量,咋算盒子中苹果和桔子的数量啊?



1.2 第二版:初步优化,开始引入类的划分,但仍有缺陷
结合第一版马总指出存在的问题,我重新看了要求,然后重新看了陈伟VB视频的苹果桔子例子的实现,终于搞懂了他真实的需求,按照自己理解的面向对象又实现了一版。




1.3 第三版:最终版本,贴合面向对象设计思想
经过马总和其他同学对我第二版代码的review中提出的问题以及指出问题真正原因所在,我终于搞明白这个例子怎么用面向对象的思想实现,也更深入的重新理解了面向对象。
水果类Fruit
java
package polymorphism;
public class Fruit {
/**
* 原始重量
*/
private Double primitiveWeight;
/**
* 每天减少的重量
*/
private Double reduceWeight;
private Double currentWeight;
Fruit(Double primitiveWeight, Double reduceWeight) {
this.primitiveWeight = primitiveWeight;
this.reduceWeight = reduceWeight;
this.currentWeight = primitiveWeight;
}
public Double oneDayReduce() {
double limitWeight = (double) 3 / 5 * primitiveWeight;
if ((currentWeight - reduceWeight) < limitWeight) {
this.currentWeight = limitWeight;
return currentWeight - limitWeight;
} else {
this.currentWeight = currentWeight - reduceWeight;
return this.getReduceWeight();
}
}
public Double getPrimitiveWeight() {
return primitiveWeight;
}
public void setPrimitiveWeight(Double primitiveWeight) {
this.primitiveWeight = primitiveWeight;
}
public Double getReduceWeight() {
return reduceWeight;
}
public void setReduceWeight(Double reduceWeight) {
this.reduceWeight = reduceWeight;
}
public Double getCurrentWeight() {
return currentWeight;
}
}
苹果类Apple
java
package polymorphism;
public class Apple extends Fruit {
Apple() {
super(50d,4d);
}
}
桔子类Orange
java
package polymorphism;
public class Orange extends Fruit {
Orange() {
super(30d, 3d);
}
}
盒子类Box
java
package polymorphism;
import java.util.ArrayList;
import java.util.List;
//1、输出盒子中苹果的数量;
//2、输出盒子中桔子的数量;
//3、输出一天来盒子中水果减轻的总重量
//4、输出当前水果的总重量
public class Box {
private List<Fruit> fruitList = new ArrayList<>();
public void putFruit(Fruit fruit) {
this.fruitList.add(fruit);
}
public void pastOneDay(int i) {
System.out.println("【第" + i + "天】" + "一天前总重量有:" + this.getTotalFruitsWeight());
System.out.println("【第" + i + "天】" + "其中苹果有:" + this.getFruitNum(new Apple()));
System.out.println("【第" + i + "天】" + "其中桔子有:" + this.getFruitNum(new Orange()));
System.out.println("【第" + i + "天】" + "一天内损失重量:" + this.getEveryDayReduceWeight());
System.out.println("【第" + i + "天】" + "目前总重量" + this.getTotalFruitsWeight());
}
public int getFruitNum(Fruit fruitParam) {
int fruitNum = 0;
for (Fruit fruit : this.fruitList) {
if (fruit != null && fruitParam.getClass().equals(fruit.getClass())) {
fruitNum = fruitNum + 1;
}
}
return fruitNum;
}
public Double getEveryDayReduceWeight() {
Double totalReduceWeight = 0d;
for (Fruit fruit : this.fruitList) {
Double reduceWeight = fruit.oneDayReduce();
totalReduceWeight = reduceWeight + totalReduceWeight;
}
return totalReduceWeight;
}
public Double getTotalFruitsWeight() {
Double totalWeight = 0d;
for (Fruit fruit : this.fruitList) {
Double currentWeight = fruit.getCurrentWeight();
totalWeight = currentWeight + totalWeight;
}
return totalWeight;
}
public List<Fruit> getFruitList() {
return fruitList;
}
public void setFruitList(List<Fruit> fruitList) {
this.fruitList = fruitList;
}
}
客户端类TestClient
java
package polymorphism;
public class TestClient {
public static void main(String[] args) {
Box box = new Box();
box.putFruit(new Apple());
box.pastOneDay(1);
box.putFruit(new Orange());
box.pastOneDay(2);
box.pastOneDay(3);
box.pastOneDay(4);
box.pastOneDay(5);
box.pastOneDay(6);
box.pastOneDay(7);
}
}
运行效果

类图

2. 交流探讨,纠正过往认知误区
(1)第一版的经验教训
1)审题要明确。
不要着急忙慌就开始埋头苦干,否则可能干得跟要求的南辕北辙、背道而驰。
2)提高复用。
- 如何提高代码的复用率?
- 用接口还是用父类?
- 什么场景应该用接口?
- 什么场景应该用父类?
同样的逻辑不要重复写
这个例子中的水果、苹果、桔子这三个类,显然基本上都是一致的,只是涉及到具体的数值时有自己的个性化数据,这种场景下更适合把所有内容都在父类水果类里实现,个性化数据使用参数传递,这样能最大程度的复用代码。简单来说,写一份代码,三个类的调用都用。比起我用接口和苹果类、桔子类实现的复用率差距有3倍,这个简单的苹果桔子例子尚且如此,以此类推,复杂的实际业务项目中设计好的复用代码和没设计好的复用代码,它们之间的差距那就更是天差地别了。
(2)第二版的经验教训
1)代码细节与语法层面的纠正
① this 与 super 的使用规范
- 什么时候必须用this
- 什么时候必须用super
构造方法中:
参数名与属性名相同 → 必须用 this.xxx = xxx
参数名与属性名不同 → 可以不用 this
方法重写场景:子类重写了父类方法 → 必须区分 this.method() vs super.method()
子类未重写 → this 和 super 效果相同(不存在区分)
子类有重名属性/方法 → 必须用 this/super 区分子类没有重名 → 直接写变量名即可,不存在区分
② 边界临界值的判断逻辑
看到代码不能"一眼过去",要逐行审视每处定义的合理性。
- 什么条件下必须有?
- 什么条件下一定不能有?
苹果桔子例子:

代码要不要有,取决于结合当前、未来考虑是否需要
2)设计思想层面的反思
① 属性和方法的边界要分清楚。
我的getCurrentWegiht()方法里实现的是每种水果每天衰减的逻辑,并且还需要从外部传入参数,用的currentWegiht属性的get方法,这是方法命名没有和属性边界区分开,根本原因是没有对对象内部属性和外部入参做清晰的区分------哪些是对象自己知道的,哪些是外部需要传进来的,哪些是需要返回出去的,这三层没想清楚。
属性和方法不能起同样的名字(get/set除外),边界必须清晰。
②属性和方法的抽象。
- 一个属性要不要定义为成员变量?
- 父类定义属性时,要不要考虑未来可能出现的子类?
- 如何保证父类的设计能覆盖未来扩展?
定义和调用必须明确分开
定义时:方法参数是 Fruit 类型;
调用时:传入 Apple 或 Orange 实例
属性只有一个方法用 → 不放属性,放方法参数/局部变量属性多个方法共用 → 提升为成员变量
子类共有的 → 提升到父类
能放方法参数 → 不放属性能放局部变量 → 不放方法参数
③父类抽象范围和规则。
- 苹果类和桔子类的哪些属性必须抽象到父类(水果类)?
- 哪些属性必须保留在子类(苹果类、桔子类)自己那里?
- 哪些方法必须抽象到父类(水果类)?
- 哪些方法必须保留在子类(苹果类、桔子类)?
父类里定义属性,所有子类都会有------要判断是不是必须的
共有的属性 → 放父类独有的属性 → 放子类
不确定的 → 暂不放,有了再考虑
以这个苹果桔子例子来说,苹果和桔子都有原始重量、当前重量、每天衰减重量这三个属性,且如果后续扩展新的水果子类,这三个属性也是必须的,所以原始重量、当前重量、每天衰减重量应该放在父类(水果类)中定义。
⑤代码的三维验证法:
评估一段代码是否合理,可以从三个维度同时验证,三维都通才是好代码。
- 生活逻辑(和真实世界是否相符)
- 代码逻辑(能否实现功能)
- 时序图逻辑(流程是否正确、有没有死分支)。
⑥空间换时间的设计取舍
空间换时间 :定义成员变量占用内存,节省CPU重复计算。
时间换空间:不定义变量,每次用时现算,节省内存但消耗CPU。
决策依据:
- CPU资源吃紧 → 用内存换时间(定义成员变量)
- 内存资源吃紧 → 用时间换空间(不定义,现算)
- 两者都不吃紧 → 看维护性和复用性
3、重构认知:重新理解面向对象
(1)面向对象的四点
抽象
属性抽象
- 属性只有一个方法用 → 不放属性,放方法参数/局部变量
- 属性多个方法共用 → 提升为成员变量
- 子类共有的 → 提升到父类
- 每次调用值都不同的变量 → 不存属性,实时返回即可;如果非要存属性 → 必须存完整历史(每一天的值)
方法抽象
- 能放方法参数 → 不放属性
变量抽象
- 如果只有一个地方用某个值→直接写在那就行,不需要提取
- 如果有两个方法都用到→才考虑提取到上面(类级别或父类级别)
- 能放局部变量 → 不放方法参数
判断变量作用范围的三步法:
- 把它改成方法内部局部变量,功能还能实现吗?→ 能 → 用局部变量
- 如果需要跨方法共享,放成员变量 → 存所有历史(list)还是只存最新值?→ 所有历史 → 用集合
- 只存最新值,能区分是"第几天"的值吗?→ 不能区分 → 存最新值无意义,应放局部变量
核心原则 :生命周期越短越好。变量只活在它需要活的范围内,是代码清洁度和可维护性的基础。
封装
继承
多态
静态多态
编译时确定的多态,如方法重载、参数类型。
例子:传Fruit类型参数,实际传Apple/Orange
java
//往盒子里放水果
public void putFruit(Fruit fruit) {
this.fruitList.add(fruit);
}
// 客户端调用时多态
Box box = new Box();
box.putFruit(new Apple());
box.putFruit(new Orange());
// 计算每种水果的个数
public int getFruitNum(Fruit fruitParam) {
int fruitNum = 0;
for (Fruit fruit : this.fruitList) {
if (fruit != null && fruitParam.getClass().equals(fruit.getClass())) {
fruitNum = fruitNum + 1;
}
}
return fruitNum;
}
// 过一天逻辑里打印每种水果数量时多态
System.out.println("【第" + i + "天】" + "其中苹果有:" + this.getFruitNum(new Apple()));
System.out.println("【第" + i + "天】" + "其中桔子有:" + this.getFruitNum(new Orange()));
动态多态
动态多态是指在运行时确定具体调用的方法或函数,主要通过继承和虚函数(Virtual Function)来实现。
代码实现:
java
// 打印每个水果的原始重量、每天衰减重量
List<Fruit> fruitList = box.getFruitList();
for (Fruit fruit : fruitList) {
System.out.println("该水果的原始重量为"+fruit.getPrimitiveWegiht());
System.out.println("该水果的原始重量为"+fruit.getReduceWeight());
}
运行效果:

(2)面向对象实现
面向对象的正确思维路径 :先问"这件事归谁管?"→ 找到归属对象 → 明确该对象内部的属性和方法 → 代码自然写出来,结果自然是对的,几乎不用调试。
正确做法:写什么类就化身那个类,只为它考虑
苹果桔子例子:
bash
盒子里必须有「水果集合」------这是盒子的属性
苹果数量/橘子数量 → 不应是盒子的全局属性
应由每个水果自己汇总上报,盒子只负责收
每天减多少 → 水果自己的事,问它就行
盒子.过一天()
↓ 遍历每个水果
↓ 水果.告诉我你今天减了多少()
↓ 水果内部判断:要不要减?减多少?
↓ 不减就返回0
↓ 汇总返回
关键:过了一天是大家共有的行为
→ 盒子遍历每个元素,但跳过不由盒子决定
→ 由每个水果自己决定今天减不减
(3)面向对象设计中的核心决策
- 什么该放属性
- 什么该放方法参数
- 什么该放父类
- 什么该放子类
(4)每定义一个变量/方法,都要追问:
- 它该不该在这儿
- 生命周期应该多长
- 去掉它得到了什么、失去了什么
设计方法前先问三个问题:
①这个方法的输入是什么(外部不知道、需要由外部告知的)
②这个方法的输出是什么(调用方需要得到的结果)
③这个方法内部用到的其他数据从哪里来(成员变量还是入参)。
四、总结与思考
关于面向对象
不是用面向对象的编程语言写的代码就叫面向对象,而是真的从设计时站在对象的角度考虑和实现。
抽象、封装、继承、多态每一个概念不仅要弄清楚它们的内涵和外延,还要能够映射到代码,具体就是你要能够说得清楚你的哪一行代码用的是抽象,哪一行代码用的封装,哪一行代码用的是继承,哪一行代码用的是多态。做不到映射到代码,那学了等于没学。
关于编程(写代码)
不能只考虑功能能否实现,还要考虑各种资源的消耗。每个人写代码都不管资源的消耗,那么最后写出来的软件需要消耗的资源就是巨量的。
不仅要会用别人封装好的API,还要懂逻辑,否则很可能用错。
关于迭代思维
先能描述问题(定量)→ 再能抽出问题的共性本质(定性)→ 再能把定性的结论继续追问(多层迭代)
「定量找问题」vs「定性抽本质」
从"指出具体问题"上升到"抽象本质原因"。
从定量到定性就是从找问题到抽本质的过程,定量相对容易做到,但是定性很难,需要不断的去刻意练习。
多层追问------迭代思维训练
训练自己的多维思维,验证对方/自己是不是真的懂:把说的结论包起来再问一层,看能不能答出来?如果只能顺着说、不能往上追溯,说明只是听来的,不是自己掌握的。
每问一层,是在把已知结论变为前提,向上寻找更根本的原因。这是苏格拉底式对话的精髓------不停问"为什么",直到触达无法再追问的底层公理。
本篇用经典的水果装箱案例,一步步演示从过程式思维到面向对象思维的转变,适合刚接触 OOP、或是想重新梳理面向对象思想的同学参考。
ps:欢迎大家在评论区讨论面向对象的问题和给我指出问题✿✿ヽ(°▽°)ノ✿