2

我昨天在继承自 Bar 的类 Foo 中写了这个:

public override void AddItem(double a, int b)
{
    //Code smell?
    throw new NotImplementedException("This method not usable for Foo items");
}

随后想知道这是否可能表明我应该使用Bar,而不是从它继承。

还有哪些“代码气味”可以帮助在继承和组合之间进行选择?

编辑我应该补充一点,这一个片段,还有其他共同的方法,我只是不想太详细。我必须分析切换到作曲的含义,并想知道是否还有其他“代码气味”可以帮助打破平衡。

4

8 回答 8

18

您在上面给出的示例显然是代码异味。该AddItem方法是基类的一种行为Bar。如果Foo不支持该AddItem行为,则不应继承自Bar.

让我们考虑一个更现实的 (C++) 示例。假设您有以下课程:

class Animal
{
    void Breathe() const=0;
}

class Dog : public Animal
{
    // Code smell
    void Breathe() { throw new NotSupportedException(); }
}

基本抽象类Animal提供了一个纯虚Breathe()方法,因为动物必须呼吸才能生存。如果它不呼吸,那么根据定义,它就不是动物。

通过创建一个Dog继承自该行为Animal支持该Breathe()行为的新类,您违反了Animal该类规定的合同。可怜的狗活不下去了!

公共继承的简单规则是,只有在派生类对象真正“是”基类对象时才应该这样做。

在您的特定示例中:

  • Foo不支持合同AddItem()约定的行为Bar
  • 因此,根据定义,Foois "not a" Bar,不应该从它继承。
于 2009-04-09T06:22:31.527 回答
3

那么,如果没有扩展的 Foo 的行为不像 Bar,为什么要从 Bar 继承呢?想想看,我什至不会在基类中将像“AddItem”这样的方法声明为虚拟的。

于 2009-04-09T06:20:25.057 回答
3

是的,您必须“取消实现”方法表明您可能不应该使用“is-a”关系。您的 Foos 似乎并不是真正的酒吧。

但首先要考虑你的 Foos 和 Bars。是Foos酒吧吗?你能在纸上画出集合和子集吗,每个 Foo(即 Foo 类的每个成员)也会是 Bar(即 Bar 类的成员)吗?如果没有,您可能不想使用继承。

另一个表明 Foos 不是真正的 Bars 并且 Foo 不应该继承 Bar 的代码味道是您不能使用多态性。假设您有一个将 Bar 作为参数的方法,但它无法处理 Foo。(可能是因为它在其参数中调用了 AddItem 方法!)您必须添加一个检查或处理 NotImplementedException,这使代码变得复杂且难以理解。(还有气味!)

于 2009-04-09T06:39:04.870 回答
3

不!正方形可能是矩形,但 Square 对象绝对不是 Rectangle 对象。为什么?因为 Square 对象的行为与 Rectangle 对象的行为不一致。从行为上讲,正方形不是矩形!软件真正关注的是行为。

来自Object Mentor 中的 Liskov 替换原则

于 2009-04-09T07:17:40.427 回答
1

当然也有直接的反面,如果你的 Foo 实现了很多只将消息转发到它拥有的 Bar 的接口,这可能表明它应该一个 Bar。只是,气味并不总是正确的。

于 2009-04-09T06:21:19.453 回答
1

虽然上面的示例可能表明出现了问题,但 NotImplementedException 本身并不总是错误的。这都是关于超类的契约和实现这个契约的子类。如果你的超类有这样的方法

// This method is optional and may be not supported
// If not supported it should throw NotImplementedException 
// To find out if it is supported or not, use isAddItemSupported()
public void AddItem(double a, int b){...}

然后,合同不支持这种方法仍然可以。如果不支持,您可能应该在 UI 中禁用相应的操作。所以如果你对这样的合同没意见,那么子类就会正确地实现它。

当客户端明确声明它不使用所有类方法并且永远不会使用时的另一个选择。像这样

// This method never modifies orders, it just iterates over 
// them and gets elements by index.
// We decided to be not very bureaucratic and not define ReadonlyList interface,
// and this is closed-source 3rd party library so you can not modify it yourself.
// ha-ha-ha
public void saveOrders(List<Order> orders){...}

然后可以传递不支持添加、删除和其他变异器的 List 的实现。只需记录它。

// Use with care - this implementation does not implement entire List contract
// It does not support methods that modify the content of the list.
public ReadonlyListImpl implements List{...}

虽然让你的代码来定义你的所有合约是好的,因为它让你的编译器检查你是否违反了合约,但有时这是不合理的,你必须求助于弱定义的合约,比如注释。

简而言之,问题在于,如果您真的可以安全地将子类用作超类,考虑到超类是由其合同定义的,而不仅仅是代码。

于 2009-04-16T21:04:12.787 回答
0

.Net 框架有这样的例子,特别是在 System.IO 命名空间中——一些读者没有实现他们所有的基类属性/方法,如果你尝试使用它们会抛出异常。

例如 Stream 有一个 position 属性,但是有些流不支持这个。

于 2009-04-09T07:19:59.937 回答
0

组合比继承更可取,因为它降低了复杂性。更好的方法是使用构造函数注入并在 Foo 类中保留对 Bar 的引用。

于 2009-04-09T18:37:27.380 回答