为什么强烈不建议使用继承
这两天有空的时候看了下 继承和复合如何选择这个知识点,其实之前开发的时候遇到类似的问题我是无脑继承的,也没有考虑这么多,因为这些新增的父子类,都是在包内使用,而且父子类基本都是我们同一个开发人员,所以一般不会有什么意外情况。
但是如果我们要开发新的类,这个类需要对外开放,有很多模块会来继承我们的类(或者我们会继承第三方提供的公共类),这个时候就需要很小心的设计了,如果小伙伴们觉得以后不会接到这样的需求,其实就不用继续看的,下面内容还是有点枯燥无聊的🤣。
学习的内容
1. 继承(Inheritance)是什么
继承其实不用过多的去解释,因为大家都是非常熟悉的,它和封装(encapsulation) 、抽象(abstraction) 、多态(polymorphism) 组成面向对象编程的(Object-Oriented Programming)主要特征。
- 代码示例
//父类
public class Animal {
//名称
protected String name;
//种类
String species;
protected String getName() {
return name;
}
protected void setName(String name) {
this.name = name;
}
protected String getSpecies() {
return species;
}
protected void setSpecies(String species) {
this.species = species;
}
}
//子类
public class Birds extends Animal {
//翅膀长度
protected String wingSize;
protected String getWingSize() {
return wingSize;
}
protected void setWingSize(String wingSize) {
this.wingSize = wingSize;
}
}
总结:
继承的优点:
- 子类可以复用父类的代码,继承父类的特性,可以减少重复的代码量
- 父子类之前结构层次更加清晰
继承的缺点:
- 父子类之间属于强耦合性,一旦父类改动(比如增加参数),很可能会影响到子类,这就导致代码变得脆弱
- 如果子类新增一个方法,但是后续父类升级之后,和子类的方法签名相同返回类型不同,这会导致子类编译失败
- 会破坏封装性
- 不能进行访问控制
缺点第三条的解释:
下面是新建了一个集成HashSet的类,主要目的是想统计这个实例一共添加过多少次元素
- addAll:批量增加数据
- add:单个数据增加
最终统计出来的结果是 4 ,只是因为 super.addAll(c) 最终会调用add方法,也就导致重复计数了。
出现这种情况是因为我们在编写子类逻辑时不清楚父类方法的实现细节,从而造成了错误,即使我们把add中的addCount++ 删除,也同样不能保证父类的逻辑会不会变动,这样就会导致子类非常脆弱且不可控,简单总结就是子类依赖了父类的实现细节,所以这就是为什么会说破坏了封装性。
封装性:将数据和行为结合,形成一个整体,使用者不用了解内部细节,只能通过对外提供的接口进行访问
@Slf4j
public class DestroyInheritance<E> extends HashSet<E> {
private int addCount = 0;
@Override
public boolean add(E o) {
addCount++;
return super.add(o);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
//测试类,使用addAll批量增加
DestroyInheritance<String> item = new DestroyInheritance<>();
String[] arr = new String[]{"s","a"};
item.addAll(Arrays.asList(arr));
log.info("count:{}",item.getAddCount());
}
}
2.复合是什么
复合从字面意思上也是可以理解的,就是将多个实例的行为和特征组合成一个新的类,简单理解就是新增的这个类,它拥有其他一个或者多个类的特征,比如家用汽车有轮子、底盘、发动机等等组件组成,那车子这个类就包含了轮子类和底盘类这些属性。
看一下下面这段代码,Car是一个使用了复合的类,他包含了引擎类Engine和轮胎类Tyre,那为什么要这样写呢,我的想法是有下面几点:
- 在doSomething方法中,我无需全部继承引擎类或者轮胎类,只需要根据实际情况调用某些方法即可,减少了之前对父类的严重依赖,造成的耦合性影响
- 引擎类只需要提供个别公共的方法给Car类使用,不需要完全暴露其内部细节,也不用担心会出现类似addAll最终调用add的问题
- 代码示例
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* 汽车类
**/
@Slf4j
public class Car {
//引擎类实例
private final Engine engine;
//轮胎类实例
private final Tyre tyre;
public Car(Engine engine,Tyre tyre) {
this.engine = engine;
this.tyre = tyre;
}
public void doSomething(){
//自定义逻辑
engine.setBrand("坏牌子轮胎");
}
public String getEngineBrand(){
//返回轮胎名称
return engine.getBrand();
}
public static void main(String[] args) {
Engine engine = new Engine();
engine.setBrand("好牌子引擎");
engine.setPower("250");
Tyre tyre = new Tyre();
tyre.setBrand("好牌子轮胎");
tyre.setSize("50cm");
Car car = new Car(engine, tyre);
car.doSomething();
log.info("轮胎名称:{}",car.getEngineBrand());
}
}
/**
* 引擎类
*/
@Data
class Engine{
private String brand;
private String power;
}
/**
* 轮胎类
*/
@Data
class Tyre{
private String brand;
private String size;
}
3.继承和复合如何选择
说了半天,那究竟是用复合还是用继承呢,我觉得最重要的一点我觉得是要搞清楚类之间的关系,对于继承而言,它是 "is-a" 的关系,是对事情的一种比如:人是动物、华为mate60是手机,只是对于动物、手机这种是更为抽象的事物,人和华为是对其类的衍生。
复合则是 "has-a" 的关系,比如:健康的人有两只眼睛、家用汽车有四个轮子,对于这种情况而言,我们就需要用到复合。
继承和复合并非是绝对的好与坏,而是我们要结合实际情况,如果是is-a关系,只有当子类和父类非常明确存在这种关系时,我们可以使用继承,并且在代码设计时,一定要考虑日后可能出现的继承问题及后续代码的升级迭代,不然很可能出现令人崩溃的后续问题;而如果某一个对象只是新增类中的一个属性时,我们就要使用复合来解决问题。
总结
写这篇文章也搜索了一些其他博主的文章,总体的感受就是国内主流博客上相关的文章并不多,大家好像并不关心这个点😑,搜到有几篇还都是直接抄书放上去(而且长得都一模一样),很是郁闷,最后还是去跳出去看了几篇别人的文章,感觉还是有很多点还是得仔细琢磨琢磨,后面再继续学习学习。
链接:https://juejin.cn/post/7250091744527269944
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。