【设计模式】六大原则之二(依赖倒转原则与里氏代换原则)

    xiaoxiao2021-03-26  7

    【前言】

            上一篇博客中介绍了六大原则其中之二,这篇博客中将会再给大家介绍两个原则--依赖倒转原则和里氏代换原则

    【依赖倒转原则】

    1、问题由来

            类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是底层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险

            那么如何解决这个问题呢?

            将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率

            依赖倒转原则基于一个这样的事实:相对于细节的多边形,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多

    2、什么是依赖倒转原则

            依赖倒转原则是程序要依赖于抽象接口,不要依赖于具体的实现。简单的说就是要对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合

    3、为什么要用依赖倒转原则

            采用依赖倒转原则可以减少类间的耦合性,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和可维护性

            我们用一个例子来说明面向接口编程比相对于面向现实编程好在什么地方。母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下:

           

    class Book { public String getContent() { return "很久很久以前有一个阿拉伯的故事……"; } } class Mother { public void narrate(Book book) { System.out.println("妈妈开始讲故事"); System.out.println(book.getContent()); } } public class Client { public static void main(String[] args) { Mother mother = new Mother(); mother.narrate(new Book()); } }

            运行结果如下:

            妈妈开始讲故事

            很久很久以前有一个阿拉伯的故事……

            运行良好,假如有一天,需求改变成:让这位母亲讲一下报纸上的故事,报纸的代码如下:

           

    class Newspaper { public String getContent() { return "林书豪38+7领导尼克斯击败湖人……"; } }

            这位母亲办不到,只是将书换成报纸,必须要修改Mother才能读。假如以后需求改成杂志?改成网页?还是要不断的修改Mother,这显然不是好的设计。原因在于Mother于Book之间的耦合性太高了,必须降低他们之间的耦合度才行

            我们引入一个抽象的接口IReader

           

    interface IReader { public String getContent(); }

            Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范围,他们各自去实现IReader接口,这样就符合依赖倒转原则了,代码如下:

           

    class Newspaper implements IReader { public String getContent() { return "林书豪17+9助尼克斯击败老鹰……"; } } class Book implements IReader { public String getContent() { return "很久很久以前有一个阿拉伯的故事……"; } } class Mother { public void narrate(IReader reader) { System.out.println("妈妈开始讲故事"); System.out.println(reader.getContent()); } } public class Client { public static void main(String[] args) { Mother mother = new Mother(); mother.narrate(new Book()); mother.narrate(new Newspaper()); } }

            运行结果:

            妈妈开始讲故事

            很久很久以前有一个阿拉伯的故事……

            妈妈开始讲故事

            林书豪17+9助尼克斯击败老鹰……

            这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

            在实际编程中,我们一般需要做到如下5点:

          (1)低层模块尽量都要有抽象类或抽象接口,或者两者都有

          (2)变量的声明类型尽量是抽象类或接口

          (3)使用继承时遵循里氏替换原则

          (4)尽量不要覆写基类的方法

          (5)任何类都不应该从具体类派生

    4、依赖倒转的实现方式

          (1)通过构造函数传递依赖对象

            比如在构造函数中的需要传递的参数是抽象类或接口的方式实现

           

    public interface IDriver { //是司机就应该会驾驶汽车 public void drive(); } public class Driver implements IDriver { private ICar car; //构造函数注入 public Driver(ICar _car) { this.car = _car; } //司机的主要职责就是驾驶汽车 public void drive() { this.car.run(); } }

          (2)通过setter方法传递依赖对象

            即在我们设置的setXXX方法中的参数为抽象类或接口,来实现传递依赖对象

           

    public interface IDriver { //车辆型号 public void setCar(ICar car); //是司机就应该会驾驶汽车 public void drive(); } public class Driver implements IDriver { private ICar car; public void setCar(ICar car) { this.car = car; } } //司机的主要职责就是驾驶汽车 public void drive() { this.car.run(); }

          (3)接口声明实现依赖对象,也叫接口注入

            即在函数声明中参数为抽象类或接口,来实现传递依赖对象,从而达到直接使用依赖对象的目的

    【里氏代换原则】

    1、问题由来

            有一个功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障

            如何解决这个问题呢?

            当使用继承时,遵循里氏代换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法

    2、什么是里氏代换原则

            所有引用基类(父类)的地方必须能透明地使用其子类的对象。通俗点说就是,一个软件实体如果使用的是一个父类的花,那么一定适用其子类,而且察觉不出父类对象和子类对象的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化

            通俗点说就是,子类可以扩展父类的功能,但不能改变父类原有的功能

    3、里氏代换原则的4层含义

    (1)子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法

            里氏代换原则的关键点在于不能覆盖父类的非抽象方法。父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏代换原则就是表达了这一层含义。

             在面向对象的设计思想中,继承这一特性为系统带来了极大的便利,但是随之而来的也有一定的风险

             

    public class C { public int func(int a, int b) { return a+b; } } public class C1 extends C { @Override public int func(int a, int b) { return a-b; } } public class Client { public static void main(String[] args) { C c = new C1(); System.out.println("2+1=" + c.func(2, 1)); } }

             运行结果:

             2+1=1

            上面的运行结果明显是错的。类C1继承C,后来需要增加新功能,类C1并没有新写一个方法,而是直接重写了父类C的func方法,违背里氏代换原则,引用父类的地方并不能透明的使用子类的对象,导致运行结果出错

    (2)子类中可以增加自己特有的方法

            在继承父类属性和方法的同时,每个子类也可以有自己的特性,在父类的基础上扩展自己的功能。当功能扩展时,子类尽量不要重写父类的方法,而是另写一个方法,所以对上面的代码加以更改,使其更符合里氏代换原则,代码如下:

           

    public class C { public int func(int a, int b) { return a+b; } } public class C1 extends C { public int func2(int a, int b) { return a-b; } } public class Client { public static void main(String[] args) { C1 c = new C1(); System.out.println("2-1=" + c.func2(2, 1)); } }

            运行结果:

            2-1=1

    (3)当子类的方法实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松

    (4)当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格

    4、使用里氏代换原则的好处

            需求变化时,只需继承,而别的东西不会改变。由于里氏代换原则才使得开放封闭成为可能。这样使得子类在父类无需修改的话就可以扩展

    【小结】

            里氏代换原则是关于子类与父类的原则,依赖倒转原则是关于抽象与细节的原则。里氏代换原则中的子类具有自己的独立性,依赖倒转原则中的细节依赖于抽象。 依赖倒转原则的应用范围比里氏代换原则更广泛

        

           本文只是对基础知识做一个小小的总结,不深究。如有不同,见解欢迎指正

           本文所有代码均已通过作者测试

           本文所有内容均为作者原创,如有转载,请注明出处

    转载请注明原文地址: https://ju.6miu.com/read-650320.html

    最新回复(0)