概述
定义:
1)高层模块不应该依赖低层模块,二者都应该依赖其抽象。从代码的角度来说,高层模块就是调用者,低层模块就是被调用者。
2)抽象不应该依赖细节,细节应该依赖抽象
依赖倒置的中心思想是面向抽象(抽象类或接口)编程
依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是抽象类或接口;细节就是具体的实现类,实现接口或者继承抽象类所产生的类。
使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
案例演示
人喂养动物案例:
依赖正置方案:
动物类:
public class Dog {
public void eat() {
System.out.println("狗吃骨头");
}
}
public class Cat {
public void eat() {
System.out.println("猫吃鱼");
}
}
Person类:
public class Person {
public void feed(Dog dog) {
dog.eat();
}
public void feed(Cat cat) {
cat.eat();
}
}
测试类:
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.feed(new Dog());
p.feed(new Cat());
}
}
依赖正置:就是类间的依赖是具体类间的依赖,也就是面向实现编程,高层模块依赖低层模块(Person类的方法中调用了动物类,即Person类依赖动物类),当低层模块发生变动时,高层模块也得跟着一起变(当增加动物时,Person类也要修改,增加相应的重载的feed方法)。
优点:代码简单,容易想到。
缺点:如果增加鸟、猪等,则需要增加类,同时Person类需要增加相应的重载方法。在类级别违反了开闭原则,类间的耦合度高,可维护性和复用性较低。
改进方案:依赖倒置
定义IAnimal接口,包含抽象的eat()方法,让动物类实现该接口实现eat()方法,Person类不再依赖具体动物类,而是依赖IAnimal接口,当需要增加喂养的动物时,让它实现IAnimal接口即可,无需修改IAnimal接口和Person类。
依赖正置与依赖倒置的UML图:
依赖倒置的三种实现方式:普通方法传递依赖对象、构造方法传递依赖对象、set方法传入依赖对象
普通方法传递依赖对象
声明接口类型的形参,调用时传递实现类对象
IAnimal接口:
public interface IAnimal {
void eat();
}
动物类实现IAnimal接口:
public class Dog implements IAnimal {
public void eat() {
System.out.println("狗吃骨头");
}
}
public class Cat implements IAnimal {
public void eat() {
System.out.println("猫吃鱼");
}
}
Person类:
public class Person {
//feed方法传递依赖对象
public void feed(IAnimal animal) {
animal.eat();
}
}
测试类:
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.feed(new Dog());
p.feed(new Cat());
}
}
一般只有在调用方法的时候才需要传递依赖,依赖程度最低。
构造方法传递依赖对象
IAnimal接口:
public interface IAnimal {
void eat();
}
动物类实现IAnimal接口:
public class Dog implements IAnimal {
public void eat() {
System.out.println("狗吃骨头");
}
}
public class Cat implements IAnimal {
public void eat() {
System.out.println("猫吃鱼");
}
}
Person类:
public class Person {
private IAnimal animal;
//构造方法传递依赖对象
public Person(IAnimal animal) {
this.animal = animal;
}
public void feed() {
this.animal.eat();
}
}
测试类:
public class Test {
public static void main(String[] args) {
Person p1 = new Person(new Dog());
p1.feed();
Person p2 = new Person(new Cat());
p2.feed();
}
}
依赖对象对于调用对象是必须的,并且他们的生命周期也是一致的,在创建调用对象的时候就需要传递依赖对象才能构造成功,并且对于同一个调用对象 依赖对象一般是不可变的,使用的始终是同一个。
set方法传入依赖对象
IAnimal接口:
public interface IAnimal {
void eat();
}
动物类实现IAnimal接口:
public class Dog implements IAnimal {
public void eat() {
System.out.println("狗吃骨头");
}
}
public class Cat implements IAnimal {
public void eat() {
System.out.println("猫吃鱼");
}
}
Person类:
public class Person {
private IAnimal animal;
//set方法传递依赖对象
public void setAnimal(IAnimal animal) {
this.animal = animal;
}
public void feed() {
this.animal.eat();
}
}
测试类:
public class Test {
public static void main(String[] args) {
Person p1 = new Person();
p1.setAnimal(new Dog());
p1.feed();
Person p2 = new Person();
p2.setAnimal(new Cat());
p2.feed();
}
}
依赖对象和调用对象的生命周期一般不是同步的,也有可能不是必须的。可以在需要的时候传递依赖对象,还可以多次传递。
依赖倒转原则的注意事项和细节
依赖倒置原则的本质就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。
低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好。
变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,有利于程序扩展和优化。