“有一个”vs“是一个” - 代码味道决定

时间:2009-04-09 06:11:13

标签: oop inheritance composition

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

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

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

还有哪些“代码味道”可以帮助在继承和合成之间做出选择?

编辑我应该补充一点,这是一个片段,还有其他 共同的方法,我只是不想详细介绍。我必须分析切换到合成的含义,并想知道是否可能有其他“代码味道”可能有助于提高平衡。

8 个答案:

答案 0 :(得分: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规定的合同{1}}课程。可怜的狗将无法生存!

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

在您的特定示例中:

  • Foo不支持AddItem()合同规定的Bar行为。
  • 因此,根据定义,Foo“不是”Bar,不应继承。{/ li>

答案 1 :(得分:3)

那么为什么你会继承Bar,如果没有扩展名的Foo不像Bar那样? 想想看,我甚至不会在基类中将'AddItem'这样的方法声明为虚拟。

答案 2 :(得分:3)

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

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

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

答案 3 :(得分:3)

  

没有!正方形可能是一个矩形,但是   Square对象肯定不是   矩形对象。为什么?因为   Square对象的行为不是   符合a的行为   矩形对象。 行为,a   Square不是Rectangle!它是   软件真的全部的行为   约。

来自对象导师中的The Liskov Substitution Principle

答案 4 :(得分:1)

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

答案 5 :(得分: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){...}

然后可以通过那些不支持添加,删除和其他mutator的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{...}

虽然让您的代码定义所有合同是好的,因为它让您的编译器检查您是否违反合同,有时它是不合理的,您必须诉诸弱定义的合同,如评论。

简而言之,问题是,如果你真的可以安全地使用你的子类作为超类,考虑到超类是由它的契约定义的,而不仅仅是代码。

答案 6 :(得分:0)

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

E.g。 Stream有一个位置属性,但有些流不支持此属性。

答案 7 :(得分:0)

组合是继承preferable,因为它降低了复杂性。更好的方法是使用constructor injection并在你的Foo类中保留对Bar的引用。