【问题标题】:why taking base class ability is bad为什么学习基类能力不好
【发布时间】:2014-08-08 12:54:05
【问题描述】:

我正在阅读Head First Design Patterns 的书。

有一个例子谈到了Duck 基类:

class Duck
{
   public void Quack()
   {
   }

   public void Fly()
   {
   }
}

如果我创建一个不能像这样飞行的子类:

class CantFlyButQuackDuck : Duck
{
    // I can use base Fly() here, but this kind of duck cannot fly!
}

所以,这本书说这很糟糕,因为子类不需要飞行,它必须覆盖任何内容。

但是,如果我不从 Child 类中调用 Fly() 也没关系。那为什么书上说不好呢?我将在我的应用程序中使用这个子类,例如:

CantFlyButQuackDuck  myobj= new CantFlyButQuackDuck();
myobj.Quack();

【问题讨论】:

  • 不叫Fly,不代表不能叫。像现在这些课程的样子,你的 CantFlyButQuackDuck 仍然可以飞。
  • 然后添加 Print()、Scream() 和 DestroyTheWorld() 方法。只要你不打电话给他们……
  • 你给鸭子添加了飞行能力,这是完全错误的。
  • 编写类不仅仅是将一些属性和方法混为一谈。类构成了一个对象模型,它应该是合乎逻辑的和一致的。当一个类以它不应该有的成员结束时,那么你的模型是错误的。大错特错。
  • 面向对象原则的基础是“Is A”与“Has A”的概念。也许这可以澄清:w3resource.com/java-tutorial/…

标签: java c# inheritance design-patterns


【解决方案1】:

如果子类不共享父类的一个或多个特征,那么有什么理由让它继承父类?

继承背后的想法是基类的属性和方法必须由它的所有子类共享。除此之外,每个孩子都可能有自己的特殊属性和方法,以及覆盖继承的方法。在您的情况下,Duck 可以Fly(),所有从它继承的实体也必须如此。仅仅因为你不给孩子打电话Fly,并不意味着孩子不“知道”如何Fly()

事实上,这可能是一个使用接口的好场景。该接口将有一个方法Quack(),您的类可以实现该方法。然后,虽然它与Duck 共享Quack 的能力,但它不能Fly(),因此不会从它继承,同时仍保留一些共享的能力。

为了说明这一点,假设您有一个Whistle,它也可以是Quack,但不是Fly。由于按照您的逻辑,在CantFlyButQuackDuck 上不调用Fly 似乎是合理的,所以在Whistle 上也应该同样合理,只是很明显Whistle 绝不是Duck 的一种。这意味着您应该寻找的是共享某些属性/方法,而不是在您的世界模型中暗示“X 是 Y”的关系。

【讨论】:

    【解决方案2】:

    因为您通常构建类的方式是从最少功能到最多功能,这可以防止意外功能被继承。

    你通常会这样走:

    class Bird 
    {
        public void MakeSound() 
        {
            // insert sound related code here
        }
    }
    

    然后你根据需要继承

    class Duck : Bird 
    {
        public void Fly() 
        {
            // insert flight code here
        }
    }
    
    class FlightlessDuck : Bird {}
    

    这样做你绝对可以肯定不会飞的鸭子不会飞,而鸭子是。这两只鸭子都会叫(发出声音),但只有一只会飞。

    以另一种方式从基类中携带Fly 方法,因此即使您没有在子类中实现任何代码,您仍然可以调用它,这可能会破坏一切.

    更理想的是,您应该添加一个包含所有与飞行相关的方法的接口,这样您就可以确保所有需要飞行的鸟类都遵守相同的标准。但这超出了您的问题范围。

    【讨论】:

      【解决方案3】:

      这就像给孩子糖果并告诉孩子不要吃它!但是,嘿,孩子仍然可以(而且一旦有机会就很可能​​会:D)!

      实际上:
      在行业标准中,您通常应该使用其他人编写的代码或其他人应该使用您的代码。你不能假设某事永远不会完成!

      从根本上说:
      这听起来甚至很荒谬!你是在告诉孩子们,嘿,这是你的父母,这就是你的父母允许你做的事情。现在就像一些真诚的孩子一样,您选择不使用这些功能之一……但您仍然可以。鸭子即使不应该飞也能飞!

      代码:

      CantFlyButQuackDuck  myobj= new CantFlyButQuackDuck();
      myobj.Quack();
      //so okay if you dont call fly() method.Everything is ok!
      
      // 4months later in the code. Bob comes here and observes ... heck! my duck can Fly ... why not!!
      myobj.Fly(); // BOOM!
      

      【讨论】:

        【解决方案4】:

        您有三个选项可以覆盖Fly

        • 不要覆盖,但这意味着会调用基础苍蝇,并且它可能会在不应该这样做时飞行。
        • 用空方法覆盖。这至少会停止调用 base,但调用者不会从它那里得到任何反馈。
        • 覆盖 fly 并引发异常。这不会阻止开发人员调用它,但会停止在运行时调用 fly。

        这取决于你的情况。你想在想知道为什么当你告诉它时某只鸭子没有飞起来时你会挠头吗?或者当你试图让那只鸭子飞起来时,你想让程序崩溃吗?

        在实践中,我们宁愿有一个替代解决方案:

        目前,您的基类说这两种方法是内聚的。如果后代实现了一个,它必须实现两者或保持基本行为。所以要解决这个问题,我们可以将segregate your interfaces 转换成有凝聚力的单元:

        interface IFly {
          void Fly();
        }
        interface IQuack {
          void Quack();
        }
        

        或者我们可以通过稍后在树中添加fly 方法来解决:

        class QuackingDuck
        {
           public void Quack()
           {
           }
        }
        
        class FlyingDuck : QuackingDuck
        {
           public void Fly()
           {
           }
        }
        
        class CantFlyButQuackDuck : QuackingDuck
        {
        }
        

        或者你可以将这两种技术结合起来。

        【讨论】:

          【解决方案5】:

          无论您是否选择调用Fly(),它可以被调用(不正确),并且成为所有CantFlyButQuackDuck对象的成员,尽管事实上,这个成员在不能(也不应该)用它做任何事情的对象的上下文中没有位置。

          类似的问题可以应用于许多 OOP 原则。这只是一种以可扩展且有意义的方式组织和分解代码的情况。如果您以现有方式扩展Duck,您的代码将编译并运行良好,但稍后您会遇到问题。

          【讨论】:

            【解决方案6】:

            问题在于,您通常不会为自己设计类层次结构,而是为团队中其他人或客户使用的包设计。并且由于子类仍然具有某些人不可避免地会尝试使用它的方法。

            对于现实世界的问题,这可能会导致您不希望发生的数据操作。 例如:有人可以让你在地面上的鸭子飞起来,但由于它不知道如何着陆,所以喂食会遇到问题。

            【讨论】:

              【解决方案7】:

              实际上,设计的意义在于其用例以及与其他实体的关系。你真的能说不会飞的鸭子是连鸭子吗?

              我会争辩说,一开始就有一个错误的假设——所有鸭子都可以飞,但这里有一个特殊情况,鸭子实际上不能飞。但是这个错误很容易犯,因为鸭子会飞的假设是正确的。

              但如果我不从 Child 类调用 Fly() 也没关系。那为什么这本书说它不好呢?

              这通常不是一个伟大的设计,因为它给出了一个错误的想法,即它可以像所有其他鸭子一样飞行。这并不是说它是错误的 - 你可以让它工作,它只是不理想,并且必须在你的文档中非常清楚地说明 Fly 不会为特定类型的 Duck 做任何事情。

              我想到了组合而不是继承...继承和组合肯定有好有坏——在某些需要继承的情况下,即依赖注入——所以是行为的概括。

              在这种情况下,您可能需要继承,以便您可以遍历鸭子并让它们一起嘎嘎叫或一起飞,而无需知道鸭子集合中有什么类型的鸭子。

              与此问题相关的同一域中的问题:https://softwareengineering.stackexchange.com/questions/75189/why-avoid-java-inheritance-extends

              【讨论】:

                猜你喜欢
                • 2017-01-13
                • 2021-02-23
                • 1970-01-01
                • 2019-09-06
                • 1970-01-01
                • 2017-01-30
                • 1970-01-01
                • 2022-07-10
                • 2015-02-24
                相关资源
                最近更新 更多