从封装变化的角度看设计模式——接口隔离

封装变化之接口隔离

在组件的构建过程中,某些接口之间直接的依赖经常会带来不少问题、甚至根本没法实现。采用添加一层间接(稳定)的接口,来隔离原本互相紧密关联的接口是一种常见的解决方案。java

这里的接口隔离不一样于接口隔离原则,接口隔离原则是对接口职责隔离,也就是尽可能减小接口职责,使得一个类对另外一个类的依赖应该创建在最小的接口上。程序员

而这里所讲到的接口隔离是对依赖或者通讯关系的隔离,经过在原有系统中加入一个层次,使得整个系统的依赖关系大大的下降。而这样的模式主要有外观模式、代理模式、中介者模式和适配器模式。面试

外观模式 - Facade

Facade模式其主要目的在于为子系统中的一组接口提供一个一致的界面(接口),Facade模式定义了一个高层接口,这个接口使得更加容易使用。安全

在咱们对系统进行研究的时候,每每会采用抽象与分解的思路去简化系统的复杂度,所以在这个过程中就将一个复杂的系统划分红为若干个子系统。也正是由于如此,子系统之间的通讯与相互依赖也就增长了,为了使得这种依赖达到最小,Facade模式正好能够解决这种问题。架构

Facade模式体现的更多的是一种接口隔离的思想,它体如今不少方面上,最多见的好比说用户图形界面、操做系统等。这均可以体现这样一个思想。框架

facade.png

Facade模式从结构上能够简化为上面这样一种形式,但其形式并不固定,尤为是体如今其内部子系统的关系上,由于其内部的子系统关系确定是复杂多样的,而且SubSystem不必定是类或者对象,也有多是一个模块,这里只是用类图来表现Facade模式与其子系统之间的关系。ide

从代码体现上来看,能够这样表现:学习

public class SubSystem1 {
    public void operation1(){
        //完成子系统1的功能
        ......
    }
}
public class SubSystem2 {
    public void operation2(){
        //完成子系统2的功能
        ......
    }
}
public class SubSystem3 {
    public void operation3(){
        //完成子系统3的功能
        ......
    }
}
public class SubSystem21 extends SubSystem2{
    //对子系统2的扩展
    ......
}
public class SubSystem22 extends SubSystem2 {
    //对子系统2的扩展
    ......
}复制代码

上面子系统内部各部分的一个体现,如何结合Facade来对外隔离它的系统内部复杂依赖呢?看下面:网站

public class Facade {
    private SubSystem1 subSystem1;
    private SubSystem2 subSystem2;
    private SubSystem3 subSystem3;
    public Facade(){
        subSystem1 = new SubSystem1();
        subSystem2 = new SubSystem21();
        subSystem3 = new SubSystem3();
    }
    public void useSystem1(){
        subSystem1.operation1();
    }
    public void useSystem2(){
        subSystem2.operation2();
    }
    public void useSystem3(){
        subSystem3.operation3();
    }
}复制代码

固然,这只是Facade模式的一种简单实现,可能在真正的实现系统中,会有着更加复杂的实现,好比各子系统之间可能存在依赖关系、又或者调用各子系统时须要传递参数等等,这些都会给Facade模式的实现带来很大的影响。this

public class Client {
    public static void main(String[] args) {
        Facade facade = new Facade();
        facade.useSystem1();
        facade.useSystem2();
        facade.useSystem3();
    }
}复制代码

当存在Facade以后,客户对子系统的访问就只须要面对Facade,而不须要再去理解各子系统之间的复杂依赖关系。固然对于普通客户而言,使用Facade所提供的接口天然是足够的;对于更加高级的客户而言,Facade模式并未屏蔽高级客户对子系统的访问,也就是说,若是有客户须要根据子系统定制本身的功能也是能够的。

对Facade的理解很简单,可是在具体使用时,又须要注意些什么呢?

  • 进一步地下降客户与子系统之间的耦合度。具体实现是,使用抽象类来实现Facade而经过它的具体子类来应对不一样子系统的实现,而且能够知足客户根据要求本身定制Facade。

    除了使用子类的方式以外,经过其余的子系统来配置Facade也是一个方法,而且这种方法的灵活性更好。

  • 在层次化结构中,可使用外观模式定义系统中每一层的入口。刚才咱们就提到过,SubSystem不必定只表示一个类,它包含的多是一些类,而且是一些具备协做关系的类,那么对于这些类,天然也是使用外观模式来为其定义一个统一的接口。

  • Facade模式自身也有缺点,虽然它减小系统的相互依赖,提升灵活性,提升了安全性;可是其自己就是不符合开闭原则的,若是子系统发生变化或者客户需求变化,就会涉及到Facade的修改,这种修改是很麻烦的,由于不管是经过扩展或是继承均可能没法解决,只能以修改源码的方式。

代理模式 - Proxy

在Proxy模式中,咱们建立具备现有对象的代理对象,以便向外界提供功能接口。其目的在于为其余对象提供一种代理以控制对这个对象的访问。

这是由于一个对象的建立和初始化可能会产生很大的开销,这也就意味着咱们能够在真正须要这个对象时再对其进行相应的建立和初始化。

好比在文件系统中对一个图片的访问,当咱们以列表形式查看文件时,并不须要显示整个图片的信息,只有在选中图片的时候,才会显示其预览信息,再在双击以后可能才会真正打个这个图片,这时可能才须要从磁盘当中加载整个图片信息。

ProxyImage.png

对图片代理的理解就如同上面的结构图同样,在文件栏中预览时,只是显示代理对象当中的fileName等信息,而代理对象当中的image信息只会在真正须要Image对象的时候才会创建实线指向的联系。

经过上面的例子,能够清楚的看到代理模式在访问对象时,引入了必定程度的间接性,这种间接性根据不一样的状况能够附加相应的具体处理。

好比,对于远程代理对象,能够隐藏一个对象不存在于不一样地址空间的事实。对于虚代理对象,能够根据要求建立对象、加强对象功能等等。还有保护代理对象,能够为对象的访问增长权限控制。

这一系列的代理都体现了代理模式的高扩展性。但同时也会增长代理开销,因为在客户端和真实主题之间增长了代理对象,所以有些类型的代理模式可能会形成请求的处理速度变慢。而且实现代理模式须要额外的工做,有些代理模式的实现很是复杂。

对于上面的例子,能够用类图更加详细地阐述。

Proxy.png

在这样一个结构中,jpg图片与图片代理类共同实现了一个图片接口,而且在图片代理类中存放了一个对于JpgImage的引用,这个引用在未有真正使用到时,是为null的,只有在须要使用时,才对其进行初始化。

//Subject(代理的目标接口)
public interface Image {
    public void show();
    public String getInfo();
}
//RealSubject(被代理的实体)
public class JpgImage implements Image {
    private String imageInfo;
    @Override
    public void show() {
        //显示完整图片
        ......
    }

    @Override
    public String getInfo() {
        return imageInfo;
    }
    public Image loadImage(String fileName){
        //从磁盘当中加载图片信息
       // ......
        return new JpgImage();
    }
}复制代码
//Proxy(代理类)
public class ImageProxy implements Image {
    private String fileName;
    private Image image;
    @Override
    public void show() {
        if (image==null){
           image = loadImage(fileName);
        }
        image.show();
    }

    @Override
    public String getInfo() {
        if (image==null){
            return fileName;
        }else{
            return image.getInfo();
        }
    }

    public Image loadImage(String fileName){
        //从磁盘当中加载图片信息
        ......
        return new JpgImage();
    }
}复制代码
public class Client {
    public static void main(String[] args) {
       Image imageProxy = new ImageProxy();
       imageProxy.getInfo();
       imageProxy.show();
    }
}复制代码

在实际的使用过程上,客户就能够再也不涉及具体的类,而是能够只关注代理类。

代理模式的种类有不少,根据代理的实现形式不一样,能够划分为:

  • 远程代理:为一个对象在不一样的地址空间提供局部表明。
  • 虚代理:为须要建立开销很大的对象生成代理。(如上面的实例)
  • 保护代理:控制对原始对象的访问。保护代理主要用于对象应该有不一样的保护权限时。
  • 智能指引:在访问对象时执行一些附加的操做。

以上的代理都是静态代理的形式,为何说是静态呢,这是由于在实现的过程当中,它的类型都是事先预约好的,好比ImageProxy这个类,它就只能代理Image的子类。

与静态相对的天然就产生了动态代理。动态代理中,最主要的两种方式就是基于JDK的动态代理和基于CGLIB的动态代理。这两种动态代理也是Spring框架中实现AOP(Aspect Oriented Programming)的两种动态代理方式。这里,就不深刻了,后面有机会再对动态代理作一个详细的讲解。

中介者模式 - Mediator

中介者模式用一个中介对象来封装一系列的对象交互。中介者使各对象不须要显示地相互引用,从而使其耦合松散,并且能够独立地改变它们之间的交互。

中介者模式产生的一个重要缘由就在于,面向对象设计鼓励将行为分页到各个对象中。而这种分布就可能会致使对象间有许多链接,这些链接就是致使系统复用和修改困难的缘由所在。

就好比一个机场调度的实现,在这个功能当中,各个航班就是Colleague,而塔台就是Mediator;若是没有塔台的协调,那么各个航班飞机的起降将只能由航班飞机之间造成一个多对多(一对多)的通讯网来控制,这种控制必然是及其复杂的;可是有了塔台的加入,整个系统就简化了许多,全部的航班只须要和塔台进行通讯,也只须要接收来自塔台的控制便可完成全部任务。这就使得多对多(一对多)的关系转化成了一对一的关系。

mediator.png

看到中介者模式类图的时候,有没有发觉好像和哪一个模式有点类似,有没有点像观察者模式。

之因此如此类似的缘由就是观察者模式和中介者模式都涉及到了对象状态变化与状态通知这两个过程。观察者模式当中,目标(Subject)的状态发生变化就会通知其全部的(Observer);一样,在中介者模式当中,其相应的同事类(一群经过中介者相互协做的类)状态发生变化,就须要通知中介者,再由中介者来处理状态信息并反馈给其余的同事类。

所以,中介者模式的实现方法之一就是使用观察者模式,将Mediator做为一个Observer,各个Colleague做为Subject,一旦Colleague状态发生变化就发送通知给Mediator。Mediator做出响应并将状态改变的结果传播给其余的Colleague。

另外还有一种方式,是在Mediator中定义一个特殊的接口,各个Colleague直接调用这个接口,并将本身做为参数传入,而后由这个接口来选择将信息发送给谁。

//Mediator
public class ControlTower {
    private List<Flight> flights
                        = new ArrayList<>();
    public void addFlight(Flight flight){
        flights.add(flight);
    }
    public void removeFlight(Flight flight){
        flights.remove(flight);
    }
    public void control(Flight flight){
        //对航班进行起降控制
        ......
        //若是航班起飞,则从flights移除
        //若是航班降落,则加入到flights
    }
}复制代码
public class Flight {
    private ControlTower cTower;
    public void setcTower(ControlTower cTower) {
        this.cTower = cTower;
    }
    public void changed(){
        cTower.control(this);
    }
}
public class Flight1 extends Flight{
    public void takeOff(){
        //起飞操做
        ......
    }
    public void land(){
        //降落操做
        ......
    }
}
public class Flight2 extends Flight{
    //起飞 降落 操做
    ......
}
public class Flight3 extends Flight{
  //一样 起飞 降落 操做
    ......
}复制代码

那么客户怎样使用这样一个模式呢?看下面这样一个操做:

public class Client {
    public static void main(String[] args) {
        ControlTower controlTower = new ControlTower();
        //假设一个飞机入场要么是有跑道空闲要么是另外一个飞机起飞
        Flight f1 = new Flight1();
        f1.setcTower(controlTower);
        //此时一号机降落,
        //controlTower调用contorl控制飞机起降
        f1.changed(); 

        Flight f2 = new Flight2();
        f2.setcTower(controlTower);
        //此时二号机降落,
        //controlTower调用contorl控制1号飞机起飞,二号降落
        f2.changed();

        .......
    }
}复制代码

中介者模式主要解决的是,若是系统中对象之间存在比较复杂的引用关系,致使它们之间的依赖关系结构混乱并且难以复用该对象,就可使用中介者来简化依赖关系。可是这也可能会使得中介者会庞大,变得复杂难以维护,因此在使用中介者模式时,尽可能是在保持中介者稳定的状况下使用。

适配器模式 - Adapter

适配器的目的在于将一个类的接口转换成客户但愿的另一个接口,从而使得本来因为接口不兼容而不能在一块儿工做的类能够在一块儿工做。

首先在使用适配器的时候,须要明确的是,适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。为何,由于适配器自己就存在一些问题,好比明明我想调用的是一个文件接口,结果传输出来的倒是一张图片,若是系统当中出现太多这样的状况,那无异会使得系统的应用变得极其困难。

因此只有在系统正在运用,而且重构困难的状况下,才选择使用适配器来适配接口。

而适配器模式又根据做用对象能够分为类适配器和对象适配器两种实现方式。

假设咱们如今已经存在一个播放器,这个播放器只能播放mp3格式的音频。可是如今又出现了一个新的播放器,这个播放器有两种播放格式mp4和wma。

也就是说,如今的状况能够用下图来进行描述:

adapter-before .png

这时候,为了右边的系统Player 融入到右边中,就能够采用适配器模式。

adapter-object .png

经过增长一个适配器,并将player做为适配器的一个属性,当传入具体的播放器时,就在newPlay()中调用player.play()

具体实现以下:

//Adaptee (适配者,要求将这个存在的接口适配成目标的接口)
public interface Player {
    public void play();
}
public class Mp3Player implements Player {
    @Override
    public void play() {
        System.out.println("播放mp3格式");
    }
}复制代码
//Target(适配目标,须要适配成那个目标的接口)
public interface NewPlayer {
    public void newPlay();
}
public class WmaNewPlayer implements NewPlayer {
    @Override
    public void newPlay() {
        System.out.println("播放wmas格式");
    }
}
public class Mp4NewPlayer implements NewPlayer {
    @Override
    public void newPlay() {
        System.out.println("播放mp4格式");
    }
}复制代码

接下来就是适配器的实现了。

//对象适配器
//首先在适配器中,增长一个适配者(Player)的引用
//而后使用适配者(Player)实现适配目标(NewPlayer)的接口
public class PlayerAdapter implements NewPlayer {
    private  Player player;
    public PlayerAdapter(Player player){
        this.player = player;
    }
    @Override
    public void newPlay() {
        player.play();
    }
}复制代码

而后整个系统的调用变化为:

public class Client {
    public static void main(String[] args) {
        //播放mp4,wma的形式不变
        NewPlayer mp4Player = new Mp4NewPlayer();
        mp4Player.newPlay();
        NewPlayer wmaPlayer = new WmaNewPlayer();
        wmaPlayer.newPlay();

        //若是要播放mp3格式,可使用适配器来进行
        Player adapter
            = new PlayerAdapter(new Mp3Player());
        adapter.newPlay();
    }
}复制代码

这样的一个适配过程可能存在一点不完善的地方,就在于,虽然对两都进行了适配,但调用方式不统一。为了统一调用过程,其实还能够作以下修改:

//对象适配器修改成
//首先在适配器中,增长适配者(newPlayer)和目标(player)的引用
//而后使用适配者(newPlayer)实现适配目标(Player)的接口
public class PlayerAdapter implements NewPlayer {
    private  NewPlayer newPlayer;
    private  Player player;

    public PlayerAdapter(NewPlayer newPlayer){
        this.newPlayer = newPlayer;
    }
    public PlayerAdapter(Player layer){
        this.player = player;
    }
    @Override
    public void newPlay() {
        if(player!=null){
            player.play();
        }else{
            newPlayer.newPlay();
        }
    }
}
//这样修改适配器以后,客户类的调用就变成了都经过适配器来进行
public class Client {
    public static void main(String[] args) {
       //播放mp3
        Player adapter1 
           = new PlayerAdapter(new Mp3Player());
        adapter1.newPlay();
        //播放mp4
        Player adapter2 
            = new PlayerAdapter(new Mp4NewPlayer());
        adapter2.newPlay();
        //播放wma
        Player adapter3 
            = new PlayerAdapter(new WmaNewPlayer());
        adapter3.newPlay();
    }
}复制代码

以前说了除了对象适配器以外,还有类适配器。而类适配器若是要实现就须要适配中的适配者是一个已经实现的结构,若是没有实现还须要适配者本身实现,这种实现方式就致使其灵活性没有对象适配器那么高。

adapter-class.png其类图就是上面这样一种形式,主要区别体如今适配器的实现,而其部分变化不大。

//类适配器
public class PlayerAdapter extends Mp3Player implements NewPlayer {
    @Override
    public void newPlay() {
        play();
    }
}
//客户调用过程就变化为:
public class Client {
    public static void main(String[] args) {
        //播放mp4,wma的形式不变
        NewPlayer mp4Player = new Mp4NewPlayer();
        mp4Player.newPlay();
        NewPlayer wmaPlayer = new WmaNewPlayer();
        wmaPlayer.newPlay();
        //若是要播放mp3格式,可使用适配器来进行
        Player adapter = new PlayerAdapter();
        adapter.newPlay();
    }
}复制代码

可是若是Player存在不一样子类,那明显使用对象适配器是更好的选择。

固然也不是说类适配器就不必定没有对象适配器以外的优点。二者的使用有不一样的权衡。

类适配器:

  • 用一个具体的Adapater类对Adaptee和Target进行匹配。结果是当咱们想要匹配一个类以及全部它的子类时,类适配器就再也不适用。
  • 由于Adapter是Adaptee的子类,这就使得Adapter能够重定义Adaptee的部分行为。
  • 不须要再引入对象,不须要引入额外的引用就能够获得adaptee。

对象适配器:

  • 容许一个Adapter与多个Adaptee——即Adaptee自己以及它的全部子类同时工做,而且Adapter也能够一次给全部的Adaptee添加功能。
  • 想要重定义Adaptee的行为比较困难,但对于加强Adaptee的功能却很容易。若是要自定义Adaptee的行为,就只能生成Adaptee的子类来实现重定义。

最后,最近不少小伙伴找我要Linux学习路线图,因而我根据本身的经验,利用业余时间熬夜肝了一个月,整理了一份电子书。不管你是面试仍是自我提高,相信都会对你有帮助!目录以下:

免费送给你们,只求你们金指给我点个赞!

电子书 | Linux开发学习路线图

也但愿有小伙伴能加入我,把这份电子书作得更完美!

有收获?但愿老铁们来个三连击,给更多的人看到这篇文章

推荐阅读:

相关文章
相关标签/搜索