Common Lisp对象系统是现存最好的对象系统? - mendhekar

21-06-30 banq

软件中一个常见的设计/架构/分析原则是结构/行为二分法。在这里,作为系统的设计者,我们确定结构部分,然后确定每个结构部分在系统中的行为方式。

面向对象的设计中,这通常可以帮助我们识别类以及它们如何相互关联(结构)和方法(行为)。传统的面向对象乍一看似乎很自然的步骤:它将相关的结构和行为分组到一个对象中。这就是我们向学生教授面向对象编程的方式。

将结构和行为结合起来的传统动机有两个:

  1. 首先,现实世界中的对象的行为是这样的。物理对象具有定义其行为的结构。
  2. 第二个是一个单一的机制:继承,允许重用既结构和行为,这绝对是方便,然后,这种重用成为在软件设计中采用 OO 的主要驱动力。

您可以使用预先编写的软件,并根据您的需要逐步推导出新的功能。这是一个美好的承诺,随着软件的复杂性继续呈指数级增长,它导致了 OO 的快速采用。

 

传统OO复杂度增加

在这种世界观中,结构和行为之间的分离有些丢失。两者在臀部连接:

方法现在有一个它们从属的“主要”对象。在传统的面向对象中,这通常在几乎每个实现中都被视为合乎逻辑的结论。对象携带一个物理链接到对象的方法表(通常通过某种指针),以便编译器可以实现快速分派(稍后会详细介绍)。

这种合并结构和行为的设计选择的问题在于,抽象信息系统设计的世界与物理世界非常不同。我们的对象的行为取决于它们与之交互的其他对象。当按钮出现在语音界面和屏幕上时,它必须“看起来”并且可能表现不同。

传统的面向对象通过其他机制解决了这个问题,通常需要一些技巧。例如,一个简单的解决方法是创建一个Button类,该类包含一个抽象渲染方法,然后是ScreenButton和VoiceButton 的两个子类。通常,渲染方法需要不同的渲染上下文,具体取决于它是ScreenRenderingContext还是VoiceRenderingContext。所以现在你需要一个抽象的RenderingContext以便抽象渲染方法的类型,用在ScreenButton和VoiceButton 中作为各个Render渲染方法的超类型。

你在想:这有什么问题?这是非常自然的 OO 设计。

但我只是从一个结构(按钮)和两个行为变成了六个结构 + 行为。在一个小例子中,复杂性增加了 3 倍。在这个简单的版本中,我们甚至没有考虑不同的屏幕尺寸和响应能力以及你有什么。

这将我们带到下一个面向对象编程。几乎每个现代 OO 系统都是一个巨大的类集合,通常是这样设计的,可能包含大量不必要的对象和方法,必须编写这些对象和方法来适应这种限制。

 

Dispatch问题

所有传统的面向对象编程都归结为一个基本操作:将方法的名称与要运行的特定代码段匹配。编程语言专家将其称为“Dispatch分派”,这是一种相当常见的编程模式,称为“分派表”。

因此,当遇到复杂行为时,在传统 OO 中围绕它进行设计的唯一方法是将复杂行为分解为一​​系列“单一分派”。继承、匹配类型等的额外约束然后强制引入“中间”单一分派,这通常只是这种设计选择的技巧。这就是为什么我们最终得到来自一种结构和两种行为的 6 个单分派表。

当这种结构和行为的合并变得过于复杂时,传统的面向对象几乎放弃并定义了一个新的野兽:接口,它完全放弃了结构来指定行为子集的要求。这是因为在不影响代码复杂性目标的情况下真正设计所有这些单个分派是不可能的,因此您必须在某些方面削弱类型约束。

 

实际上,传统 OO 的每一个特性都可以追溯到这种将结构和行为混为一谈的设计选择,最终形成单一Dispatch分派。在像 Java 这样的单一范式语言中,更糟糕的是,你一直被迫以这种特殊的“单一Dispatch分派”方式思考。在 Java 的其他限制中,我发现这是最糟糕的。它极大地限制了解决方案设计空间,并引入了比可能需要的更多的复杂性。这也是 ORM 如此糟糕的主要原因之一。不可能以适合所有人的方式将单分派映射到多对象行为。

好的,谈论传统 OO 的问题就够了。解决办法是什么?

 

结构与行为分离

CLOS(Common Lisp Object System的简称)对面向对象世界的最大贡献是它彻底拒绝将结构和行为混为一谈。我敢肯定这最后一句话让你完全困惑/震惊/茫然。你的整个世界一直都是携带着自己行为的对象。分离怎么可能呢?

CLOS 实例定义无行为内在的对象。行为不从属于任何主要对象,并以通用函数的形式独立存在。

 

泛型函数

泛型函数是给以参数类型为基础的行为集合的名称。因此,例如,我可以在与 ( Button , ScreenRenderContext ) 和 ( Button , VoiceRenderContext )一起使用的Render通用函数上定义方法。当使用参数调用时,泛型函数会在两个参数上分派。即,它查找与参数关联的类型最特定的方法,并调用该方法。

请注意,我不再需要ScreenButton和VoiceButton,也不需要定义Button是具有未定义渲染方法的抽象类。其实CLOS并没有抽象类的概念。此外,由于这种结构分离,您可以轻松扩展泛型函数以在预定义类/内置类型上调度,而无需任何技巧。

CLOS使用方法组合,它还有一些其他的微妙之处(例如方法之前、之后和周围),使您能够真正灵活地将行为应用于对象。

然而,所有这些方法组合机制的重点在于,您不仅实现了结构和行为的分离,而且还保留了继承的好处!您可以在必要时调用先前定义的方法,而无需重写它们。

详细点击标题

banq:在DDD与OO重要区别中我认为:上下文比抽象更重要,对于上面案例button类,有两个不同上下文:ScreenRenderingContext还是VoiceRenderingContext,这里应该以上下文为切分依据,将Button从属于这两个不同的上下文,而不是将这两个上下文从属于Button这个抽象类。

 

猜你喜欢