这是我参与更文挑战的第7天,活动详情查看:更文挑战
1 多层次继承代码的不可维护性
假设我们要设计一个关于“鸟”的类。这时我们抽象出“Bird”这个抽象类,所有鸟类都要直接或间接继承这个抽象类。一般的鸟类都有“飞”的行为,于是我们定义一个 fly()
抽象方法。
public Abstract class Bird{
public Abstract void fly();
}
public class Sparrow extends Bird { //麻雀
public void fly() {
System.out.println("我能飞八尺高");
}
}
public class Roc extends Bird { //大鹏
public void fly() {
System.out.println("扶摇直上九万里");
}
}
复制代码
开始还好好的,每种鸟类都实现这个 fly()
方法,后来从袋鼠国来一只鸵鸟,从南极来了一只企鹅。鸵鸟和企鹅明显不会飞,要他们去实现fly()
方法好像不太正确。于是又生成两个抽象类继承与“Bird”,一个是FlyableBird,一个是UnFlyableBird。麻雀和大鹏继承FlyableBird,而鸵鸟和企鹅继承UnFlyableBird。这样继承层次从两层变为了三层。
但我们不止关心鸟会不会飞,还要关心鸟会不会下蛋、会不会叫。如果沿用相似的设计思路,继承的层次会继续加深,关系也越来越复杂,最后影响代码的可读性和可维护性。
2 组合优先于继承
如果某个场景的代码复用既可以通过类继承实现, 也可以通过对象组合实现, 尽量选择对象组合的设计方式。
面向对象设计的过程中, 两个最常用的技巧就是类继承和对象组合,同一个场景下的代码复用,这两个技巧基本上都可以完成。 但是他们有如下的区别:
- 通过继承实现的代码复用常常是一种**“白盒复用”**, 这里的白盒指的是可见性: 对于继承来说,父类的内部实现对于子类来说是不透明的(实现一个子类时, 你需要了解父类的实现细节, 以此决定是否需要重写某个方法)
- 对象组合实现的代码复用则是一种**“黑盒复用”“: 对象的内部细节不可见,对象仅仅是以“黑盒”**的方式出现(可以通过改变对象引用来改变其行为方式。
3 传递影响
假设我要造一个会飞的汽车,这时有两种做法
- 继承一个车的抽象类,实现
Flyer
接口 - 编写一个FlyCar的抽象类,这个类继承车的抽象类,同时实现接口
Flyer
;所有会飞的汽车都继承这个会飞的车抽象类。
- 图中箭头方向为依赖方向。依赖方向反向为影响方向。
- 也就是说:
- 左图中,Car、Flyer、FlyCar变化时,都会影响到MyCar。
- 右图中,Car、Flyer变化时,会影响到MyCar。
影响传递不一定是坏事。比如,你有个FlyCar的产品线,FlyCar有很多种,由FlyCar来定义通用的规则。那这个影响传递就是好事,是需要的。
再比如,你设计时,只是打造了一个概念,注重汽车具有飞行能力,然后也不知道后续有什么需求、扩展项,那就要尽可能灵活,减小影响的传递。
4 小结
以上这么多,似乎没有一个明确的指导,到底该用抽象类还是接口。实际上也是如此,前人通过经验总结,给出很多思想指导原则,这些原则都是作为指导思想而非条条框框。在实际开发中,具体使用抽象类还是接口,并没有绝对限制,而是取决于你的业务场景和架构设计。在灵活性上,组合优于继承。但实际决策中,不一定灵活性占主导的。灵活性是有代价的,要增加复杂性的。需求如果很明确,那也不需要很灵活。