在多态性的背景下,如何处理为子类型添加的方法?


14

当使用多态性的概念时,您将创建一个类层次结构,并使用父代引用来调用接口函数,而无需知道对象具有哪种特定类型。这很棒。例:

您有动物的集合,并且可以调用所有动物的功能,eat而不必关心它是吃狗还是猫。但是在同一个类层次结构中,您的动物除了从类继承和实现Animal(例如makeEggsgetBackFromTheFreezedState等等)外,还具有其他动物。因此,在某些情况下,您可能需要了解调用其他行为的特定类型。

例如,如果是早晨,如果它只是一只动物,那么您可以致电eat;否则,如果是人类,那么请首先致电washHandsgetDressed然后再致电eat。如何处理这种情况?多态性死亡。您需要找出对象的类型,这听起来像是代码气味。是否有处理这种情况的通用方法?


7
您描述的多态类型称为子类型多态,但这不是唯一的类型(请参见多态)。您不必创建一个类层次结构来进行多态性(实际上,我认为继承不是实现子类型多态性的最常见方法,实现接口要普遍得多)。
文森特·萨瓦德

24
如果Eater使用该eat()方法定义接口,则作为客户端,您不必关心Human必须先调用washHands()getDressed()的实现,它是此类的实现细节。如果作为客户,您确实关心这个事实,那么您很可能没有使用正确的工具来完成工作。
文森特·萨瓦德

3
您还必须考虑到,在早晨,一个人可能需要getDressed在他们面前eat吃午餐,而情况并非如此。根据您的情况,washHands();if !dressed then getDressed();[code to actually eat]可能是对人类实施此操作的最佳方法。如果其他事物需要washHands和/或被getDressed调用,另一种可能性是怎么办?假设你有leaveForWork?您可能需要对程序流程进行结构调整,以使其在此之前很久就被调用。
邓肯X辛普森

1
请记住,在OOP中检查确切的类型可能是代码的味道,但这在FP中是很常见的做法(即,使用模式匹配来确定已区分联合的类型,然后对其进行操作)。
Theodoros Chatzigiannakis

3
当心诸如动物之类的OO层次结构的教室示例。实际程序几乎从来没有这样干净的分类法。例如,ericlippert.com / 2015/04/27 / wizards-and-warriors-part-one。或者,如果您想全力以赴,并质疑整个范例:面向对象编程是Bad
jpmc26 '18

Answers:


18

要看。不幸的是,没有通用的解决方案。考虑一下您的要求,并尝试弄清楚这些事情应该做什么。

例如,您说早上不同的动物做不同的事情。您如何介绍一种方法getUp()prepareForDay()类似方法。然后,您可以继续多态性,并让每只动物执行其早晨例行程序。

如果要区分动物,则不应将它们随意存储在列表中。

如果没有其他方法起作用,那么您可以尝试使用Visitor Pattern访问者模式),这是一种黑客手段,可以进行动态调度,您可以在其中提交可以从动物接收确切类型的回调的访问者。但是,我要强调的是,如果其他所有方法都失败了,这应该是不得已的选择。


33

这是一个好问题,这是很多人在尝试了解如何使用OO时遇到的麻烦。我认为大多数开发人员都为此感到困扰。我希望我能说大多数人都过去了,但我不确定情况是否如此。以我的经验,大多数开发人员最终都会使用伪OO属性包

首先,让我说清楚。这不是你的错 通常讲授OO的方式存在很大缺陷。该Animal例子是首要的罪犯,IMO。基本上,我们说,让我们谈谈对象,它们可以做什么。一个Animal罐头eat(),它可以speak()。超。现在创建一些动物并编码它们的饮食和说话方式。现在您知道OO了,对吧?

问题在于,这是从错误的方向到OO。为什么该程序中有动物,为什么它们需要说话和进食?

我很难想到一种Animal类型的实际用途。我敢肯定它存在,但让我们讨论一下我认为更容易推理的内容:交通模拟。假设我们要在各种情况下对流量建模。这是我们必须要做的一些基本事情。

Vehicle
Road
Signal

我们可以深入研究行人和火车,但我们会保持简单。

让我们考虑一下Vehicle。车辆需要什么功能?它需要在道路上行驶。它需要能够在信号处停止。它需要能够导航十字路口。

interface Vehicle {
  move(Road road);
  navigate(Road... intersection);
}

这可能太简单了,但这只是一个开始。现在。车辆可能会做的所有其他事情呢?他们可以转过马路进入沟渠。这是模拟的一部分吗?不,不需要它。一些汽车和公共汽车的液压系统使它们可以分别弹跳或跪下。这是模拟的一部分吗?不,不需要它。大多数汽车燃烧汽油。有些没有。电厂是仿真的一部分吗?不,不需要它。轮毂尺寸?不用了 GPS导航?信息娱乐系统?不需要em。

您只需要定义将要使用的行为。为此,我认为通常最好使用与它们交互的代码来构建OO接口。您从一个空接口开始,然后开始编写调用不存在的方法的代码。这就是您知道接口上需要哪些方法的方式。然后,一旦完成,就开始定义实现这些行为的类。不使用的行为是无关紧要的,不需要定义。

OO的全部意义在于,您可以稍后在不更改调用代码的情况下添加这些接口的新实现。唯一有效的方法是调用代码的需求确定接口中的内容。无法定义以后可能想到的所有可能事物的所有行为。


13
这是一个很好的答案。“为此,我认为通常最好使用与其交互的代码来构建OO接口。” 绝对,我认为这是唯一的方法。您不能仅通过实现来了解接口的公共契约,它总是从其客户的角度定义的。(并且附带说明,这实际上是TDD所涉及的。)
Vincent Savard,

@VincentSavard“我认为这是唯一的方法。” 你是对的。我想我之所以没有做到绝对,是因为一旦有了想法,就可以充实界面,然后以这种方式对其进行完善。最终,当您精疲力尽时,这才是唯一重要的事情。
JimmyJames

@ jpmc26也许措辞有点强烈。我不确定我是否很少实现它。我不确定如果您不以这种方式使用接口,除了标记接口(我认为这是一个糟糕的主意)之外,接口将如何有用。
JimmyJames

9

TL; DR:

考虑适用于所有子类并涵盖您需要的所有内容的抽象和方法。

让我们先来看您的eat()例子。

作为人类的一种本性,作为进食的前提,人类要在进食前洗手并穿好衣服。如果您希望有人来找您一起吃早餐,您不告诉他们洗手并穿衣服,而是在您邀请他们时自己做,否则他们回答“不,我不能来结束,我还没洗手,还没穿衣服。”

返回软件:

作为一个Human实例,如果没有前提条件就不会吃东西,那么我将使用Humaneat()方法washHands()getDressed()如果还没有完成的话。eat()知道这些特殊性不应该是您的要求。如果不满足先决条件,那么固执的人的选择是抛出一个异常(“我不准备吃!”),这会让您感到沮丧,但至少会告知吃不起作用。

makeEggs()

我建议您改变思维方式。您可能想要执行所有众生的预定早晨任务。同样,作为呼叫者,知道他们的职责不应该是您的工作。因此,我建议doMorningDuties()所有类都实现一个方法。


我同意这个答案。Narek关于代码气味是正确的。界面设计很臭,所以请修复它并保持良好状态。
Jonathan van de Veen

这个答案所描述的通常被称为Liskov替代原理
菲利普

2

答案很简单。

如何处理功能超出预期的对象?

您不需要处理它,因为它没有用。通常根据接口的使用方式来设计接口。如果您的界面没有定义洗手,那么您就不必将其当作界面调用者;如果您这样做的话,您将有不同的设计。

例如,如果是早上,并且如果它只是动物,那么您就叫eat;否则,如果是人类,那么就应首先呼叫washHands,getDressed,然后才呼叫eat。如何处理这种情况?

例如,用伪代码:

interface IEater { void Eat(); }
interface IMorningRoutinePerformer { void DoMorningRoutine(); }
interface IAnimal : IEater, IMorningPerformer;
interface IHuman : IEater, IMorningPerformer; 
{
  void WashHands();
  void GetDressed();
}

void MorningTime()
{
   IList<IMorningRoutinePerformer> items = Service.GetMorningPerformers();
   foreach(item in items) { item.DoMorningRoutine(); }
}

现在,您实现IMorningPerformerAnimal仅执行进餐的目的,并且Human还实现了洗手和穿衣的目的。调用MorningTime方法的人可能不太关心它是人类还是动物。它只需要执行早上的例行程序,这要归功于OO,每个对象的表现都非常出色。

多态性死亡。

还是呢?

您需要找出对象的类型

为什么要假设呢?我认为这可能是错误的假设。

是否有处理这种情况的通用方法?

是的,通常可以通过精心设计的类或接口层次结构来解决。请注意,在上面的示例中,没有任何与您给出的示例相矛盾的示例,但是,您可能会感到不满意,因为您已经做出了一些其他假设,即截至撰写本文时,您尚未在问题中进行写作,并且这些假设可能会违反。

通过收紧您的假设并修改答案以仍然满足它们,可能会陷入困境。但是,我认为这没有用。

设计良好的类层次结构很困难,并且需要对您的业务领域有很多了解。对于复杂的域,随着它们对业务域中不同实体如何交互的了解不断加深,直到经历了适当的模型,人们才经历了两次,三个甚至更多的迭代。

那就是缺乏简单的动物例子的地方。我们想教的很简单,但是我们要解决的问题只有在您深入研究之后才变得显而易见,那就是要考虑更复杂的考虑因素和领域。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.