OOP 的表象
- 封装
- 继承
- 多态
三大特征嘛,都能倒背如流,谁不知道呢,是吧?
所以 你会发现大多数面向对象的代码长得差不多,总结下来有个亲切的称谓套路三板斧
- 第一式,封装
class calculate_base {
public:
virtual void add(int num) = 0;
virtual void minus(int num) = 0;
virtual bool check(int num) {
printf("this is base\n");
return false;
}
};
- 第二式,继承
class add_son :public calculate_base
- 第三式,多态
class add_son :public calculate_base {
private:
uint64_t x = 666;
public:
void add(int num) { x = x + num; }
void minus(int num) { //毛用不起,还非要实现 }
bool check(uint64_t num) { //为毛调用不到我, num 确实有 uint64_t的需求啊
printf("this is sun_add\n");
if (num == x) {
return true;
}
return false;
}
};
一套下来,行运流水般丝滑,当你沉醉于
多么标准的 OOP 代码时,你是否想过下面问题:
- add_son 加法类为什么要实现一个
minus
减法? check
多态能被调用吗?
此处可能有杠精,你不会改基类啊?!
确实可以改基类,然后你把继承 calculate_base 的所有子类小心翼翼地全撸一遍(可能不全是你写的),可能还会引入 bug,多么酸爽的领悟
到这,你可能心存疑虑,但又说不出缘由,多年的 OOP 信仰,怎么感觉有点不对劲呢?
OOP 的历史与扭曲
对象范式的原始概念根本不包括类和继承,只有
- 程序是由对象组成的
- 对象之间互相 “传递消息”,协作完成任务
而教条式的 OOP 已经扭曲了原有 OOP 的含义。多数人认为写面向对象一定要符合上面的套路三板斧形式。
究其缘由还有一个小故事:
c++之父在博士期间深入研究过第一个面向对象的语言Simula,非常欣赏Sinula语言的思想,故c++面向对象思路受Sinula语言影响,采用调用目标对象的方法来“传递消息”,所以需要事先知道这个对象本身有哪些方法。
因此,定义对象本身有哪些方法的 “类” 和 “继承” 的概念,一下超越了对象本身,而对象只不过是类这个模子里 “造出来的” 东西,反而变得不重要。
随着c++的大行其道,继承、封装、多态变成了面向对象世界的核心概念,至此OOP被扭曲为COP --- Class Oriented Programming (面向类程序设计)
这套三板斧COP 概念本身是有缺陷的 。用起来更像是为了聊天而去相亲…说错了,是为了面向对象而去面向对象。
实现起来似乎要这样 :
程序员成为 ⟶ \longrightarrow ⟶ 领域专家 ⟶ \longrightarrow ⟶ 领域分类学专家 ⟶ \longrightarrow ⟶ 构造一个完整的继承树 ⟶ \longrightarrow ⟶ new 出对象 ⟶ \longrightarrow ⟶ 程序跑起来
理解起来似乎要这样 :
你想要了解清楚眼前的这位妹子,不但要知道她已有的一切(所有成员),还要知道她祖上的一切(继承链),更要知道这位妹子做了什么叛逆行为(虚函数重写),还不得不知道,这些叛逆行为,会对祖传的既有秘方产生什么样的影响(调用虚函数的函数)
1990 年代中期,COP 问题已经十分明显,不过历史的车轮总是在前进
- 1994 年 Robert C. Martin 在《Object-Oriented C++ Design Using Booch Method》中,建议面向对象设计从对象活动图入手,而不是类图
- 1995 年 经典作品《Design Patterns》中,建议优先考虑组合而不是继承,也是尽人皆知的事情
- 2000 年 delphi 之父在创建 .Net Framework 时,不想要继承
- 2000 年后 工程界明确的提出:“组合比继承重要,而且更灵活”
- 2007 年 go 语言放弃继承
- 2015 年 rust 语言放弃继承
强行用套路三板斧的 COP 来组织代码,至少会面临以下问题:
1. 强制继承,侵入绑架,污染后续业务代码
2. 面向基类编程,追求形同,基类变动,子类也需要改动。增加维护难度
3. 代码以树形结构组织,继承越多,层级越深,代码越复杂。增加理解难度
4. 虚函数,弱规则,基于约定。增加出错可能性
5. 纯虚函数,强规则,冗余实现。增加多余无用代码
6. 基于虚函数的多态,带来性能损耗的惩罚
当然套路三板斧的 COP 代码,在某些情况下是必要的
- 类型擦除,比如 std::function 的实现
- 某些特定 行为可能需要被改写,比如 QCombobox 实现右侧删除键
OOP 的本质
OOP 的本质是抽象,和任何实现手段无关。
抽象 | 具化 |
---|---|
属性 | 数据 |
行为 | 接口 |
一定是抽象正向驱动实现,而不是实现反向驱动抽象。
怎么理解呢?假设有一个需求,让集合中的动物们都说一句 hello word,典型的异构类型多态。
一个反向驱动的例子
class animal {
public:
virtual void say(const std::string& c) = 0;
};
class dog :public animal {
public:
void say(const std::string& c) {
printf("dog say:%s\n", c.c_str());
}
};
class cat :public animal {
public:
void say(const std::string& c) {
printf("cat say:%s\n", c.c_str());
}
};
class pig :public animal {
public:
void say(const std::string& c) {
printf("pig say:%s\n", c.c_str());
}
};
int main() {
std::vector<std::shared_ptr<animal>> animals;
animals.emplace_back(new dog);
animals.emplace_back(new cat);
animals.emplace_back(new pig);
for (auto&& animal : animals) {
animal->say("hello word");
}
return 0;
}
- 因为动物们都说一句,所以每个动物类需要有 say 接口
- 因为集合中的动物们,所以每个动物类需要一个相同的基类,然后才可以加入到一个集合中
- 因为需要集合中的动物们都说一句,所以基类需要有个 say 虚接口,动物类都实现这个 say 接口
明显的三板斧COP 代码,实现完成,所以抽象完成。但是进一步想:
- 如果 dog 喜欢说废话,需要再加一个 extra 参数呢?
- 如果 cat 喜欢重复说话,需要指定个重复次数呢?
- 如果 pig 喜欢特立独行,只说给定话语的摘要呢?
明显地它们都有 say 能力,但是个体差异很明显,导致 say 行为也有差异。
强制继承,侵入绑架一模一样的 say 行为,明显就不合理。
现实就是如此,对象可能会有相同的行为,但是行为的表现力却是不一样的
所以需要抽象一种普适的 concept 行为,在需要时,辅以特定的处理,以抽象驱动实现
一个正向驱动的例子
class dog {
public:
void say(const std::string& c, const std::string& extra = "") {
printf("dog say:%s\n", c.c_str());
if (!extra.empty()) {
printf("dog extra say:%s\n", extra.c_str());
}
}
};
class cat {
public:
void say(const std::string& c, int count = 1) {
for (int i = 0; i < count; i++) {
printf("cat say:%s\n", c.c_str());
}
}
};
class pig {
public:
void say(uint64_t c) {
printf("pig say:%llu\n", c);
}
};
#define ANIMALS dog,cat,pig
int main() {
std::vector<std::variant<ANIMALS>> animals;
animals.emplace_back(dog{});
animals.emplace_back(cat{});
animals.emplace_back(pig{});
for (auto&& animal : animals) {
std::visit([](auto&& animal) {
using animal_type = typename std::remove_const_t<std::remove_reference_t<decltype(animal)>>;
if constexpr (std::is_same_v<animal_type, pig>) {
animal.say(std::hash<std::string>()("hello word"));
}
else {
animal.say("hello word");
}
}, animal);
}
return 0;
}
根本就不关心所谓的基类,所谓的继承,所谓的抽象行为接口。
对象本就应改如此简约,让对象回归本质。不为实现而抽象,让抽象回归对象
结束语
历史让 c++走上舞台,历史也终将让 COP 重新回到 OOP 的本来面目,谨以此文,尝试唤醒那些深陷COP 的人。。。
其实,很多时候,人需要尝试从另一个方向去否定当下的自己。在和自己的攻防斗争中,才能加深对事物的理解,不被事物的某一面所蒙蔽