设计模式 — 6大设计原则(依赖倒置和接口隔离原则)

依赖倒置原则

依赖倒置原则(Dependence Inversion Principle,DIP)这个名字看着有点别扭,“依赖” 还 “倒置” ,这到底是什么意思?依赖倒置原则翻译过来包含三层含义

1、高层模块不应该依赖底层模块,两者都应该依赖其抽象;
2、抽象不应该依赖细节;
3、细节应该依赖抽象。
辅助理解:
	底层模块:每个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是底层模块。
	高层模块:原子逻辑的再组装就是高层模块。(领域编排)
	抽象:指接口或抽象类,两者都是不能直接被实例化的。
	细节:细节就是实现类实现接口或继承抽象类而产生的类,其特点就是可以直接被实例化

依赖倒置原则在Java语言中的表现就是:

1、模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
2、接口或抽象类不依赖实现类;
3、实现类依赖接口或抽象类
4、简单来说就是 “面向接口编程”

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

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

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

示例 一

现在的汽车越来越便宜了,一个卫生间的造价就可以买一辆不错的汽车,有汽车就必然有人来驾驶,司机驾驶奔驰车的类图如下:
在这里插入图片描述
奔驰车可以提高一个方法run,代表车辆运行,实现过程代码如下:

public class Benz {

    public void run() {
        System.out.println("奔驰汽车开始运行...");
    }
}

public class Driver {
    //司机的主要职责就是驾驶汽车
    public void drive(Benz benz) {
        benz.run();
    }
}

public class Client {

    public static void main(String[] args) {

        Driver zhangSan = new Driver();

        Benz benz = new Benz();
        //张三开奔驰车
        zhangSan.drive(benz);
    }
}

运行结果得出:

奔驰汽车开始运行...

通过以上的代码,完成了司机开动奔驰车的场景,到目前为止,这个司机开奔驰车的项目没有任何问题,但是变更缺不灵活,业务需求变更永无止休,技术前进就永无止境,在原有程序上加上,张三不仅要开奔驰车,还要开宝马车,新增代码如下:

public class BMW {

    //宝马车当然也可以开动了
    public void run() {
        System.out.println("宝马汽车开始运行...");
    }
}

到这里,我们发现没有办法让张三把宝马车也开动起来,为什么?张三没有(调用)开动宝马车的方法呀!很显然,从设计上出了问题,司机类和奔驰车类之间是紧耦合关系,其导致的结果就是系统的可维护性大大降低,可读性降低。通过以上的例子证明反论题已经部分不成立了。

注意:设计是否具备稳定性,只要适当地 “松松土” ,观察 “设计的蓝图” ,是否还可以茁壮地成长就可以得出结论,稳定性较高的设计,在周围环境频繁变化的时候,依然可以做到 “我自巍然不动”
示例 二

我们继续证明, “减少并行开发引起的风险” ,什么是并行开发的风险? 并行开发最大的风险就是风险扩散,本来只是一段程序的错误或异常,逐步波及了整个项目。为什么并行开发就有这样的风险呢? 一个团队,20个开发人员,各人负责不同的功能模块,甲负责汽车类的建造,乙负责司机类的建造,在甲没有完成的情况下,乙是不能完全地编写代码的,缺少汽车类,编译器根本就不会让你通过!所以为了解决并行开发的问题,依赖倒置原则就产生了!

引入依赖倒置原则后的类图如下:
在这里插入图片描述

司机接口及实现

public interface IDriver {

    public void drive(ICar car);
}

public class Driver implements IDriver {


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

汽车接口以及奔驰和宝马的实现类

public interface ICar {

    //是汽车就应该能跑
    public void run();
}

public class BMW implements ICar {

    //宝马车当然也可以开动了
    public void run() {
        System.out.println("宝马汽车开始运行...");
    }
}

public class Benz implements ICar {

    public void run() {
        System.out.println("奔驰汽车开始运行...");
    }
}

在业务场景中,我们贯彻 “抽象不应该依赖细节” ,也就是我们认为抽象 (ICar接口)不依赖BMW和Benz两个实现类(细节),因此在高层次的模块中应用都是抽象的,Client的实现过程如下:

public class Client1 {

    public static void main(String[] args) {

        IDriver zhangSan = new Driver();

        ICar benz = new Benz();
        //张三开奔驰车
        zhangSan.drive(benz);
    }
}

Client属于高层业务逻辑,它对底层模块的依赖都建立在抽象上,zhangSan的表面类型是IDriver,Benz的表面类型是ICar,
这里,也许你要问,在这个高层模块中也调用到了底层模块,比如new Driver() 和 new Benz()等,如何解释?确实如此,zhangSan的表面类型是IDriver,是一个接口,是抽象的、非实体化的,在其后的所有操作中,zhangSan都是以IDriver类型进行操作,屏蔽了细节对抽象的影响。当然,张三如果要开宝马车,修改一下业务场景即可,如下:

public class Client2 {

    public static void main(String[] args) {

        IDriver zhangSan = new Driver();

        ICar bmw = new BMW();
        //张三开宝马车
        zhangSan.drive(bmw);
    }
}

在新增加底层模块时,只修改了业务场景类,也就是高层模块,对其他底层模块如Driver类不需要做任何修改,业务就可以运行,把 “变更” 引起的风险扩散降低到最小。

注意:在Java中,只要定义变量就必然要有类型,一个变量可以有两种类型:表面类型和实际类型,表面类型是在定义的时候赋予的类型,实际类型是对象的类型,如zhangSan的表面类型是IDriver,实际类型是Driver

抽象是对实现的约束,对依赖而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不脱离契约的范畴,确保约束双方按照既定的契约(抽象)共同发展。

依赖的三种写法

依赖是可以传递的,A对象依赖B对象,B对象依赖C,C又依赖D,生生不息,依赖不止。只要做到抽象依赖,即使是多层的依赖传递也无所谓!

对象的依赖关系有三种方式来传递,如下所示:

  1. 构造函数传递依赖对象
    在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入,按照这种方式的注入,IDriver和Driver的程序修改后如代码清单:
public class Driver implements IDriver {

    private ICar car;

    //构造函数注入
    public Driver(ICar _car) {
        this.car = _car;
    }
    //司机的主要职责就是驾驶汽车
    @Override
    public void drive(ICar car) {
        car.run();
    }
}
  1. Setter方法传递依赖对象
    在抽象中设置Setter方法声明依赖关系,依照依赖注入的说法,这是Setter依赖注入,按照这种方式的注入,IDriver和Driver的程序修改后代码如下:
public interface IDriver {

    public void drive(ICar car);
    
    public void setCar(ICar car);
}

public class Driver implements IDriver {


    private ICar car;

    public void setCar(ICar _car) {
        this.car = _car;
    }
    @Override
    public void drive(ICar car) {
        car.run();
    }
}
  1. 接口声明依赖对象
    在接口中的方法声明依赖对象,在示例 二的例子就使用了接口声明依赖的方法,该方法也叫接口注入。
public interface IDriver {

    public void drive(ICar car);
}

public class Driver implements IDriver {


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

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

1、每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了
抽象才可能依赖倒置
2、变量的表面类型尽量是接口或者是抽象类
3、任何类都不应该从具体类派生
4、尽量不要覆写基类的方法,如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性毁产生一定的影响。
5、结合里氏替换原则使用

“倒置” 到底是什么?先说 “正置”是什么意思,依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,这也是正常人的思维方法,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑,而编写程序需要的是对现实世界的事物进行抽象,抽象的结果就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖, “倒置” 就是从这里产生的。

依赖倒置原则的优点在小型项目中很难体现出来,例如小于10个人月的项目,主要体现在大中型项目中,采用依赖倒置原则有非常多的优点,特别是规避一些非技术因素引起的问题。

依赖倒置原则是6个设计原则中最难以实现的原则,它是实现开闭原则的重要途径,依赖倒置原则没有实现,就别想实现对扩展开放,对修改关闭。在项目中,只需要记住 “面向接口编程” 基本上抓住了依赖倒置原则的核心。

上述讲了这么多依赖倒置原则的优点,同时也存在缺点,在现实世界中确实存在着必须依赖细节的事物,比如法律,就必须依赖细节的定义。 “杀人偿命” 在中国的法律中古今有之,那这个的 “杀人” 就是一个抽象的定义,怎么杀,杀什么人,为什么杀人,都没有定义,只要是杀人就统统偿命,这就是有问题了,好人杀了坏人,还要赔上自己的一条性命,这是不公正的,从这一点看,在实际项目中使用依赖倒置原则时需要审时度势,不要抓住一个原则不放,每一个原则的优点都是有限度的,并不是放之四海而皆准的真理,所以别为了遵循一个原则而放弃了一个项目的终极目标:投产上线和盈利。

接口隔离原则

在讲接口隔离原则之前,先明确一下我们的主角。接口分为两种:

实例接口 (Object Interface),在Java中声明一个类,然后用new关键字产生一个实例,它是对一个类型的事物的描述,这是一种接口。
比如定义Person这个类,然后使用Person zhangSan = new Person()产生了一个实例,这个实例要遵从的标准就是Person这个类。
类接口(Class Interface),Java中经常使用的interface关键字定义的接口。

主角已经定义清楚了,那什么是隔离呢?它有两种定义。如下所示:

客户端不应该依赖它不需要的接口。
类间的依赖关系应该建立在最小的接口上。

上面两个定义概括为一句话:建立单一接口,不要建立臃肿庞大的接口。再通俗一点讲:接口尽量细化,同时接口中的方法尽量少。

单一职责原则与接口隔离原则的区别:
单一职责:类和接口职责单一,注重职责,这是业务逻辑上的划分
接口隔离:要求接口的方法尽量少。

实例 一

比如定义一下什么是美女,首先要面膜好看,其次是身材要窈窕,然后要有气质,当然了,这三者各人的排列顺序不一样,总之要成为一名美女就必须具备:面貌、身材和气质,我们用类图体现一下星探找美女的过程,如图:
在这里插入图片描述
定义了一个IPettyGirl接口,声明所有的美女都应该有goodLooking、niceFigure和great - Temperament,然后又定义了一个抽象类AbstractSearcher,其作用就是搜索美女并显示其信息,只要美女都按照这个规范定义,Searcher(星探) 就轻松多了,美女类的实现代码清单如下:

// 美女接口
public interface IPettyGirl {
    
    //要有姣好的面孔
    public void goodLooking();
    //要有好身材
    public void niceFigure();
    //要有气质
    public void greatTemperament();
}


public class PettyGirl implements IPettyGirl {
    
    private String name;
    
    //美女都要有名字
    public PettyGirl(String _name) {
        this.name = _name;
    }
    
    @Override
    public void goodLooking() {
        System.out.println(this.name+ "~~~~脸蛋很漂亮!");
    }

    @Override
    public void niceFigure() {
        System.out.println(this.name+ "~~~~气质很好!");
    }

    @Override
    public void greatTemperament() {
        System.out.println(this.name+ "~~~~身材特别好!");
    }
}

通过三个方法,把对美女的要求都定义出来了,按照这个标准,如花姑娘被排除在美女标准之外了。有美女,就有搜索美女的星探,其具体实现如代码清单:

星探抽象类:

public abstract class AbstractSearcher {
    protected  IPettyGirl pettyGirl;

    public AbstractSearcher(IPettyGirl _pettyGirl){
        this.pettyGirl = _pettyGirl;
    }

    //搜索美女,列出美女信息
    public abstract void show();
}

星探实现类:

public class Searcher extends AbstractSearcher{

    public Searcher(IPettyGirl _pettyGirl) {
        super(_pettyGirl);
    }

    //展示美女的信息
    @Override
    public void show() {

        System.out.println("------美女的信息如下:----------");

        //展示面容
        super.pettyGirl.goodLooking();

        //展示身材
        super.pettyGirl.niceFigure();

        //展示气质
        super.pettyGirl.greatTemperament();
    }
}

场景类:

public class Client {

    //搜索并展示美女信息
    public static void main(String[] args) {

        //定义一个美女
        IPettyGirl feiFei = new PettyGirl("菲菲");
        AbstractSearcher searcher = new Searcher(feiFei);
        searcher.show();
    }
}

星探搜索美女的运行结果如下所示:

------美女的信息如下:----------
菲菲~~~~脸蛋很漂亮!
菲菲~~~~气质很好!
菲菲~~~~身材特别好!

星探寻找美女的程序开发完毕了,但是接口是否做好了最优设计?答案是没有,还可以对接口进行优化。
我们的审美观点都在变化,美女的定义也在变化。唐朝的杨贵妃如果活在现在这个年代非羞愧而死不可,为什么?胖呀!但是胖并不影响她入选中国四大美女,说明当时的审美观与现在是有差异的。气质型美女也是美女,每个人的定义不一样,不一样要样貌、身材、气质都具备才算美女,所有需要扩展一个美女类,只实现greatTemperament方法,其他两个方法置空,什么都不写,不就可以了吗?但是行不通!为毛呢?星探greatTemperament依赖的是IPettyGirl接口,它有三个方法,你只实现了两个方法,星探的方法是不是又要修改?我们上面的程序打印出来的信息少了两条,还让星探怎么去辨别是不是美女呢?

分析到这里,我们发现接口IPettyGirl的设计是有缺陷的,过于庞大了,容纳了一些可变的因素,根据接口隔离原则,星探AbstractSearcher应该依赖于具有部分特质的女孩子,而我们却把这些特质都封装起来,放到一个接口中,封装过度了,需要重新设计一下类图,修改后的类图如下所示:
在这里插入图片描述
从上述类图可以看到,把原IPettyGirl接口拆分为两个接口,一种是外形美的美女IGoodBodyGirl,这类美女的特别就是脸蛋和身材极棒,另外一种是气质美的美女IGreatTemperamentGirl,谈吐和修养都非常高。

两种类型的美女定义:

public interface IGoodBodyGirl {
    
    //要有姣好的面孔
    public void goodLooking();
    //要有好身材
    public void niceFigure();
}
public interface IGreatTemperamentGirl {

    //要有气质
    public void greatTemperament();
}

按照脸蛋、身材、气质都具备才算美女,实现类实现两个接口,如代码清单:

public class PettyGirlS implements IGoodBodyGirl, IGreatTemperamentGirl {
    
    private String name;

    //美女都要有名字
    public PettyGirlS(String _name) {
        this.name = _name;
    }

    @Override
    public void goodLooking() {
        System.out.println(this.name+ "~~~~脸蛋很漂亮!");
    }

    @Override
    public void niceFigure() {
        System.out.println(this.name+ "~~~~气质很好!");
    }

    @Override
    public void greatTemperament() {
        System.out.println(this.name+ "~~~~身材特别好!");
    }
}

通过这样的重构以后,不管以后是要气质美女还是要外形美女,都可以保持接口的稳定,以上把一个臃肿的接口变更为两个独立的接口所依赖的原则就是接口隔离原则,接口是我们设计时对外提供的契约,通过分散定义多个接口,可以预防未来变更的扩散,提高系统的灵活性和可维护性。

总结

接口隔离原则是对外接口进行规范约束,其包含以下4层含义:

接口要尽量小(根据接口隔离原则拆分接口的前提必须满足单一职责原则)
接口要高内聚 (提高接口、类、模块的处理能力,减少对外的交互)
定义服务
接口设计是有限度的

接口隔离原则是对接口的定义,同时也是对类的定义,接口和类尽量使用原子接口或原子类来组装。在实践中可以根据以下几个规则来衡量:

一个接口只服务于一个子模块或业务逻辑;
通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达成 “满身筋骨肉”,而不是 “肥嘟嘟” 的一大堆方法。
已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转换处理。
了解环境,拒绝盲从。

设计模式 — 6大设计原则(迪米特法则和开闭原则)

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值