【问题标题】:Does Inheritance contradict Dependency Inversion Principle继承是否与依赖倒置原则相矛盾
【发布时间】:2019-09-28 01:43:07
【问题描述】:

依赖倒置原则说(Head First Java):

  • 依赖于抽象。不要依赖具体的类。

继承是什么意思?因为子类依赖于具体类。

我问的是一个案例——有一个接口Bird(没有飞方法,因为有些鸟不能飞),它代表所有非会飞的鸟。所以我创建了一个类——NonFlyingBird,它实现了Bird。

现在我想为会飞的鸟创建一个课程。由于NonFlyingBirds 和FlyingBirds 具有相同的属性,我从NonFlyingBirds 扩展FlyingBirds 并实现Flyable 以使其具有飞行行为。

由于FlyingBirds 是从一个具体类NonFlyingBirds 扩展而来,它不会破坏依赖倒置原则吗?

interface Bird {   // Represents non Flying birds

    void getColor();
    void getHeight();
    ...
 }



class NonFlyingBird implements Bird {
     void getColor();
     void getHeight();
      ...
  }


class FlyingBird extends NonFlyingBird implements Flyable { // Does it break Dependency Inversion principle by extending concrete NonFlyingBird class? 

    Flyable fly;
    ...
  }

注意 - 我扩展的唯一原因是因为 FlyingBird 具有与 NonFlyingBird 相同的属性和方法 + 飞行行为。所以通过继承重用代码是有意义的。

【问题讨论】:

  • 现在每一个FlyingBird也是一个NonFlyingBird,而你违反了Liskov's substitution principle
  • @JohannesKuhn 但我想不出为什么用NonFlyingBird 替换FlyingBird 会导致任何问题?
  • 也许不是现在,但下个月会有下一个实习生说“我知道,我只是使用 instanceof 检查它是非飞鸟还是飞鸟”,然后会诅咒你的决定。
  • @JohannesKuhn 嗯,好点。在我看来,我需要为移动和非移动的鸟类创建两个不同的类。对吗?
  • 您在下面有一些很好的答案,但问题是错误的。当你对继承做了一些糟糕的事情时,继承不是问题。问题是你用它做了什么。不要那样做。

标签: java oop design-patterns dependency-inversion


【解决方案1】:

尽管我喜欢那里的答案中的策略示例,但我也会回答,因为我认为您对依赖倒置原则有点困惑。

依赖倒置原理的意思

如果您真的不需要 LinkedList 行为,请不要使用它:

public class A {
    private LinkedList<SomeClass> list;
//...
}

改用它:

public class A {
    private List<SomeClass> list; //or even use Collection or Iterable
//...
}

依赖是我们在类中使用的。

继承

继承就是我们所说的IS-A关系,与原则无关。如果要让A类继承B类,你需要回答一个问题:A是B是真的吗?如果你问这个问题,你会发现表达式“FlyingBird is a NonFlyingBird”是废话。

通过继承重用代码

想一想:不是所有的鸟都会飞,也不是只有鸟(例如苍蝇)会飞。 它可能会让我们产生一个想法,即我们应该像您已经完成的那样创建接口 Flyable。然后我们应该将NonFlyingBird 重命名为SimpleBird,因为如果某个生物是鸟,并不意味着它可以飞。最后你会得到:

class FlyingBird extends SimpleBird implements Flyable { 

    void fly() {
        ...
    }
    ...
}

希望对你有所帮助。

【讨论】:

  • 您能否解释一下,“不是所有的鸟都会飞,也不是只有鸟会飞。”?
  • @lynxx,对不起,我写的很复杂。我更新了帖子。
【解决方案2】:

简短回答:不。

更长的答案:使用策略模式。

依赖于Bird 接口的类仍然可以在不知道区别的情况下传递FlyingBird 或NotFlyingBird 实例。

Bird 的接口还是一样的,或者一般来说应该是一样的,如果类确实有不同的接口(你的调用代码依赖的FlyingBird 中的新方法),那么就有问题了。

也许解决问题的更好方法是使用策略模式。

即兴例子:

public interface Bird {

    void fly();
}

public class BirdImpl implements Bird {

    private FlightStrategy flightStrategy;

    public BirdImpl(FlightStrategy flightStrategy) {

        this.flightStrategy = flightStrategy;
    }

    public void fly() {

        this.flightStrategy.fly();
    }
}

public interface FlightStrategy {

    void fly();
}

public class FlyingBirdFlightStrategy implements FlightStrategy {

    public void fly() {

        System.out.println("Wings flap");
        System.out.println("Wings flap");
        System.out.println("Wings flap");
        System.out.println("Wings flap");
    }
}

public class NonFlyingBirdFlightStrategy implements FlightStrategy {

    public void fly() {

        // do nothing non flying birds can't fly.
    }
}

然后在创建您的Bird 以供使用时,创建默认的BirdImpl 并传入您想要的鸟类型的FlightStrategy。

【讨论】:

  • 那些不会飞的鸟呢?他们在这个设计中是如何表现的?
  • 嗯,好点。我想音乐引用不是很好的例子......我会简化。但基本上,不会飞的鸟的飞行方法,也没什么用。不会飞的鸟是不会飞的。
  • 使用空方法有用吗?我在某处读到这是我需要重构的标志。这就是我提出该架构(继承)的全部要点。
  • 嗯,这取决于。在我看来,空的、没有行为的方法本身并没有什么问题。在这种特殊情况下,我觉得这是正确的行为。我认为fly() 是一种非常合理的方法,您希望在名为Bird 的接口上找到它。而且我认为,当你把一只不会飞的鸟捡起来扔到空中时(当然是轻轻地),正确的行为是不要飞。
【解决方案3】:

是的,您的直觉是正确的 - 继承在子代与其父代之间引入了不可破坏的编译时和运行时依赖关系。

因此继承的应用是有限的:你应该只使用继承来实现“is”关系,而不是“has code to be shared”关系。但是,您应该小心:仅当对象在整个生命周期中“是”其子类时才继承。 Human 可以安全地扩展 Mammal,但是将 Student 定义为 Human 的子类是有风险的。人类可以不再是学生,你不能在运行时改变它。

class FlyingBird extends NonFlyingBird

这是异端! :)

反对“AbstractBird”之类的模板类的运动非常激烈。不幸的是,许多介绍性的编程书籍都教授这种代码共享模式。它本质上没有任何问题,但现在有更好的解决方案——策略、桥牌。在 Java 中,您甚至可以拥有穷人的特质 - 接口中的默认实现。

在我看来,依赖倒置原则在应用于继承时会转化为“优先组合优于继承”。

【讨论】:

  • 请注意,Strategy 和 Bridge(以及所有 GoF 模式)已有 30 多年的历史,所以说“现在有更好的解决方案”有点奇怪。这些解决方案已经存在了几十年。不过,完全同意继承是一种具体的依赖。看看我用“is a”做了什么?
猜你喜欢
  • 1970-01-01
  • 2015-06-14
  • 2017-09-09
  • 2012-05-09
  • 1970-01-01
  • 2013-07-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多