2009-04-09 48 views
2

我这个昨天写在一类Foo从酒吧继承:“拥有”与“是” - 代码味道来决定

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

纳闷随后,如果这是一个可能的迹象,我应该使用一个Bar,而不是从它继承。

什么其他'代码气味'可以帮助选择继承和组成?

编辑我要补充一点,这是一个片段,还有其它方法是常见,我只是不想去考虑太多的细节。我必须分析转换为合成的含义,并想知道是否可能存在其他可能有助于平衡的“代码气味”。

回答

17

上面给出的例子显然是代码味道。 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()方法,因为动物必须呼吸才能生存。如果它没有呼吸,那么根据定义它不是动物。

通过创建一个新的类DogAnimal继承,但支持Breathe()行为,你正在打破由Animal类规定的合同。可怜的狗将无法生存!

公共继承的简单规则是,如果派生类对象确实是“基类”对象,那么您应该只做它。

在你的具体的例子:

  • Foo不支持由Bar合同规定的AddItem()行为。
  • 因此,根据定义,Foo是“不是”Bar,不应该从它继承。
+0

我不知道NotImplementedException是否是一种代码异味 - 异常表明它没有实现_yet_。如果它是一个NotSupportedException,我同意。 – Lennaert 2009-04-09 06:59:42

+0

同意。考虑到我的懒惰 - 我只是复制并粘贴了原来的问题。 – LeopardSkinPillBoxHat 2009-04-09 07:07:53

+0

我修改了我对NotSupportedException的回答。 – LeopardSkinPillBoxHat 2009-04-09 07:21:57

3

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

+0

想想富行为90%喜欢酒吧。而客户端代码只使用这90%的行为。就像使用java.util.List但不修改它的代码一样,只能通过索引迭代并获取它的运行结果。然后你可以给它一个子类,其中设置,添加,删除和清除方法没有实现。 – 2009-04-16 21:10:18

+1

帕维尔:当有人要实际使用的其他10%和编译器不阻止他,因为你已经颠覆了静态类型检查安全网的问题就来了。 – 2009-12-10 22:01:05

1

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

3

是的,你必须“不执行”的方法是一个迹象表明,也许你不应该使用“是”的关系。你的Foos似乎并不真的是酒吧。

但首先想想你的Foos和酒吧。 Foos酒吧?你可以在纸上绘制集合和子集,并且每个Foo(即每个Foo类的成员)也都是一个Bar(即Bar类的成员)吗?如果不是,你可能不想使用继承。

另一个代码的气味,指出FOOS不是真正的酒吧,和富不应该继承吧,是你不能使用多态。假设你有一个方法将Bar作为参数,但它不能处理Foo。 (也许是因为它在它的参数中调用了AddItem方法!)你必须添加一个检查,或者处理NotImplementedException,这使得代码变得复杂而难以理解。 (和闻!)

3

没有!一个正方形可能是一个矩形,但是 一个正方形对象绝对不是 矩形对象。为什么?因为Square对象的 行为不是 ,与 Rectangle对象的行为一致。 行为上, 正方形不是矩形!并且它是 行为那个软件真的全是 一下。

从对象向导中的The Liskov Substitution Principle

0

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

E.g.流有位置属性,但有些流不支持这一点。

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{...} 

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

总而言之一句话说到这个问题,如果你真的可以放心地使用你的子类的父类,考虑到超类是由它的合同,不仅是代码中定义。