blog icon indicating copy to clipboard operation
blog copied to clipboard

类与数据结构

Open jiayisheji opened this issue 5 years ago • 0 comments

类与数据结构

什么是类?

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

什么是对象?

对象是一组对封装的数据元素进行操作的功能。或者更确切地说,对象是一组对隐含数据元素进行操作的功能。

什么是隐含的数据元素?

对象的功能意味着某些数据元素的存在。 但是该数据无法直接访问,也无法在对象外部看到。

数据不是在对象内部吗?

它可能是,但没有规则说必须如此。 从用户的角度来看,一个对象不过是一组功能。 这些功能所依据的数据必须存在,但是用户不知道该数据的位置。

花开两朵,各表一枝。

什么是数据结构?

数据结构是一组内聚的数据元素。或者,换句话说,数据结构是由隐含功能操作的一组数据元素。

数据结构未指定对数据结构进行操作的功能,但是数据结构的存在意味着某些操作必须存在。

那么,现在关于这两个定义你注意到了什么?

它们彼此相反。确实,它们是彼此的补充。

  • 对象是对隐式数据元素进行操作的一组功能。
  • 数据结构是由隐含功能操作的一组数据元素。

所以对象不是数据结构,对象是数据结构的对立面。

DTO(数据传输对象)是对象吗?

DTO是数据结构。

数据库表是对象吗?

数据库包含数据结构,而不是对象

那么,ORM和对象关系映射器不是将数据库表映射到对象吗?

当然不是,数据库表和对象之间没有映射。 数据库表是数据结构,而不是对象。

什么是ORM,ORM会做什么?

对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。ORM会提取业务对象所操作的数据。 该数据包含在ORM加载的数据结构中。

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

为什么会这样呢,可以这样想。数据库模式不只为一个应用程序进行调优;它必须服务于整个企业。因此,数据的结构是许多不同应用程序之间的折衷。

也许现在考虑每个单独的应用程序,每个应用程序的对象模型描述了这些应用程序的行为结构。每个应用程序都有不同的对象模型,并根据该应用程序的行为进行调优。

那么,由于数据库模式是所有各种应用程序的折衷,所以该模式不符合任何特定应用程序的对象模型。

对象和数据结构受到非常不同的约束,他们很少排成一起。人们把这种情况称为对象/关系阻抗不匹配。

ORM可以解决了阻抗不匹配的问题,因为对象和数据结构是互补的,而不是同构的,所以没有阻抗失配。

那又怎样?

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

对立?

是的,以一种非常有趣的方式。对象和数据结构意味着完全相反的控制结构。

考虑一组对象类,它们都符合一个公共接口。例如,想象一下表示二维形状的类,它们都具有计算形状的面积和周长的函数。

为什么每个软件示例都涉及形状呢?

我们只考虑两种不同的类型:正方形和圆形。 应该清楚的是,这两类的面积和周长函数在不同的隐式数据结构上运行。 还应该清楚的是,调用这些操作的方式是通过动态多态性进行的。

对象知道其方法的实现,现在让我们将这些对象转换为数据结构。

在我们的例子中,这只是两个不同的数据结构。 一个用于Square,另一个用于CircleCircle数据结构具有中心点和数据元素的半径。 它还有一个类型代码,可将其标识为圆形。

你是说像枚举?

当然,Square数据结构具有左上角和边的长度。 它还具有类型鉴别符–枚举。

带有类型代码的两个数据结构,现在考虑面积函数。 它要有一个switch语句,不是吗?

当然,对于两种不同的情况。 一个用于Square,另一个用于Circle。 周长功能将需要类似的switch语句

现在想想这两个场景的结构。在对象场景中,area函数的两个实现彼此独立,并且属于(某种意义上的)类型。正方形面积函数属于正方形,圆形面积函数属于圆形。

那么,处理方法。 在数据结构方案中,area函数的两个实现在同一函数中在一起,他们不会“属于”该类型。

如果要将Triangle类型添加到对象方案中,必须更改哪些代码?

无需更改代码。 您只需创建新的Triangle类。

所以当你添加一个新类型时,几乎没有更改。现在假设您想要添加一个新函数,比如center函数。

那么,我们必须将其添加到所有三种类型(圆形,正方形和三角形)中。因此添加新功能很困难,我们必须更改每个类。

但是数据结构却有所不同,为了添加Triangle,必须更改每个函数以将三角形案例添加到switch语句。

所以,添加新类型非常困难,我们必须更改每个功能。

但是当我们添加新的center函数时,什么也不需要改变。

添加新函数很容易吗?

哈哈,恰恰相反。

那当然是。 我们来回顾一下:

  • 向一组类中添加新功能很困难,必须更改每个类。
  • 向一组数据结构添加新函数很容易,只需添加该函数,其他内容都不变。
  • 向一组类添加新类型很容易,只需添加新类即可。
  • 向一组数据结构添加新类型很困难,必须更改每个函数。

它们以一种有趣的方式对立。如果你知道你要给一组类型添加新的函数,想要你使用数据结构。但是如果你知道你将添加新的类型,那么希望你使用类。

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

依赖关系?

源代码依赖关系的方向。

有什么区别吗?

考虑数据结构的情况,每个函数都有一个switch语句,该语句根据所区分的并集内的类型代码选择适当的实现。

那又怎样呢?

考虑对area函数的调用。调用者依赖于area函数,而area函数依赖于每个特定的实现。

所说的“依赖”是什么意思?

想象一下,区域的每个实现都被写入了自己的功能中。 所以有circleAreasquareArea以及triangleArea

所以switch语句仅调用那些函数。

想象一下,这些功能在不同的源文件中。然后,带有switch语句的源文件必须导入、使用或包含所有这些源文件。

这是一个源代码依赖项。一个源文件依赖于另一个源文件。这种依赖的方向是什么?

带有switch语句的源文件取决于包含所有实现的源文件。

那么area函数的调用者呢?

area函数的调用者依赖于带有switch语句的源文件,它依赖于所有的实现。

从调用者到实现,所有源文件依赖性都指向调用的方向。 因此,如果你对其中的一种实现进行了微小的更改……

你该目标我的意思, 对任何一种实现的更改都会导致重新编译带有switch语句的源文件,这将导致每个调用该switch语句的人(在本例中为area函数)都被重新编译。

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

这需要大量的重新编译,还有大量的重新部署。

但是这在类的情况下是相反的吗?

是的,因为area函数的调用者依赖于接口,而实现函数也依赖于接口。

Square类的源文件导入、使用或包含Shape接口的源文件。

实现的源文件指向调用的相反方向。 他们从实现指向调用者。 至少对于静态类型的语言是这样。 对于动态类型的语言,区域函数的调用者完全不依赖任何内容。 链接在运行时确定。

因此,如果你对其中一个实现做了改变...

只有更改后的文件需要重新编译或重新部署,这是因为源文件之间的依赖关系指向调用的方向。

这种方式我们称为依赖倒置。

最后让我们总结一下, 类和数据结构在至少三种不同的方式上是相反的:

  • 类使功能可见,同时保持隐含数据。 数据结构使数据可见,同时保持隐含功能。
  • 类使得添加类型很容易,但是添加函数很困难。数据结构使得添加函数很容易,但是添加类型却很困难。
  • 数据结构将调用者暴露给重新编译和重新部署。类将调用者与重新编译和重新部署隔离开来。

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

jiayisheji avatar Dec 17 '19 03:12 jiayisheji