多态实现依赖倒转原则
依赖倒转原则(Dependency Inversion Principle,DIP):是指将两个模块之间的依赖关系倒置为依赖抽象类或接口。具体有两层含义:
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
- 抽象不应该依赖于细节,细节应该依赖于抽象。
一、为什么要设计这种原则呢?
在传统的结构化程序设计当中,高层次模块要完成自己封装的功能,就必须要使用低层模块(即继承低层模块),于是高层模块就依赖于低层模块。但低层模块经常会发生变动,一旦低层模块发生改变,高层模块也会受到影响,也需要进行修改。
在面向对象设计中,类和类之间依赖关系可以分为两种类型:
- 具体耦合关系:发生在两个具体的(可实例化的)类之间,经由一个类对另一个具体类的直接引用造成。
- 抽象耦合关系:发生在一个具体类和一个抽象类(或接口)之间,使两个必须发生关系的类之间存有最大的灵活性。
如果高层模块直接调用低层模块提供的服务,那么就是具体耦合关系,这样高层模块依赖于低层模块就不可避免。但是,如果我们使用抽象耦合关系,在高层模块和低层模块之间定义一个抽象接口,高层模块调用抽象接口定义的方法,低层模块实现该接口。这样,就消除了高层模块和低层模块之间的直接依赖关系。现在,高层模块就不依赖于低层模块了,二者都依赖于抽象。同时也实现了“抽象不应该依赖于细节,细节应该依赖于抽象”。
二、名词解析
- 多态:指允许不同类型的对象对同一消息做出不同的响应。即同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。也就是说,同一个接口,可以使用不同的实例而执行不同操作。
- 依赖:指如果一个模块A使用另一个模块B,我们称模块A依赖模块B。一般可以通过继承关系来体现。
- 高/低层模块:在应用程序中,有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外,有一些高层次的类,这些类封装了某些复杂的逻辑,这些类我们称之为高层模块。
三、案例分析
3.1 定义一个接口
(高层次模块需要的服务或功能的抽象描述)
// 抽象接口
public interface AnimalSound {
void makeSound();
}
3.2 创建两个实现这个接口的类
(低层次模块的具体实现)
// 猫类,实现AnimalSound接口
public class Cat implements AnimalSound {
@Override
public void makeSound() {
System.out.println("喵喵喵");
}
}
// 狗类,实现AnimalSound接口
public class Dog implements AnimalSound {
@Override
public void makeSound() {
System.out.println("汪汪汪");
}
}
3.3 创建一个高层次模块
(它依赖于上述的抽象接口,而不是具体的类/低层模块)
// 高层次模块,依赖于AnimalSound接口
public class AnimalSoundMaker {
private AnimalSound animalSound;
// 通过构造函数注入依赖
public AnimalSoundMaker(AnimalSound animalSound) {
this.animalSound = animalSound;
}
public void makeAnimalSound() {
animalSound.makeSound(); // 调用接口方法,实现多态
}
}
3.3 在主程序中创建具体的对象,并传递给高层次模块
public class Main {
public static void main(String[] args) {
// 创建猫对象
AnimalSound cat = new Cat();
// 创建高层次模块对象,并传入猫对象
AnimalSoundMaker catSoundMaker = new AnimalSoundMaker(cat);
catSoundMaker.makeAnimalSound(); // 输出:喵喵喵
// 创建狗对象
AnimalSound dog = new Dog();
// 创建高层次模块对象,并传入狗对象
AnimalSoundMaker dogSoundMaker = new AnimalSoundMaker(dog);
dogSoundMaker.makeAnimalSound(); // 输出:汪汪汪
}
}
在这个例子中,AnimalSound 是抽象接口,Cat 和 Dog 是具体的实现类,AnimalSoundMaker 是高层次模块,它依赖于 AnimalSound 接口而不是具体的 Cat 或 Dog 类。这样,我们就通过多态实现了依赖反转原则,高层次模块不再依赖于具体的实现类,而是依赖于抽象接口。这增加了代码的灵活性和可维护性,因为我们可以很容易地添加新的动物类(如 Bird),只需要在低层模块中添加新的动物类(如 Bird)即可,而不需要修改 AnimalSoundMaker 类。
四、依赖注入的方式
依赖注入(Dependency Injection,DI)是一种设计模式,可以将对象的创建和对象之间的依赖关系分离,从而使代码更加模块化、可测试和可维护。
4.1 构造函数注入(Constructor Injection)
通过将依赖关系作为构造函数参数传递来实现依赖注入。在本文中的3.3节对AnimalSoundMaker类的实现就采用了构造函数注入的方式。
4.2 Setter方法注入(Setter Injection)
通过对象的Setter方法来注入依赖。允许在对象创建后动态地注入依赖关系,灵活性较高。
public class MyClass {
private MyDependency myDependency;
// Setter方法注入
public void setMyDependency(MyDependency myDependency) {
this.myDependency = myDependency;
}
}
4.3 接口注入(Interface Injection)
通过对象实现一个特定的接口来注入依赖。注:接口注入较少被使用,通常情况下不如构造器注入和Setter方法注入灵活。
public interface DependencyInjector {
void injectDependency(MyClass myClass);
}
public class MyClass implements DependencyInjector {
private MyDependency myDependency;
// 接口注入
@Override
public void injectDependency(MyDependency myDependency) {
this.myDependency = myDependency;
}
}