【编程学习】复盘经典 VB OOP 示例:推翻旧认知,重学面向对象

一、背景介绍

最近一周重温了陈伟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)面向对象的四点

抽象
属性抽象
  • 属性只有一个方法用 → 不放属性,放方法参数/局部变量
  • 属性多个方法共用 → 提升为成员变量
  • 子类共有的 → 提升到父类
  • 每次调用值都不同的变量 → 不存属性,实时返回即可;如果非要存属性 → 必须存完整历史(每一天的值)
方法抽象
  • 能放方法参数 → 不放属性
变量抽象
  • 如果只有一个地方用某个值→直接写在那就行,不需要提取
  • 如果有两个方法都用到→才考虑提取到上面(类级别或父类级别)
  • 能放局部变量 → 不放方法参数

判断变量作用范围的三步法

  1. 把它改成方法内部局部变量,功能还能实现吗?→ 能 → 用局部变量
  2. 如果需要跨方法共享,放成员变量 → 存所有历史(list)还是只存最新值?→ 所有历史 → 用集合
  3. 只存最新值,能区分是"第几天"的值吗?→ 不能区分 → 存最新值无意义,应放局部变量

核心原则生命周期越短越好。变量只活在它需要活的范围内,是代码清洁度和可维护性的基础。

封装
继承
多态
静态多态

编译时确定的多态,如方法重载、参数类型。

例子:传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:欢迎大家在评论区讨论面向对象的问题和给我指出问题✿✿ヽ(°▽°)ノ✿

相关推荐
Y敲键盘的地方10 小时前
第9章 工具调用循环——Agent的行动闭环
java·服务器·前端
专注写bug10 小时前
Java线程池——ThreadLocal上下文污染问题
java
武子康10 小时前
Java-09 深入浅出 MyBatis 注解开发详解:从 CRUD 到复杂关系映射
java·后端·spring
Amctwd10 小时前
【后端】多个后端系统,如何共用一套登录状态?单点登录详解
java
用户2986985301410 小时前
Java 进阶:在 Word 文档中动态增删页面
java·后端
likerhood10 小时前
Java 集合框架入门:List、Set、Queue 与 Map
java·开发语言·list
Java 码思客10 小时前
【Spring AI实战】第2章 大模型基础调用:同步/异步/流式输出
java·人工智能·spring·ai
郝学胜-神的一滴10 小时前
系统设计 013:高并发系统缓存:从原理到实践全解析
java·开发语言·python·缓存·系统架构·php·软件构建
欧米欧10 小时前
C++进阶之AVL树
java·服务器·c++