【问题标题】:"Has a" vs "Is a" - code smells for deciding“有一个”与“是一个”——决定的代码味道
【发布时间】:2009-04-09 06:11:13
【问题描述】:

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

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

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

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

EDIT我要补充的是,这是一个sn-p,还有其他方法共同的,我只是不想太详细.我必须分析切换到作曲的含义,并想知道是否可能存在其他“代码气味”有助于打破平衡。

【问题讨论】:

    标签: oop inheritance composition


    【解决方案1】:

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

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

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

    在您的特定示例中:

    • Foo 不支持Bar 合约规定的AddItem() 行为。
    • 因此,根据定义,Foo“不是”Bar,不应继承自它。

    【讨论】:

    • 我不知道 NotImplementedException 是否是代码异味——该异常向我表明它尚未实现。如果是 NotSupportedException,我同意。
    • 同意。考虑一下我的懒惰 - 我只是从原始问题中复制并粘贴它。
    • 我修改了对 NotSupportedException 的回答。
    【解决方案2】:

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

    【讨论】:

    • 想想 Foo 90% 的行为都像 Bar。以及只使用这 90% 行为的客户端代码。就像使用 java.util.List 但不修改它的示例代码一样,仅通过索引迭代和获取事件。然后你可以给它一个子类,其中没有实现 set、add、remove 和 clear 方法。
    • Pavel:当有人想要实际使用另外 10% 并且编译器没有阻止他,因为你已经破坏了静态类型检查安全网时,问题就出现了。
    【解决方案3】:

    不!正方形可能是长方形,但 Square 对象绝对不是 矩形对象。为什么?因为 Square 对象的行为不是 符合a的行为 矩形对象。 在行为上,一个 正方形不是矩形!它是 软件就是一切的行为 关于。

    来自 Object Mentor 中的 The Liskov Substitution Principle

    【讨论】:

    • 我认为决定 Rectangle 和 Square 的行为取决于开发人员。如果开发人员对 Rectangle 的唯一需要是 .getHeight() 和 .getWidth() ,那么 Square 在行为上就是一个 Rectangle。如果开发人员需要类似 .setSize(windth,height) 的东西,那么 Square 可能不是 Rectangle
    • @Pavel:当然。这个决定属于类设计者。如果您转到文档并阅读其上下文中的引用,您会发现形状是可调整大小的 - 顺便说一句,在讨论继承和分类时,这是唯一有趣的例子。
    • @KaptajnKold 谢谢。我很高兴它很有用。那是很久以前的事了,去年。
    【解决方案4】:

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

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

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

    【讨论】:

    • 我不知道谁反对这个,但你给出的解释非常清楚和有用。投反对票的人应该花时间解释为什么他们发现有问题。
    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

      虽然上面的示例可能表明出现了问题,但 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){...}
      

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

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

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

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

      【讨论】:

        【解决方案7】:

        .Net 框架有这样的示例,尤其是在 System.IO 命名空间中 - 一些阅读器没有实现其所有基类属性/方法,如果您尝试使用它们会抛出异常。

        例如流有一个位置属性,但有些流不支持这个。

        【讨论】:

        • 这是一个糟糕设计的例子。另一件事是,如果您有一个像“IsPositionSupported”这样的属性,根据设计,您必须在使用“Position”之前将其作为先决条件进行查询;那么至少你会从一开始就知道“有些流有位置,有些则没有”。
        • 拥有一个受支持的标志绝对是一个好主意,但我可以看到这背后的原因 - 从流继承的 8 个流中大约有 7 个启用了位置,但最后一个,虽然它本质上是流,不能返回位置或搜索。
        【解决方案8】:

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

        【讨论】:

          猜你喜欢
          • 2011-04-14
          • 1970-01-01
          • 2020-06-28
          • 1970-01-01
          • 2011-07-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多