7

我在一个 Java 程序员团队中工作。我的一位同事不时建议我做一些类似“只需添加一个类型字段”(通常是“字符串类型”)的事情。或者代码将被提交满载“ if (foo instanceof Foo){...} else if( foo instanceof Bar){...}”。

尽管 Josh Bloch 告诫说“标记类是对适当类层次结构的模仿”,但我对这类事情的单线反应是什么?然后我如何更认真地阐述这个概念?

我很清楚 - 上下文是 Java - 所考虑的对象类型就在我们集体面前 - IOW:紧跟在“类”、“枚举”或“接口”等之后的词。

但除了难以演示或量化(现场)“它使你的代码更复杂”之外,我怎么说“用(或多或少)强类型语言进行鸭式打字是一个愚蠢的想法,暗示了更深层次的设计病态?

4

11 回答 11

8

其实你说的还不错。

事实是,comb 的“实例”几乎总是一个坏主意(例如,当您进行编组或序列化时会发生异常,在很短的时间间隔内您可能没有手头的所有类型信息。)正如 josh 所说,否则这是一个糟糕的类层次结构的标志。

知道这是一个坏主意的方式是它使代码变得脆弱:如果您使用它,并且类型层次结构发生变化,那么它可能会在它出现的任何地方破坏该梳子的实例。更重要的是,你会失去强类型的好处;编译器无法通过提前捕获错误来帮助您。(这有点类似于 C 中类型转换引起的问题。)

更新

让我稍微扩展一下,因为从评论看来我不太清楚。您在 C 中使用类型转换的原因,或者instanceof,您想说“好像”的原因:将其foo用作bar. 现在,在 C 中,根本没有运行时类型信息,所以您只是在没有网络的情况下工作:如果您对某些内容进行类型转换,生成的代码将把该地址视为包含特定类型,无论如何,并且您应该只希望它会导致运行时错误,而不是默默地破坏某些东西。

鸭子打字只是将其提升为常态。在像 Ruby、Python 或 Smalltalk 这样的动态弱类型语言中,一切都是无类型引用;你在运行时向它发送消息,看看会发生什么。如果它理解一个特定的信息,它就会“像鸭子一样走路”——它会处理它。

这可能非常方便和有用,因为它允许奇妙的 hack,例如将生成器表达式分配给 Python 中的变量,或将块分配给 Smalltalk 中的变量。但这确实意味着您在运行时容易受到强类型语言在编译时可以捕获的错误的影响。

在像 Java 这样的强类型语言中,您根本不能真正严格地进行鸭式类型:您必须告诉编译器您将要处理的东西是什么类型。您可以通过使用类型转换来获得类似鸭子类型的东西,这样您就可以执行类似的操作

Object x;   // A reference to an Object, analogous to a void * in C

// Some code that assigns something to x

((FoodDispenser)x).dropPellet();          // [1]

// Some more code

((MissleController)x).launchAt("Moon");   // [2]

现在在运行时,只要 x 是一种FoodDispenserat [1] 或MissleControllerat [2] 就可以了;否则繁荣。或者出乎意料,没有繁荣

在您的描述中,您通过使用else ifinstanceof

 Object x ;

 // code code code 

 if(x instanceof FoodDispenser)
      ((FoodDispenser)x).dropPellet();
 else if (x instanceof MissleController )
      ((MissleController)x).launchAt("Moon");
 else if ( /* something else...*/ ) // ...
 else  // error

现在,您可以免受运行时错误的影响,但您有责任稍后在else.

但是现在假设您对代码进行了更改,因此“x”可以采用“FloorWax”和“DessertTopping”类型。您现在必须检查所有代码并找到该梳子的所有实例并修改它们。现在代码是“脆弱的”——需求的变化意味着大量的代码变化。在 OO 中,您正在努力使代码不那么脆弱。

OO 解决方案是使用多态性,您可以将其视为一种有限的鸭子类型:您定义了可以信任执行的所有操作。你可以通过定义一个高级类来做到这一点,可能是抽象的,它具有低级类的所有方法。在 Java 中,像这样的类最好表示为“接口”,但它具有类的所有类型属性。实际上,您可以将接口视为一种承诺,即可以信任特定类以“就好像”它是另一个类一样。

  public interface VeebleFeetzer { /* ... */ };
  public class FoodDispenser implements VeebleFeetzer { /* ... */ }
  public class MissleController implements VeebleFeetzer { /* ... */ }
  public class FloorWax implements VeebleFeetzer { /* ... */ }
  public class DessertTopping implements VeebleFeetzer { /* ... */ }

您现在所要做的就是使用对 VeebleFeetzer 的引用,编译器会为您计算出来。如果您碰巧添加了另一个属于 VeebleFeetzer 子类型的类,编译器将选择该方法并检查交易中的参数

VeebleFeetzer x;   // A reference to anything 
                   // that implements VeebleFeetzer

// Some code that assigns something to x

x.dropPellet();

// Some more code

x.launchAt("Moon");

 
    
于 2009-03-31T05:33:23.067 回答
6

这与其说是鸭式打字,不如说是正确的面向对象风格;事实上,能够继承类 A 并在类 B 上调用相同的方法并让它做其他事情是语言中继承的全部要点。

如果你经常检查一个对象的类型,那么你要么太聪明了(尽管我认为这是鸭子打字爱好者喜欢的这种聪明,除了不那么脆弱的形式)或者你没有接受对象的基础知识面向编程。

于 2009-03-31T05:29:31.937 回答
3

嗯……

如果我错了,请纠正我,但标记类和鸭子打字是两个不同的概念,尽管不一定相互排斥。

当一个人有在类中使用标签来定义类型的冲动时,恕我直言,应该修改他们的类层次结构,因为这是一个明显的概念流血,抽象类需要知道类父级尝试的实现细节隐藏。你使用正确的模式吗?换句话说,您是否试图以一种不自然地支持它的模式强制行为?

鸭子类型是一种松散定义类型的能力,只要定义了参数实例中的必要方法,方法就可以接受任何类型。然后该方法将使用该参数并调用必要的方法,而不会过多地打扰实例的父级。

所以在这里......正如查理指出的那样,臭味提示是使用instanceof。很像静态或其他臭名昭著的关键字,每当它们出现时,人们必须问“我在这里做正确的事吗?”,并不是说它们天生就是错误的,而是它们经常被用来破解糟糕或不合适的 OO 设计。

于 2009-03-31T05:48:31.437 回答
2

我的一个回应是你失去了 OOP 的主要好处之一:多态性。这减少了开发新代码的时间(开发人员喜欢开发新代码,所以这应该有助于你的论点:-)

如果在向现有系统添加新类型时,除了确定要构造哪个实例之外,您还必须添加逻辑,那么在 Java 中,您做错了(假设新类应该只是替换为了另一个)。

通常,在 Java 中处理此问题的适当方法是保持代码多态并利用接口。因此,每当他们发现自己想要添加另一个变量或执行 instanceof 时,他们可能应该改为实现接口。

如果您可以说服他们更改代码,那么将接口改装到现有代码库中是很容易的。就此而言,我会花时间用 instanceof 编写一段代码并将其重构为多态。如果他们能看到前后版本并进行比较,人们就更容易明白这一点。

于 2009-03-31T07:03:08.980 回答
2

您可能想向您的同事指出 Liskov 替换原则,它是 SOLID 中的五个支柱之一。

链接:

于 2009-03-31T07:04:10.757 回答
1

虽然我通常是像 python 这样的鸭式语言的粉丝,但我可以在 java 中看到你的问题。

如果您正在编写将与此代码一起使用的所有类,那么您不需要鸭式类型,因为您不需要考虑代码不能直接继承(或实现)的情况接口或其他统一的抽象。

鸭子类型的一个缺点是你有一个额外的单元测试类要在你的代码上运行:一个新类可能返回与预期不同的类型,并随后导致其余代码失败。因此,尽管鸭式打字允许向后灵活性,但它需要大量的前瞻性思维来进行测试。

简而言之,你有一个包罗万象(硬)而不是几个(容易)。我认为这就是病理学。

于 2009-03-31T05:30:19.250 回答
1

为什么“模仿一个类层次结构”而不是设计和使用它?重构方法之一是用多态性替换“switch”es(链式 if 几乎相同)。为什么在多态性会导致代码更简洁的地方使用开关?

于 2009-03-31T05:31:05.410 回答
1

当您说“在强类型语言中输入鸭子”时,实际上是指“在静态类型语言中模仿(子类型)多态性”。

当您拥有不包含任何行为的数据对象 (DTO) 时,情况还不错。当您确实拥有成熟的 OO 模型时(问问自己是否真的如此),那么您应该在适当的情况下使用该语言提供的多态性。

于 2009-03-31T07:43:45.157 回答
1

这不是鸭式打字,它只是在具有(或多或少)真正多态性的语言中模拟多态性的一种糟糕方法。

于 2009-03-31T14:26:46.327 回答
0

回答标题问题的两个论据:

1) Java 应该是“一次编写,随处运行”,因此为一个层次结构编写的代码不应该在我们改变某处环境时抛出 RuntimeExceptions。(当然,这条规则也有例外——双关语。)

2) Java JIT 执行非常激进的优化,依赖于知道给定符号必须是一种类型且仅一种类型。解决此问题的唯一方法是强制转换。

正如其他人所提到的,您的“实例”与我在这里回答的问题不匹配。任何类型、鸭子或静态的任何东西都可能存在您描述的问题。有更好的 OOP 方法来处理它。

于 2009-03-31T06:12:09.040 回答
0

您可以使用 Method- 和 Strategy-Pattern 而不是 instanceof,将代码混合在一起看起来比以前好多了...

于 2009-03-31T07:06:54.340 回答