为什么要先定义抽象类?

Java
187
0
0
2024-02-06

今天我们来聊聊S.O.L.I.D原则中的D:Dependence Inversion Principle(DIP),依赖倒置原则。

DIP 简介

依赖倒转原则 (DIP)在整个S.O.L.I.D原则是最为重要的,但偏偏又是最难理解的😓

尤其是它的定义:

High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions。

官方翻译:

高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象。 抽象不应该依赖于细节。细节应该依赖于抽象。

说说这里面的两层意思:

1)高层模块不应该直接依赖于底层模块的具体实现,而应该依赖于底层的抽象。换言之,模块间的依赖是通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。

2)接口和抽象类不应该依赖于实现类,而实现类依赖接口或抽象类。这一点其实不用多说,很好理解,“面向接口编程”思想正是这点的最好体现。

一句话:一个特定的类不应该直接依赖于另外一个类,但是可以依赖于这个类的抽象(接口)。

被“倒置”的依赖是怎么回事儿呢?这里“倒置”的英文是"Inversion"。大家是不是记得Spring中有个“控制反转”,英文是"Inversion of Control"。它们在表达上很相似。Spring 中的对象由Spring容器来创建;相对的,依赖倒置中的依赖由抽象类来提供。也就是说,通过抽象类来拿依赖!

ps:我不知道这样讲,大家是否能明白!就类似说,依赖不能是new出来的具体实现,就好比Spring中一般不自己new一个对象一样。如果大家还是不理解,或者说不懂Spring中的CI,请记住”依赖抽象“就好。我们在稍后会讲为什么

DIP 示例

来个简单的例子:小明去上学,需要依赖交通工具,有自行车、地铁和小汽车。

🆗,让我们来把依赖的工具先实现出来.

public class Bike{
  void run(){
    System.out.println("骑自行车!");
  }
}
public class Subway{
  void run(){
    System.out.println("乘地铁!");
  }
}
public class Car{
  void ride(){
    System.out.println("开小汽车!");
  }
}

好了,小明要去上学了,用什么工具呢?先用自行车吧!

public class XiaoMing {
  Bike bike;
  public XiaoMing(Bike bike) {
    this.bike = bike;
  }

  public void goToSchool() {
    this.bike.run();
  }
}

非常完美!小明骑着自行车上学啦~

第二天,小明想做地铁去上学。

public class XiaoMing {
  Subway subway;
  public XiaoMing(Subway subway) {
    this.subway = subway;
  }

  public void goToSchool() {
    this.subway.run();
  }
}

没办法,我们只能把依赖改为 Subway。

第三天,小明想开车去上学。我们还得改 XiaoMing 这个类。

public class XiaoMing {
  Car car;
  public XiaoMing(Car car) {
    this.car = car;
  }

  public void goToSchool() {
    this.car.run();
  }
}

假如,突然小明家里很有前了,准备开飞机去上学。那就得新增一个 Plane 类,并再次修改 XiaoMing 类...

大家会发现:交通工具这个依赖的更换,就会导致对 XiaoMing 类的修改!

有没有办法能让依赖的更换不影响 XiaoMing 类呢?

有!依赖倒置原则派上用场。

依赖倒置原则,不是要要求依赖抽象吗?那我们就创建一个抽象类。

public interface TranspotTool{
  void run();
}

那 XiaoMing 类就依赖于它。

public class XiaoMing {
  TranspotTool tool;
  public XiaoMing(TranspotTool tool) {
    this.tool = tool;
  }

  public void goToSchool() {
    this.tool.run();
  }
}

好了,回到最开始的场景:小明骑自行车上学。需要把 Bike 依赖进来,但现在的依赖是需要 TranspotTool 类型。那么你的 Bike 就必须是 TranspotTool 类型。于是:

public class Bike implements TranspotTool{

  @Override
  void run(){
    System.out.println("骑自行车!");
  }
}

让我们来试一下:

public class Test {
  public static void main(String[] args) {
    TranspotTool tool = new Bike();
    XiaoMing xiaoMing = new XiaoMing(tool);
    xiaoMing.goToSchool();
  }
}

输出:

骑自行车!

同样,我们可以这样修改 Subway 和 Car

public class Subway implements TranspotTool{
  @Override
  void run(){
    System.out.println("乘地铁!");
  }
}
public class Car implements TranspotTool{
  @Override
  void ride(){
    System.out.println("开小汽车!");
  }
}

再来测试下:

public class Test {
  public static void main(String[] args) {
    TranspotTool tool = new Car();
    XiaoMing xiaoMing = new XiaoMing(tool);
    xiaoMing.goToSchool();
  }
}

输出:

开小汽车!

现在更换不用的交通工具时,不在需要修改 XiaoMing 类。

这里最明显的好处是:一个类依赖于抽象后,抽象类的衍生类的变化或替换不影响该类的业务代码,维护和扩展起来方便太多啦~

把上面的整个流程梳理一遍,发现抽象的 TranspotTool 接口才是依赖的核心。它把 XiaoMing 类和具体的交通工具 Bike, Subway, Car 等具体实现解耦啦。哪怕你再弄出十个、百个的其他具体交通工具,都不会影响到 XiaoMing 类。”松耦合“体现得非常的好。

总结

依赖倒置,就是可以通过抽象使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合(也是本质)

当我们应用这个原则的时候,我们能减少对特定实现的依赖性,让我们的代码复用性更高。

这就是为啥建议我们先写抽象类,由抽象类来提供依赖啦!

这样扩展性好呀!只要是这个抽象类的衍生类都可以替换。 ps:这里提到的“替换”是不是和我们之前提到过的里氏原则有点吻合呀!

还是之前提过的老话:S.O.L.I.D原则中的各个原则间是相互有关联的。

具体的关联关系,小二哥会在后续的文章中分享!今天就先到这儿啦~