结合陈伟视频-多态-理解容器与颗粒的概念
一、什么是容器?
生活中,容器是包装或装载物品的贮存器。在计算机中,容器是能将其他控件放置在其上面的控件。容器具备包装、装载的功能。容器可以将不同的、相同的独立个体包装到一起,整个容器可以被迁移。
容器的特点是:包装、装载、里面的东西不被外面看到——>引申——>统一操作、管理内部关系、对外隐藏内部细节,起到管理作用。
容器的概念在软件工程中的体现是:
“容器 = 封装多个独立模块的载体” “内部颗粒是独立的,容器管理它们的组合关系” “上层(容器)持有下层(颗粒),颗粒不知道自己被谁装着”。
层级结构是这样的:
work层(容器)
└── combo层(子容器)
└── base层(颗粒)
结合陈伟视频的例子,Box就是一个容器,容器内部有水果,水果是颗粒,每种水果彼此独立。容器持有水果类,但水果类不知道自己被谁持有。由容器对外提供“放水果”,“过一天”的操作入口。外部只能操作容器,不能越过容器直接操作水果。
二、什么情况下使用容器?
这个问题需要结合容器的特点思考,容器的特点是:统一操作、管理对象之间的关系、对外隐藏内部细节。
1、统一管理
以水果减重为例。
有容器类,客户端直接调用Box.overOneDay()方法,容器内部去给水果减重即可。
没有容器,客户端要自己维护水果类,自己去遍历每一类水果,直接调用水果的fruit.computeReduceToday()方法。客户端今天维护水果类,明天可能还要维护蔬菜类,海鲜类。。。。久而久之,形成了一个逻辑冗杂,代码量超大的胖客户端。
再做个形象的比喻:把心肝脾肺肾的功能都拆解出来,放在肚子里。某个小功能坏了,修一修,刀子一不小心就影响到心脏的功能,直接心跳停止,整个人没了。想想我们的代码,是不是这样。
以增加水果为例。
有容器类,客户端直接调用Box.addFruit()方法。
没有容器,客户端自己维护List< Fruit > fruits,调用fruits.add()方法。同时,因为fruits被暴露在外面,客户端可以随意操作集合方法,改变fruits。比如,本来想清空蔬菜集合,不小心写成了fruits.clear()方法。完了,数据直接丢失了。
判断标准:外部需要逐个操作这些对象吗? 如果是,封装进容器,把遍历逻辑内化。
2、管理对象之间的关系
例子中,苹果和橘子之间没有关系——它们只是被放在同一个盒子里。但如果对象之间有关系,就要交给容器管理,而不是让对象自己维护。否则会导致对象之间耦合,A依赖B,改B会影响A。
比如:水果发霉了,要用刀切掉。需要切水果时,由盒子执行“刀切水果”的方法即可,而不是由水果调用刀,执行刀的”切水果“方法。——>刀要由盒子管理,而不是由水果管理。
❌ 水果管刀:apple.useKnife() → apple 内部持有 knife 的引用 → 刀分散在每个水果里
✅ 盒子管刀:box.cutRottenFruit(knife) → Box 持有 knife → 刀只有一把,由容器调用
// Fruit 只管自己的状态:哪部分发霉了?
class Fruit {
double getRottenWeight() { return rottenWeight; }
void removeRotten() { rottenWeight = 0; } // "可以被切"的能力
}
// Box 管工具和关系
class Box {
Knife knife; // 工具是容器的
void cutAllRotten() {
for (Fruit f : fruits) {
if (f.getRottenWeight() > 0) {
knife.cut(f); // 容器调用工具操作颗粒
}
}
}
}
由颗粒,如”水果“,”刀“等工具提供基本功能,由容器持有工具,进行编排。
判断标准:“这些对象之间有关系(顺序、依赖、层级)吗?” 如果是,这个关系就应该由容器来管,而不是散落在每个颗粒里。
凡是有"多对多"需要协调的场景(多个水果、多把刀、多种操作),协调者就是容器,被协调者就是颗粒。
3、对外隐藏内部细节
以水果减重为例。
其实需求中,客户端只需要知道水果数量,重量。具体怎么算的,水果如何减重的,客户端不用知道。这样才能做到“迪米特法则”。
判断标准:“外部需要知道里面装了什么吗?” 如果不需要(只需要操作结果),就把内部结构隐藏起来。
三、不使用容器产生的结果是什么?
1、结果一:职责外泄——本该是 Box 管的事,跑到外面去了
边界不清的写法:
// 客户端(main 方法)自己管集合
List<Fruit> myFruits = new ArrayList<>();
myFruits.add(new Apple());
myFruits.add(new Orange());
Box box = new Box(myFruits); // 把集合从外面塞进去
表面看没什么问题,但后果是:
- 客户端现在持有
myFruits的引用,它可以在 Box 不知情的情况下随意往里加水果、删水果 - Box 以为自己管着 5 个水果,但客户端已经悄悄
myFruits.remove(0)了 - Box 失去了对自己内容的控制权——它不再是一个盒子,只是一个提供了几个方法的空壳
2、结果二:状态蔓延——属性到处都是,谁都管,谁也不负责
之前代码里的问题:
class basket {
private Integer appleCount; // ← 谁的状态?
private Integer orangeCount; // ← 谁的状态?
private BigDecimal sumReduceWeight; // ← 谁的状态?
private BigDecimal currentWeight; // ← 谁的状态?
}
这四个变量是 basket 的属性,但它们的值完全取决于 Fruit 的状态——它们是 Fruit 的信息泄漏到了 Box 里。
后果:
print()被调用两次,第二次在第一次的基础上继续累加,结果错误。- 数据的修改路径变成了:
Fruit 变化 → Box 的成员变量更新 → 打印——中间有一个"手动同步"的环节,这个环节一旦遗漏,数据就不一致
本质是:Fruit 自己的信息,不应该散落到 Box 的属性上。Fruit 的重量是 Fruit 自己管的,Box 需要的时候去 Fruit 那里取,不是提前复制一份放在自己身上。不符合面向对象。
3、结果三:扩展时"到处改"——牵一发而动全身
没有容器边界时,每加一种新水果,修改会扩散到调用方:
// 没有边界时,客户端知道太多了
void printStats(List<Fruit> fruits) {
int appleCount = 0, orangeCount = 0;
double totalReduce = 0;
for (Object f : fruits) {
if (f instanceof Apple) {
appleCount++;
totalReduce += 4; // 苹果的减少量硬编码在这里
} else if (f instanceof Orange) {
orangeCount++;
totalReduce += 2; // 橘子的减少量也硬编码在这里
}
// 加 Banana:必须在这里再加一个 else if
}
}
加 Banana 时,修改点分散在:
- 新增
Banana类 - 改
printStats里的else if - 改
totalReduce的计算
修改点越多,引入 bug 的概率越高,需要测试的范围越大。
有了容器边界之后,加 Banana 只改一个地方——新增 Banana 类本身。Box 的代码一行不动,因为 Box 只和 Fruit 说话,而 Banana extends Fruit。
四、什么情况下不使用容器?
| 场景 | 不需要容器 |
|---|---|
| 只有一个对象,没有"集合"的概念 | 直接操作对象 |
| 集合只是临时使用,用完就扔 | 局部变量 List<Fruit> 就够了 |
| 集合的操作就是基本的增删改查,没有业务逻辑 | 直接用 ArrayList |
| 每个对象处理方式完全不同,无法统一 | 不存在多态的条件,容器没有意义 |
五、总结
容器与颗粒的底层逻辑
1、 “颗粒是独立的,容器是管关系的。颗粒不知道自己在哪个容器里,但容器知道自己在管哪些颗粒。”
2、“容器只暴露行为,不暴露内部。外部通过 add 往里加,通过方法获取操作结果,永远不直接拿到集合本身。”
容器与多态的关系
多态使容器更纯粹。有多态后,容器只需要考虑Fruit类,调用Fruit的方法即可,不需要考虑具体的apple,orange。新增
香蕉类,容器也不需要关心,由Fruit自行管理。
更多推荐
所有评论(0)