6大设计原则-依赖倒置原则

依赖倒置原则

定义:
1.高层模块不应该依赖低层模块,两者都应该依赖其抽象;
2.抽象不应该依赖细节;
3.细节应该依赖抽象。
高层模块和低层模块的理解,每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块,原子模块在组装就是高层模块(还是很蒙蔽,往下看实际例子看能不能帮助理解);抽象很明确在java中就是指接口和抽象类,细节就是指实现类;依赖原则在java中的表现就是:
1.模块间的依赖通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或抽象类产生的(这个就像我们实际开发中的controller层调用service层不是直接new的实现类,而是依赖service的接口,service层依赖dao层也是一样);
2.接口或抽象类不依赖于实现类;
3.实现类依赖接口或抽象类;
这个精简的定义就是"面向接口编程"--OOD(Object-Oriented Design,面向对象设计)的精髓之一。

  1. 言而无信,你太需要契约
    采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。(因为都是通过接口或抽象类来依赖的,实现类之间自然就耦合性降低了,我要调用这个接口的方法,如果实现的需求有改动,我调用的地方不用改,只需要改接口方法的实现细节就可以了

证明一个定理是否正确,有两种常用的方法:一种是根据提出的论题,经过一番论证,推出和定理相同的结论,这是顺推证法;还有一种是假设提出的命题是伪命题,然后推导出一个荒谬与已知条件互斥的结论,这是反证法。现在用反正法来证明依赖倒置原则的优秀和伟大!(好像很严谨的样子

论题:依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

反论题:不使用依赖倒置原则也可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

现在通过一个汽车的例子来证明反论题是不成立的。
司机和车的类图3-1:

6大设计原则-依赖倒置原则_第1张图片
3-1

司机和奔驰用的组合关系( 是整体与部分的关系,但是部分不能离开整体而单独存在,没有司机车自然就不会开,好吧这块依赖关系的定义还需要多学学
代码如下:

//Benz
public class Benz {
    public void run(){
        System.out.println("Benz run ...");
    }
}
//司机
public class Driver {
    public void driver(Benz benz){
        benz.run();
    }
}
//模拟司机开车
public class Client {
    public static void main(String[] args) {
        Driver zhangsan = new Driver();
        zhangsan.driver(new Benz());
    }
}

这样张三司机开benz没有什么问题,那我们需要张三去开宝马,那么加一辆宝马

public class Bmw {
    public void run(){
        System.out.println("Bmw run ...");
    }
}

但是现在发现张三开不了宝马,这不是非常不合理,拿的一个驾照,都是车,为啥不给开宝马,难道在司机类加一个开宝马的方法,那不是表示以后每加一辆车就得改一次司机类,被依赖者变更让依赖者来承担修改成本,这谁受的了,这样的设计明显极不合理。

至于减少并行开发风险方面,什么事并行开发风险?并行开发最大的风险就是风险扩散,本来只是一段程序的错误或异常,逐步波及一个功能,一个模块,甚至最后毁坏整个项目。就拿这个例子来说,汽车类没有写完,司机类是无法完成后续开发的,那与司机相关的开发也很难完成。
上述例子引入了依赖倒置原则的类图3-2:


6大设计原则-依赖倒置原则_第2张图片
3-2

建立两个接口IDriver和ICar,分别定义司机和汽车的各个职能,代码如下:

//汽车类接口
public interface ICar {
    void run();
}
//benz实现汽车接口
public class Benz implements ICar{
    public void run(){
        System.out.println("Benz run ...");
    }
}
//bmw实现汽车接口
public class Bmw implements ICar{
    @Override
    public void run(){
        System.out.println("Bmw run ...");
    }
}
//司机类接口
public interface IDriver {
    void drive(ICar car);
}
//司机实现类
public class Driver implements IDriver{
    @Override
    public void drive(ICar car){
        car.run();
    }
}
//模拟汽车驾驶
public class Client {
    public static void main(String[] args) {
        IDriver zhangsan = new Driver();
        zhangsan.drive(new Benz());
        zhangsan.drive(new Bmw());
    }
}

抽象不应该依赖细节,IDriver依赖ICar是抽象之间的依赖,Driver实现类传入ICar接口,需要的时候只需要传具体实现类就可以了,client属于高层业务逻辑,他对低层模块的依赖都是建立在抽象上,在新增低层模块也就是汽车类的新的实现时,对其他低层模块不影响,只需要修改高层模块业务逻辑中的对应地方就好了,把变更的风险扩散降到最低了。
而且这样做只需要先定义好接口,并行开发也不会有什么影响,开发司机模块按照IDriver的接口约束开发就好了,开发汽车模块的按ICar的接口约束开发就好了。

  1. 依赖的三种写法

依赖是可以传递的,A对象依赖B对象,B又依赖C,C有依赖D......生生不息,依赖不止:只要做到抽象依赖,即使是多层依赖传递也无所畏惧;但是最好不要依赖形成一个闭环。
2.1 构造函数传递依赖对象
在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫构造函数注入,按照这种方式注入,IDriver和Driver的程序修改后代码如下:

public interface IDriver {
    void drive();
}
public class Driver implements IDriver{
    private ICar car;

    public Driver(ICar car){
        this.car = car;
    }
    @Override
    public void drive(){
        car.run();
    }
}

2.2Setter方法传递依赖对象
在抽象中设置Setter方法声明依赖关系,这是Setter依赖注入,代码如下:

public interface IDriver {
    void setCar(ICar car);
    void drive();
}
public class Driver implements IDriver{
    private ICar car;

    @Override
    public void setCar(ICar car) {
        this.car = car;
    }

    @Override
    public void drive(){
        car.run();
    }
}

2.3.接口声明依赖对象
在接口的方法中声明依赖对象,也叫作接口注入,之前的例子就是用的这个方式。

  1. 最佳实践

依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合,在项目中使用这个原则,遵循以下的几个规则就可以:

  • 每个类尽量都有接口或抽象类,或者两者都有,这是依赖倒置的基本要求,接口和抽象类都属于抽象的,有了抽象才可能依赖倒置。
    实际开发中的service层依赖注入dao层
  • 变量的表面类型尽量是接口或者抽象类
    工具类一般是不需要接口或抽象类的,还有,如果要使用类的clone方法,就必须使用实现类,这是jdk提供的一个规范
  • 任何类都不应该从具体类派生
    (一般的规则都不会说的那么绝对,具体还得看实际情况,还是那句话,合适的才是最好的)
  • 尽量不要覆写基类的实现方法
    如果基类是抽象类,而且方法已经实现了,子类尽量不要覆写,类间依赖的是抽象,覆写了抽象类的实现方法,对依赖的稳定性会产生一定影响
    同一个方法,父类一个实现方式,子类一个实现方式,很容易就造成逻辑混乱,可能在需要用父类的实现方式,但实际用的子类的,这就会影响运行结果了
  • 结合里氏替换原则使用
    依赖倒置的基本条件就是抽象和细节的依赖,根据里氏替换原则父类出现的地方子类就可以胜任,那我们可以得出一个通俗的规则:接口负责定义属性和方法,并且声明与其他对象的依赖,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

"倒置"具体是个什么,我的理解是这样的,按正常的思维就是我需要开奔驰那就创建一个奔驰类,然后依赖,需要什么就先去依赖具体的实现,就是最开时那个不合理的设计方式,可以理解为"正置",倒置就是我先把需要的依赖抽象出来,然后再去根据抽象的依赖去实现细节(个人理解,不知道准不准确
内容来自《设计模式之禅》

你可能感兴趣的:(6大设计原则-依赖倒置原则)