多重派发:一个概念上的必要性?

3
我想知道如果一个面向对象语言的性能影响可以忽略不计,是否应该包括“多重分派”(即内置支持,就像虚方法的动态调度也扩展到方法的参数)的概念。
问题:
考虑以下情况:我有一个不一定是平面的类层次结构,其中包含动物类型。在我的代码的不同位置,我想对动物对象执行一些操作。我不关心,也不能控制,如何获取此对象引用。我可能通过遍历动物列表来遇到它,或者它可能被作为方法参数之一给予我。我想要执行的操作应根据给定动物的运行时类型进行特殊化。这些操作的示例包括:
- 构建动物的视图模型,以便在GUI中呈现它。 - 构建表示此类型动物的数据对象(稍后存储到DB中)。 - 用某些食物喂动物,但根据动物的类型给出不同种类的食物(对它更健康的是什么)。
所有这些示例都在动物对象的公共API上操作,但它们所做的事情不是动物自己的业务,因此无法放入动物本身。
解决方案:
一种“解决方案”是执行类型检查。但是,这种方法容易出错,并使用反射功能(在我看来)几乎总是表明设计不良。类型应仅为编译时概念。
另一种解决方案是“滥用”(有点像)访问者模式来模拟双重调度。但这将要求我更改我的动物以接受访问者。
我相信还有其他方法。还必须解决扩展问题:如果新类型的动物加入派对,需要适应多少个代码位置,如何可靠地找到它们?
问题:
因此,在这些要求的背景下,难道多重分派不应该成为任何良好设计的面向对象语言的一个组成部分吗? 外部(不仅仅是内部)操作是否自然取决于给定对象的动态类型?

访问者模式是指双重分派的设计模式。使用访问者模式来实现双重分派并不涉及任何模仿或滥用行为。 - John Bollinger
@JohnBollinger 当描述/激励访问者时,通常会谈到对象结构的遍历。在给出的示例中,既没有对象结构也没有任何类型的遍历涉及。可以说我正在访问一个单一的对象,这是对象结构的退化形式。 :) - domin
遍历对象图是访问者模式的常见用例,但这并不意味着它是该模式定义的一部分。 - John Bollinger
我不知道这种模式的官方定义,如果这种东西甚至存在的话。我只是看到它被描述为这些用例。只要动词“访问”有意义,那么它可能不是一种滥用。同意。 - domin
3个回答

0
一种“解决方案”是执行类型检查。但这种方法容易出错,使用反射特性(在我看来)几乎总是不良设计的标志。类型应该仅作为编译时概念。
你错了。所有虚函数、虚继承和类似的东西都涉及反射特性和动态类型。在需要时推迟输入的能力绝对至关重要,并且即使是您所处的最基本情况的表述中也固有这种能力,而这种情况甚至无法发生,如果没有使用动态类型。毕竟,如果没有动态输入,为什么需要做不同的事情呢?您已经知道具体的最终类型。
当然,一点运行时输入可以处理您自己在运行时输入中遇到的问题。
只需从类型到函数构建一个字典/哈希表。您可以为任何动态链接的派生类型动态添加条目到此结构中,它是一个很好的O(1)查找,不需要内部支持。

在运行时,一个对象不必意识到自己的类型(甚至不必知道自己类型的名称),它只需通过使接收和处理消息(即方法调用)成为其行为的一部分来执行。当然,这并不排除对象某种类型的事实。你通过表现得像人类而不是知道自己是人类来成为一个人类。 - domin
关于您的提议:字典是一种动态结构。因此编译器无法给我任何保证。它基本上只是一个巨大类型开关的替代品,具有不必要(且潜在危险)的运行时更改可能性。 - domin

0

您正在建议根据方法名称/签名结合运行时实际参数类型进行动态调度。 我认为你有点疯狂。

因此,在这些要求的光环下,难道多重分派不应该成为任何良好设计的面向对象语言的组成部分吗?

虽然您提到有一些问题需要上述调度策略来简化编码,但这并不是使得此类调度内置于特定语言,更不用说每种面向对象的语言中的一个强有力的论据。

依靠给定对象的动态类型进行外部(而不仅仅是内部)操作不是很自然吗?

也许是的,但并不是所有看似“自然”的事情都是一个好主意。例如,衣服并不是天然的东西,但是如果您在公共场合中尝试不穿它们(除了伯克利之外的其他地方),会发生什么事情呢?

一些语言已经具有基于参数类型的静态调度,通常称为“重载”。另一方面,如果有多个参数需要考虑,基于参数类型的动态调度会变得非常混乱,并且无法避免它的速度较慢。当今流行的面向对象语言为您提供了执行双重调度的功能,而不需要在您不需要它的绝大多数地方支持它。

此外,虽然实现双重分派确实会带来维护问题,因为各个组件之间存在紧密耦合,但有些编码策略可以帮助使这种情况得到控制。无论如何,具备基于参数的多重分派的编程语言并不能保证能够解决这个问题。

我同意,如果有多个参数,代码会变得非常混乱(方法优先级等),而且性能也会受到影响。但是:有什么编码策略可以帮助解决这个问题?什么才是“好”的解决方案?为什么? - domin
什么编码策略会在这里有所帮助?访问者模式的好处在于基于重载的“访问”方法(而不是基于已访问类型的名称方法)进行设计,其中一个方法充当捕获所有的角色。这使得至少在Java中更容易集成新的可访问类型。在C++中并不重要,因为无论何时进行任何更改都需要重新编译所有内容。在鸭子类型的语言中,你甚至不需要担心这个问题。 - John Bollinger
什么才是“好”的解决方案?为什么呢?这在很大程度上取决于问题的具体情况和需要解决方案的环境。根据我的经验,这并不是经常出现的问题。 - John Bollinger

0

如果我们限制自己在这样一种情况下,即类型 X 的对象如何 fnorble 类型 Y 的对象的知识必须存储在 X 类或 Y 类中,那么可以让 Y 的基本类型包括一个方法,该方法接受 X 的基本类型的引用,并指示对象了解如何通过该引用标识的对象进行 fnorble,以及一个要求 Y 对象有一个 X fnorble 它的方法。

做到这一点后,可以让 X 的 Fnorble(Y) 方法首先询问 Y 对于特定类型的 X 被 fnorbled 的了解程度。如果 Y 比 X 更了解 X,则 X 的 Fnorble(Y) 方法应调用 Y 的 BeFnorbledBy(X) 方法;否则,X 应尽其所能地 fnorble Y。

根据XY的种类数量不同,Y可以定义BeFnorbledBy重载方法以适应不同种类的X,这样当X调用target.BeFnorbledBy(this)时,它会自动分派到合适的方法;然而,这种方法需要每个Y都知道任何人感兴趣的每种类型的X,无论它是否对该特定类型本身感兴趣。

请注意,这种方法不能适应这样一种情况:可能存在一个Z类的外部对象,它知道X如何fnorble Y,而X和Y本身并不知道。最好通过拥有一个“规则书”对象来处理这种情况,在这个对象中,了解各种X如何fnorble各种Y的所有内容都可以告诉规则书,想要X fnorble Y的代码可以请求规则书实现。虽然语言可以在规则书是单例的情况下提供帮助,但有时使用多个规则书可能很有用。在这些情况下,语义可能最好由代码直接使用规则书来处理。


我猜矩阵乘法的教科书例子基本上就是你抽象方案的一个实例。这基本上是一个隐藏的访问者模式在起作用。在这种情况下,耦合是如此紧密,以至于它似乎是最优雅的解决方案。 - domin
"Fnorble"是什么意思? - FreelanceConsultant
@自由职业顾问:占位符术语。我忘记在哪里第一次看到它的使用。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接