鲍勃大叔实锤:类与数据结构的比较!每个优秀的软件设计师和架构师都需要牢记的问题


什么是类?

  • 类是一组类似对象的规范。

什么是对象?
  • 对象是一组对封装数据元素进行操作的函数。
  • 或者更确切地说,对象是一组对隐含数据元素进行操作的函数。

暗示数据元素是什么意思?“
  • 对象的功能意味着存在一些数据元素; 但是该数据不能直接在对象外部访问或可见。

是不是对象里面的数据?
  • 它可能是; 但是没有规则说必须这样。从用户的角度来看,对象只不过是一组功能函数。这些函数操作的目标数据必须存在,但用户不知道该数据的位置。


好。什么是数据结构?

  • 数据结构是一组有凝聚力的数据元素。
  • 或者,换句话说,数据结构是由隐含函数操作的一组数据元素。

好的好的。我知道了。对数据结构进行操作的函数不是由数据结构指定的,但数据结构的存在意味着某些操作必须存在。
  • 对。那么你对这两个定义有什么看法?

类和数据结构彼此相反
  • 确实。它们是彼此的补充。它们像手套一样装在一起。
  • 对象是一组对隐含数据元素进行操作的函数。
  • 数据结构是由隐含函数操作的一组数据元素

哇,所以对象不是数据结构。

  • 正确。对象与数据结构相反。

那么DTO - 数据传输对象 - 不是一个对象?
  • 正确!DTO是数据结构。

所以数据库表也不是对象吗?
  • 再次纠正。数据库包含数据结构,而不是对象。

可是等等。ORM 对象关系映射器是不是将数据库表映射到对象?
  • 当然不是。数据库表和对象之间没有映射。数据库表是数据结构,而不是对象。

那么ORM做什么呢?
  • 它们在数据结构之间传输数据。

所以他们与对象没有任何关系?
  • 没什么。没有对象关系映射器这样的东西; 因为数据库表和对象之间本身就无法映射。

但我认为ORM为我们构建了业务对象。
  • 不,ORM提取业务对象操作的数据。该数据包含在ORM加载的数据结构中。

但那么业务对象不包含该数据结构吗?
  • 它可能。它可能不会。这不是ORM的业务。

这似乎是一个小的语义点。
  • 一点也不。这种区别具有重要意义。

比如?
  • 例如数据库模式的设计与业务对象的设计。业务对象是定义业务行为的结构。 数据库模式是定义业务数据的结构。这两种结构受到非常不同的力量的约束。业务数据的结构不一定是业务行为的最佳结构。

嗯。这令人困惑。
  • 这样想吧。数据库架构不针对一个应用程序进行调整; 它必须服务于整个企业。因此,该数据的结构是许多不同应用程序之间的折衷。

好的,我明白了。
  • 好。但现在看看每个单独的应用。每个应用程序的对象模型描述了这些应用的行为结构的方式。每个应用程序都有一个不同的对象模型,微调该应用程序的一些行为。

原来如此。由于数据库模式是所有各种应用程序的折衷,因此该模式将不符合任何特定应用程序的对象模型。
  • 对!对象和数据结构受到非常不同的力量的约束。他们很少很好地协调在一起工作。人们习惯称之为对象/关系阻抗不匹配。

我听说过这个。但我认为阻抗不匹配是由ORM解决的。
  • 现在你的想法现在不同了。没有阻抗不匹配,因为对象和数据结构是互补的,而不是同构的。

什么?
  • 它们是对立的,而不是类似的实体。

对立?
  • 是的,以一种非常有趣的方式。你看,对象和数据结构意味着截然相反的控制结构。

等等,什么?
  • 想想一组所有符合公共接口的对象类。例如,想象一下表示二维形状的类,它们都具有计算形状area和perimeter形状的功能。

为什么每个软件示例总是涉及形状?
  • 我们只考虑两种不同的类型:Squares和Circles。应该清楚的是,这两个类的area和permimeter函数在不同的隐含数据结构上运行。还应该清楚,调用这些操作的方式是通过动态多态。

等待。慢一点。什么?
  • 有两种不同的area函数; 一个用于Square,另一个用于Circle。当调用者调用area特定对象上的函数时,该对象知道要调用的函数。我们称之为动态多态。

好。当然。该对象知道其方法的实现。当然。
  • 现在让我们将这些对象转换为数据结构。我们将使用Discriminated Unions。

Discoominated什么?
  • 可区分联合discriminated unions。在我们的例子中,这只是两种不同的数据结构。一个为Square,另一个为Circle。该Circle数据结构具有一个中心点,和用于数据元素的半径。它还有一个类型代码,可以将其标识为Circle。

你的意思是像一个枚举?
  • 当然。该Square数据结构具有左上点,并且侧的长度。它还有类型鉴别器 - 枚举。

好。两个带有类型代码的数据结构。
  • 对。现在考虑这个area功能。它会有一个switch语句,不是吗?

嗯。当然,对于两种不同的情况。一个为Square另一个为Circle。并且该perimeter函数将需要类似的switch语句
  • 再一次。现在考虑这两种情景的结构。在对象场景中,area函数的两个实现彼此独立并且属于(在某种意义上的单词)各自类型。 Square的area函数属于Square,Circle的area函数属于Circle。

好的,我知道你要去哪里。在数据结构场景中,area函数的两个实现在同一个函数中,它们不属于“类型”(但是你的意思是那个词)。
  • 它变得更好了。如果要将Triangle类型添加到对象方案中,必须更改哪些代码?

没有代码更改。您只需创建新Triangle类。哦,我想这个实例的创建者必须改变。
  • 对。因此,当您添加新类型时,几乎没有变化。现在假设您要添加一个新函数 - 比如center函数。

那么,你就必须将它添加到所有三种类型,Circle,Square,和Triangle。
  • 好!因此类添加新函数很难,您必须更改每个类。

但是对于数据结构,它是不同的。为了添加新Triangle新类型,您必须更改每个函数以将Triangle匹配情况添加到switch语句中。
  • 对!数据结构添加新类型很难,您必须更改每个函数。

但是当你添加新center函数时,数据结构不需要任何改变。
  • 对!数据结构添加新函数很容易。

哇。两者恰恰相反。
  • 绝对是相反:
  • 向一组类添加新函数很难,您必须更改每个类;将新函数添加到一组数据结构很容易,只需添加函数,不做任何其他更改。
  • 将新类型添加到一组类很简单,只需添加新类即可;向一组数据结构添加新类型很难,您必须更改每个函数。

是啊。对立统一。以有趣的方式对立。我的意思是,如果您知道要将新函数添加到一组类型中,那么您需要使用数据结构。但是如果你知道你将要添加新类型,那么你想要使用类。

  • 好!但今天我们还有最后一件事要考虑。还有另一种方式,数据结构和类是对立的。它与依赖关系有关。

依赖?
  • 是的,源代码依赖的方向。

好的,我会咬人的。有什么不同?
  • 考虑数据结构案例。每个函数都有一个switch语句,它根据区分联合中的类型代码选择适当的实现。

好的,那是真的。但那又怎么样?
  • 考虑调用area函数。调用者依赖于area函数,area函数取决于每个特定的实现。

“依赖”是什么意思?
  • 想象一下,如果每个类实现area函数,都有写入了它自己的函数。因此,有circleArea和squareArea和triangleArea单独自己的实现area函数。

好的,所以switch语句只调用那些函数。
  • 想象一下,这些函数放在不同的源文件中。

然后,带有switch语句的源文件必须导入、使用或包含所有这些源文件。
  • 对。这是源代码依赖。一个源文件依赖于另一个源文件。这种依赖的方向是什么?

带有switch语句的源文件取决于包含所有实现的源文件。
  • 那个area函数的调用者怎么样?

area函数的调用者依赖于具有switch语句的源文件,该语句取决于所有实现
  • 正确。所有源文件依赖关系都指向调用方向,从调用者到实现。因此,如果您对其中一个实现进行微小更改......

好的,我知道你要去哪里。对任何一个实现的更改都将导致重新编译带有switch语句的源文件,这将导致调用该switch语句的每个人area(我们的情况下的函数)被重新编译。
  • 对。至少对于依赖于源文件的日期来确定应该编译哪些模块的语言系统来说是这样的。

几乎所有这些都使用静态类型,对吧?
  • 是的,有些则没有。

很多重新编译。
  • 还有很多重新部署。

好的,但是在类的情况下这是相反的吗?
  • 是的,因为area函数的调用者依赖于接口,并且实现函数也依赖于该接口。

我明白你的意思了。类的源文件Square导入或使用或包含Shape接口的源文件。
  • 对。实现的源文件指向调用的相反方向。他们从实现指向调用者。对于静态类型语言至少也是如此。对于动态类型语言,area函数的调用者完全不依赖于任何东西。链接在运行时得到解决。

对。好。因此,如果您对其中一个实施进行了更改......
  • 只需重新编译或重新部署已更改的文件。

这是因为源文件之间的依赖关系指向调用的方向。
  • 对。我们称之为依赖倒置。

总结
好的,那么让我看看我是否可以把它合起来。类和数据结构至少以三种不同的方式存在。

  1. 类在保持数据隐含的同时使函数可见。数据结构使数据可见,同时保持隐含的函数。
  2. 类使添加类型变得容易,但很难添加函数。数据结构使添加函数变得容易,但很难添加类型。
  3. 数据结构将调用者公开给重新编译和重新部署。类将调用者与重新编译和重新部署隔离开来。

你说对了。这些是每个优秀的软件设计师和架构师都需要牢记的问题。

与本文相反观点:反OOP的面向数据的编程DOP原理