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

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

什么是类?前端

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

对象是什么?ios

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

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

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

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

那么数据不在对象内吗?数据结构

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

嗯。好的,我明白了。

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

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

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

好的,我懂了。操做数据结构的函数并不被数据结构自己定义,可是它的存在暗示出,该操做函数必定存在。

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

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

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

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

哇哦,因此对象并非数据结构。

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

因此,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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索