5 Object-Oriented programming

面向对象(OO)的本质:架构视角的解读

我们即将看到,优秀架构的根基是对面向对象设计(OO)原则的理解与应用。但究竟什么是 OO?

有人说:“是数据与函数的结合。”
这个答案虽被频繁引用,却毫无说服力——它暗示 o.f()f(o) 有本质区别,这显然荒谬。早在 1966 年 Dahl 和 Nygaard 发明 OO 之前,程序员就已经在把数据结构传入函数了。

还有人说:“是对现实世界的建模。”
这充其量是个含糊其辞的答案。“建模现实世界”到底是什么意思?我们为什么要这么做?即便这句话想表达“OO 让软件更易理解”,也依然模糊不清,没说清 OO 到底是什么。

也有人搬出三个“魔法词”来解释 OO:封装、继承、多态,仿佛 OO 就是这三者的组合,或至少 OO 语言必须支持这三者。

我们逐一分析:

封装?

人们把封装归为 OO 定义的一部分,是因为 OO 语言能便捷地封装数据和函数——给一组内聚的代码画条边界,外部看不到内部数据,只能调用暴露的函数(比如类的私有成员和公有方法)。

但封装绝非 OO 独有:C 语言早就实现了“完美封装”

看这个简单的 C 程序:

// point.h
struct Point;
struct Point* makePoint(double x, double y);
double distance(struct Point *p1, struct Point *p2);

// point.c
#include "point.h"
#include <stdlib.h>
#include <math.h>
struct Point {
  double x,y;
};
struct Point* makePoint(double x, double y) {
  struct Point* p = malloc(sizeof(struct Point));
  p->x = x; p->y = y;
  return p;
}
double distance(struct Point* p1, struct Point* p2) {
  double dx = p1->x - p2->x;
  double dy = p1->y - p2->y;
  return sqrt(dx*dx+dy*dy);
}

使用 point.h 的人完全接触不到 struct Point 的成员,只能调用 makePoint()distance(),对内部实现一无所知——这是完美的封装

而 C++ 的出现反而打破了这种完美:
C++ 编译器为了计算类实例大小,必须在头文件里声明成员变量。于是代码变成这样:

// point.h
class Point {
public:
  Point(double x, double y);
  double distance(const Point& p) const;
private:
  double x; // 外部能看到变量名,只是不能访问
  double y;
};

外部用户虽然不能访问 x/y,但知道它们存在——如果改了变量名,所有包含这个头文件的代码都要重新编译!封装被破坏了。

Java、C# 干脆取消了头文件/实现文件的分离,封装性更弱。

综上:OO 非但没强化封装,反而削弱了曾经 C 语言的完美封装

继承?

OO 语言确实提供了继承——但这也不是全新的东西。
继承本质是“在作用域内重新声明一组变量和函数”,C 程序员早就靠“技巧”实现了类似效果:

// namedPoint.h
struct NamedPoint;
struct NamedPoint* makeNamedPoint(double x, double y, char* name);

// namedPoint.c
struct NamedPoint {
  double x,y; // 和 Point 前两个字段完全一致
  char* name;
};

// main.c
struct NamedPoint* origin = makeNamedPoint(0.0, 0.0, "origin");
// 强制类型转换,让 NamedPoint 伪装成 Point
printf("distance=%f\n", distance((struct Point*)origin, ...));

这种“字段对齐伪装”是 OO 出现前的常用技巧,甚至 C++ 的单继承也是这么实现的。

当然,OO 让继承更便捷(比如隐式向上转型、多继承),但并非“发明”了继承——顶多算“半分功劳”。

多态?

OO 出现前有吗?当然有。
看这个 C 拷贝程序:

#include <stdio.h>
void copy() {
  int c;
  while ((c=getchar()) != EOF) putchar(c);
}

getchar() 读 STDIN、putchar() 写 STDOUT,但 STDIN/STDOUT 可以是任意设备——这就是多态:行为依赖于设备类型。

UNIX 系统里,FILE 结构体包含指向设备驱动函数的指针,getchar() 本质是调用 STDIN->read()——这正是 OO 多态的底层逻辑(比如 C++ 的虚函数表 vtable)。

但 OO 的核心价值在于:让多态更安全、更便捷
手动用函数指针实现多态容易出错(比如忘记初始化指针),而 OO 语言把这些“约定”固化成语法,消除了风险。

这也印证了我们之前的结论:OO 对“间接控制转移施加了约束”

多态的真正威力:依赖反转

在没有安全便捷的多态之前,代码依赖永远跟着控制流走:main 调用高层函数,高层调用中层,中层调用底层——依赖方向和执行方向完全一致。

而多态能实现依赖反转

  • 高层模块(HL1)通过接口(I)调用中层模块(ML1)的函数;
  • 源码依赖(ML1 继承接口 I)和控制流方向完全相反

这给架构师带来了绝对控制权:可以任意调整源码依赖的方向,比如让数据库、UI 依赖业务规则,而非反过来。

最终实现:

  1. 独立部署:业务规则、UI、数据库可编译成独立组件,改 UI 不用动业务规则;
  2. 独立开发:不同团队可并行开发不同模块。

结论

对软件架构师而言,OO 的本质不是封装、不是继承,而是:
通过多态获得对系统所有源码依赖的绝对控制权,构建“插件式架构”——让高层策略模块独立于底层细节模块,底层细节可作为插件独立开发、部署。

核心总结

  1. OO 的封装性不如 C 语言,继承只是“便捷化的旧技巧”,二者都不是 OO 的核心价值;
  2. 多态是 OO 的核心,它并非全新概念,但 OO 让多态更安全、便捷,实现了“依赖反转”;
  3. 架构视角下,OO 的终极价值是:通过依赖反转打造独立部署、独立开发的插件式架构。
posted @ 2026-03-19 16:00  cyusouyiku  阅读(4)  评论(0)    收藏  举报