[译] 类(Class)与数据结构(Data Structures)

类(Class)与数据结构(Data Structures)

什么是类?

一个类就是一组相似对象的集合的规范。

对象是什么?

对象是一组对封装的数据元素进行操作的函数。

更确切的说,对象是一组对隐含的数据元素进行操作的函数。

隐含的数据元素是什么意思?

对象提供了某些功能,就意味着这个对象包含了一些数据元素;但是这些数据并不能在对象外部直接访问,在对象外部来看,它们是不可见的。

那么数据不在对象内吗?

这是有可能的,但是并没有强制规定必须这样。从用户的角度来看,对象仅仅是一组函数。这些函数操作的数据一定存在,但是这些数据的位置对用户是未知的。

嗯。好的,我明白了。

很好。那么什么是数据结构呢?

数据结构是一组相关性很强的数据元素。

或者,换句话说,数据结构是一组被隐含的函数操作的数据元素。

好的,我懂了。操作数据结构的函数并不被数据结构本身定义,但是它的存在暗示出,该操作函数一定存在。

是的。现在,对于这两个定义,你注意到了什么吗?

它们在某种程度上是相互对立的。

确实是这样。它们是互补的。它们就像手和手套一样契合。

对象是操作隐含数据元素的一组函数。 数据结构是被隐含的函数操作的一组数据元素。

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

正确。对象和数据结构是相互对立的。

所以,DTO —— 数据传输对象 —— 并不是对象?

正确,DTO 是数据结构。

所以数据库表也并不是对象?

正确。数据库包含了数据结构,而不是对象。

等等。ORM —— 对象关系映射 —— 不是将数据库表映射为对象了吗?

当然不是。数据库表和对象之间不存在映射关系。数据库表是数据结构,而不是对象。

那么 ORM 做了什么。

它们在数据结构之间传输数据。

它们和对象毫无关系吗?

是的,毫无关系。并没有所谓的对象关系映射;因为数据库表和对象之间并不存在映射关系。

但是我认为 ORM 为我们构建了业务对象。

不是的,ORM 抽象出了我们的业务对象操作的数据。这些数据被 ORM 加载,存在于数据结构之中。

那么并不是业务对象包含了这些数据结构?

可能包含。也可能不包含。但这都不是 ORM 需要负责的事了。

看起来只是个小小的语义点。

完全不是。这个区别有重要的意义。

比如说?

比如设计数据库模式和设计业务对象。业务对象定义了业务行为的结构。数据库模式定义了业务数据的结构。这两个结构是被非常不同的条件约束的。业务数据的结构可能并不适用于业务行为。

嗯,这很让人迷惑呀。

你可以这样来想这个问题,数据库模式并不会仅仅为一个应用而调整;它必须要服务于整个企业。所以这些数据的结构是多种不同应用需求的折中选择。

好的,这一点我明白了。

很好。现在考虑每个单独的应用。每个应用的对象模型都描述了这些应用行为的构成方式。每个应用都有不同的对象模型,这些模型都是为每个应用的行为而量身定做的。

哦,我懂了。由于数据库模式是各种应用程序的折中选择,所以这个模式和任何一个应用的对象模型都能不恰好匹配。

正确!对象和数据结构都被非常不同的条件约束。它们很少能够完美地契合。人们习惯称之为对象/关系阻抗不匹配。

我听过这个。但我之前还以为这种阻抗不匹配被 ORM 解决了。

现在你知道不同的答案了。并没有什么阻抗不匹配,因为对象和数据结构是互补的,不是同构的。

你说什么?

它们是对立的,而不是相似的实体。

对立的?

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

等一下,你说什么?

想象一组对象类,它们全都能和一个通用接口相符合。例如,代表了两种尺寸的图片类都有计算形状的面积 area 和周长 perimeter 的方法。

为什么所有软件的示例总会包含图形呢?

我们来考虑两个不同的形状:方形和圆形。我们都知道,这两种类的周长和面积的计算函数对不同的隐含数据结构进行操作。我们也都知道,这些操作被调用的方式是通过动态多态性。

等等。慢一点。你说什么?

有两个不同的计算面积的方法;一种用来计算方形面积,另一个则用来计算圆形的。当调用者基于特定类型的对象调用面积函数的时候,是对象决定了调用哪个函数。我们称之为动态多态性。

好的。是这样。对象决定了方法如何实现。这是当然的。

现在,我们将对象换成数据结构。我们将会使用 Discriminated Unions。

Discriminated Unions 是什么?

Discriminated Unions,在这个例子中其实就是两个不同的数据结构。一个用于方形,一个用于圆形。圆形数据结构的数据元素包括一个中心点坐标,和一个半径。同时它也有一个类型代码,表示它代表圆形。

你是说,就像一个枚举类型?

是的。方形的数据结构包含了左上角的点,以及边长。同时它也有鉴别类型的代码 —— 一个枚举类型。

嗯是的,两个数据结构和一个类型代码。

没错。现在考虑面积函数。它需要在内部切换状态,不是吗?

嗯,在两个不同的情境下,确实需要。一个用于计算方形面积一个用于计算圆形面积。同时计算周长的函数也需要类似的状态切换。

没错。现在思考一下这两种场景下的结构。在对象场景下,面积函数的两种实现是互相独立的,并在一定程度上是属于类型的。方形的面积函数属于方形,而圆形面积的计算属于圆形。

是的,我知道你的思路了。在数据结构的场景下,面积函数的两种实现是在同一个函数中,它们不“属于”任何一个类型。

事情变得越来越清晰了。如果你想要为对象添加三角形类型,你必须更改哪些代码?

不需要修改任何代码。你必须新建一个三角形的类。但是我认为创建实例的方法需要更改。

没错。所以当你添加一个新的类型的时候,需要修改的地方非常少。现在,比如你想要新添加一个函数 —— 计算中心点的函数。

那么现在你必须为三种类型:圆形,方形和三角形,都加上这个函数。

非常好。所以,添加一个新的函数是比较困难的,你需要修改每个类。

但是有了数据结构,就不同了。为了添加三角形这个类型,你必须为修改每个函数,为它们都加上三角形这种状态切换。

是的。新建类型也很困难,你需要修改每个函数。

但是当你添加新的计算中心的函数时,其他没什么需要修改的。

没错。添加新函数很容易。

哇,这和前文所说的是对立的。

确实是。让我们来回顾一下:

为一组类添加新的函数很困难,你需要修改每个类。 为一组数据结构添加新的函数很容易,你只需要添加函数,别的不用改。 为一组类添加新的类型很容易,你只需要新添加一个类。 为一组数据结构添加新的类型很困难,你需要修改每个函数。

是的,确实很对立。但是是以一种很有趣的方式对立起来的。我是说,如果你是要为一组类型添加新的函数,那我就会想要选择使用数据结构。但是如果你是想要添加新的类型,那么你就会想要使用类。

你提出了很棒的意见!但是今天我们还要思考最后一件事。在另一个方面,数据结构和类也是相互对立的。和依赖有关。

依赖?

是的,源代码依赖这个方面。

好吧,我要抓狂了。有什么区别呢?

首先考虑数据结构的场景下。每个函数都有一个 switch 语句,它会基于枚举类型代码选择合适的实现。

是的,确实是这样。但这样有如何呢?

想象我们调用了面积函数。调用函数的对象取决于面积函数,而面积函数取决于每个特定的实现。

如何“取决于”的呢?

想象一下,每个面积计算方法都在对象本身的函数中实现。所以会有圆形面积,方形面积和三角形面积。

好,所以 switch 语句只调用这些函数。

想象一下这些函数都在不同的源文件中。

那么这些带有 switch 语句的源文件就需要导入,或者使用,或者包含所有这些源文件。

正确。这就是源代码依赖。一个源文件依赖于另一个源文件。那么这种依赖的方向是什么呢?

具有 switch 语句的源文件依赖于包含所有实现函数的源文件。

那么面积函数的调用者又如何呢?

面积函数的调用者依赖于带有 switch 语句的源文件,而这个文件又依赖于具有所有实现的源文件。

正确。所有源文件依赖都指向调用的方向,从调用者到实现。所以,如果你在这些实现中做了一个错误的修改…

好的,我明白你的意思了。任何一个实现中的修改都将会导致具有 switch 语句的源文件被重新编译,从而导致任何使用了这个 switch 语句的函数 —— 比如我们的面积计算函数 —— 被重新编译。

是的。至少对于依赖于源文件的日期来确定应该编译哪些模块的语言系统来说是这样的。

它们几乎全都使用静态类型,是吧?

是的,但是有一些不是。

这需要大量的重新编译啊。

同时也需要大量的重新部署。

好吧,但是这些缺点在使用类的场景下是否可以被解决?

是的,因为面积函数的调用者取决于某个接口,同时负责实现的函数也依赖于这个接口。

我懂了。方形类的源文件引入,或者使用,或者包含了形状这个接口的源文件。

是的。包含了实现的源文件在调用的相反方向有作用。它们是从实现指向调用者的。至少对于静态类型语言这是肯定的。而对于动态类型语言,面积函数的调用者完全不依赖于任何东西。只在运行时才能找到它的依赖。

没错,是这样。所以如果你修改了其中一个实现…

仅有被修改的文件需要重新编译或者部署。

这是因为源文件之间的依赖的方向和调用的方向相反。

正确。我们称之为依赖反转。

好,让我来看看我是否能总结这部分内容。类和数据结构在至少三个方面互相对立。

  • 类暴露出函数而隐藏数据。数据结构暴露数据但是隐藏函数。
  • 类让增加类型容易,但是增加方法很困难。数据结构让增加函数很容易,但是增加类型困难。
  • 数据结构让调用者需要反复编译和部署。类将调用者从需要反复编译和部署的部分隔离开了。

你全都说对了。这些是每个优秀的软件设计者和架构者需要牢记于心的。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

猜你喜欢

转载自juejin.im/post/5d12efe7e51d455c8838e193