如何避免垂头丧气?


12

我的问题是关于超类Animal的特殊情况。

  1. Animal可以moveForward()eat()
  2. Seal延伸Animal
  3. Dog延伸Animal
  4. 还有一个特殊的生物也Animal被称为Human
  5. Human还实现了方法speak()(不是由实现Animal)。

在接受抽象方法的实现中,Animal我想使用该speak()方法。如果不进行低调,这似乎是不可能的。杰里米·米勒(Jeremy Miller)在他的文章中写道,沮丧的气味。

在这种情况下,如何避免垂头丧气呢?


6
修正模型。使用现有的分类层次结构建模类层次结构通常是错误的想法。您应该从现有代码中提取抽象,而不是创建抽象,然后尝试将代码适合其中。
欣快2014年

2
如果您想让动物说话,那么说话的能力就是使动物成为动物的一部分: artima.com/interfacedesign/PreferPoly.html
JeffO 2014年

8
moveForward?螃蟹呢?
Fabio Marcolini 2014年

BTW有效Java中的哪项是#?
goldilocks 2014年

1
有些人不会说话,因此如果尝试,则会引发异常。
图兰斯·科尔多瓦

Answers:


12

如果您有一种方法需要知道特定类的类型Human才能执行某项操作,那么您将违反一些SOLID原则,尤其是:

  • 开放/封闭原则-将来如果您需要添加一种可以说话的新动物类型(例如鹦鹉),或对该类型进行特定操作,则现有代码必须进行更改
  • 接口隔离原理-听起来您泛化得太多。动物可以覆盖各种各样的物种。

我认为,如果您的方法需要特定的类类型,请调用该特定方法,然后将该方法更改为仅接受该类,而不接受其接口。

像这样的东西:

public void MakeItSpeak( Human obj );

而不像这样:

public void SpeakIfHuman( Animal obj );

也可以在Animal被调用时创建一种抽象方法canSpeak,每个具体实现都必须定义它是否可以“讲话”。
布兰登2014年

但我更喜欢您的回答。许多复杂性来自于尝试将某种事物视为非某种事物。
布兰登2014年

@Brandon我猜在那种情况下,如果它不能“说话”,则抛出异常。还是什么都不做。
BЈовић

所以,你得到超载这样的:public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}public void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
巴特·韦伯

1
一直进行-makeItSpeak(ISpeakingAnimal animal)-然后可以让ISpeakingAnimal实现Animal。您还可以让makeItSpeak(Animal animal){如果instanceof ISpeakingAnimal}讲话,但是它散发出淡淡的气味。
ptyx

6

问题不在于您向下转换-而是您向下转换到Human。而是创建一个接口:

public interface CanSpeak{
    void speak();
}

public abstract class Animal{
    //....
}

public class Human extends Animal implements CanSpeak{
    public void speak(){
        //....
    }
}

public void mysteriousMethod(Animal animal){
    //....
    if(animal instanceof CanSpeak){
        ((CanSpeak)animal).speak();
    }else{
        //Throw exception or something
    }
    //....
}

这样,条件不是动物是Human-条件是它会说话。只要实现mysteriousMethod,这意味着可以与其他非人类子类Animal一起使用CanSpeak


在这种情况下,应以CanSpeak实例代替Animal
Newtopian 2014年

1
@Newtopian假设该方法不需要的任何内容Animal,并且该方法的所有用户都将保留要通过CanSpeak类型引用(甚至Human类型引用)发送给它的对象。如果是这种情况,那么该方法本来可以使用的Human,而我们不需要引入CanSpeak
Idan Arye 2014年

摆脱接口与方法签名中的特定性没有关系,只有当只有人类并且永远只有“ CanSpeak”的人类时,您才能摆脱该接口。
Newtopian 2014年

1
@Newtopian首先介绍的原因CanSpeak不是我们拥有实现它的东西(Human)而是我们拥有使用它的东西(该方法)。的目的CanSpeak是使该方法与具体的类脱钩Human。如果我们没有方法来以CanSpeak不同的方式对待事物,那么区分那些事物就没有意义了CanSpeak。我们不仅仅因为我们有一个方法就创建了接口...
Idan Arye 2014年

2

您可以将“交流”添加到“动物”。狗吠,人类说话,海豹..呃..我不知道海豹是做什么的。

但这听起来像您的方法设计为if(Animal is Human)Speak();

您可能要问的问题是,替代方案是什么?很难给出建议,因为我不确定您要实现的目标。在某些理论情况下,向下转换/向上转换是最佳方法。


15
密封件流动,但是狐狸是不确定的。

2

在这种情况下,默认实现speak()AbstractAnimal类将是:

void speak() throws CantSpeakException {
  throw new CantSpeakException();
}

到那时,您已经在Abstract类中有了一个默认实现-它的行为正确。

try {
  thingy.speak();
} catch (CantSeakException e) {
  System.out.println("You can't talk to the " + thingy.name());
}

是的,这意味着您已经在代码中分散了try-catching以处理every speak,但是替代方法是if(thingy is Human)包装所有语音。

例外的优点是,如果您在某个时候有另一种说话的东西(一只鹦鹉),则无需重新实现所有测试。


1
我认为这不是使用异常的好理由(除非它是python代码)。
布莱恩·陈

1
我不会投反对票,因为从技术上讲这是可行的,但是我真的不喜欢这种设计。为什么在父级定义一个父级无法实现的方法?除非您知道对象是什么类型(否则,否则就不会出现此问题),则需要始终使用try / catch来检查完全可避免的异常。您甚至可以使用一种canSpeak()方法来更好地处理此问题。
布兰登2014年

2
我从事的项目存在大量缺陷,原因是有人决定重写一堆工作方法以引发异常,因为它们使用的是过时的实现。他本可以修复实现,但选择将异常抛出到各处。自然,我们进入质量检查时会遇到数百个错误。因此,我偏向于在不应调用的方法中引发异常。如果不应该调用它们请将其删除
布兰登2014年

2
在这种情况下,抛出异常的替代方法可能是什么也不做。毕竟,他们不能说:)
BЈовић

@Brandon从一开始就设计例外与以后进行改造是有区别的。还可以用Java8中的默认方法查看接口,或者不抛出异常而只是保持沉默。但是,关键点在于,为了避免向下转换,需要在传递的类型中定义函数。

1

向下转换有时是必要且适当的。特别是,在以下情况中通常是合适的:一个人拥有可能具有或不具有某种能力的对象,并且希望在存在某种能力时以某种默认方式处理没有该能力的对象时使用该能力。举一个简单的例子,假设String询问a是否等于其他任意对象。为了String使另一个相等String,它必须检查另一个字符串的长度和支持字符数组。如果String询问a是否等于a Dog,则它不能访问的长度Dog,但不必访问;相反,如果String应该与a比较的对象不是aString,则比较应使用默认行为(报告其他对象不相等)。

垂头丧气的时候应该被认为是最可疑的,那是当“知道”被投射的物体属于正确的类型时。通常,如果已知一个对象是a Cat,则应使用type Cat的变量而不是type的变量Animal来引用它。有时候,这有时并不总是有效的。例如,一个Zoo集合可能将对象对保存在偶数/奇数阵列插槽中,期望每对对象能够互相作用,即使它们不能作用于其他对象对中也是如此。在这种情况下,每对对象仍然必须接受非特定的参数类型,以便可以从语法上将其他任何对的对象传递给它们。因此,即使CatplayWith(Animal other)方法仅在other是a 时才有效Cat,则Zoo需要能够将an的元素传递给它Animal[],因此其参数类型必须是Animal而不是Cat

在合理地不可避免的情况下,人们应该毫无保留地使用它。关键问题是确定何时可以明智地避免降频,并在合理可能的情况下避免降频。


在字符串的情况下,您应该有一个method Object.equalToString(String string)。这样,您就boolean String.equal(Object object) { return object.equalStoString(this); }无需进行任何下调:您可以使用动态调度。
Giorgio

@Giorgio:动态调度有其用途,但通常比向下转换更糟糕。
超级猫

动态调度通常比向下调度更糟糕?我就这样了
Bryan Chen

@BryanChen:这取决于您的术语。我认为Object没有任何equalStoString虚拟方法,并且我承认我不知道引用的示例在Java中如何工作,但是在C#中,动态调度(与虚拟调度不同)将意味着编译器本质上具有第一次在类上使用方法时进行基于反射的名称查找,这不同于虚拟调度(虚拟调度只是通过虚拟方法表中的插槽进行调用,该插槽需要包含有效的方法地址)。
超级猫

从建模的角度来看,我通常更喜欢动态调度。这也是基于对象的输入参数类型选择过程的面向对象方法。
Giorgio 2014年

1

在接受动物的抽象方法的实现中,我想使用speak()方法。

您有几种选择:

  • 使用反射调用(speak如果存在)。优势:无需依赖Human。缺点:现在对“说话”这个名称有一个隐藏的依赖性。

  • 引入一个新接口Speaker并向下转换到该接口。这比根据特定的混凝土类型更灵活。它的缺点是必须修改Human才能实现Speaker。如果您无法修改,则无法使用Human

  • 下注至Human。这样做的缺点是,每当您要讲另一个子类时,都必须修改代码。理想情况下,您希望通过添加代码来扩展应用程序,而无需反复返回并更改旧代码。

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.