封装变化之接口隔离

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

这里的接口隔离不同于接口隔离原则,接口隔离原则是对接口职责隔离,也就是尽量减少接口职责,使得一个类对另一个类的依赖应该建立在最小的接口上。

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

外观模式 - Facade

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

在我们对系统进行研究的时候,往往会采用抽象与分解的思路去简化系统的复杂度,因此在这个过程当中就将一个复杂的系统划分成为若干个子系统。也正是因为如此,子系统之间的通信与相互依赖也就增加了,为了使得这种依赖达到最小,Facade模式正好可以解决这种问题。

Facade模式体现的更多的是一种接口隔离的思想,它体现在很多方面上,最常见的比如说用户图形界面、操作系统等。这都可以体现这样一个思想。

facade.png

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

从代码体现上来看,可以这样表现:

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模式的实现带来很大的影响。

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开发学习路线图

也希望有小伙伴能加入我,把这份电子书做得更完美!

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

推荐阅读:


良许
1k 声望1.8k 粉丝