面向“类设计” = 抽象?

本文探讨了面向对象编程(OOP)的三个主要特征——封装、继承和多态,并指出教条式的OOP如何偏离了原始的OOP思想。作者通过例子展示了强制使用继承和接口带来的问题,如代码侵入、维护困难和性能损失。文章强调了OOP的本质是抽象,不应反向驱动实现,并提供了一个正向驱动的抽象示例。最后,作者呼吁回归OOP的初衷,避免陷入面向类编程(COP)的陷阱。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

OOP 的表象

  1. 封装
  2. 继承
  3. 多态

三大特征嘛,都能倒背如流,谁不知道呢,是吧?
所以 你会发现大多数面向对象的代码长得差不多,总结下来有个亲切的称谓套路三板斧

  • 第一式,封装
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 代码时,你是否想过下面问题:

  1. add_son 加法类为什么要实现一个minus减法?
  2. 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 的人。。。
其实,很多时候,人需要尝试从另一个方向去否定当下的自己。在和自己的攻防斗争中,才能加深对事物的理解,不被事物的某一面所蒙蔽

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值