(八)适配器模式详解

2年前 (2022) 程序员胖胖胖虎阿
271 0 0

                  作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可。

                  各位好,我们本次接着讨论第八个设计模式,适配器模式。

                  适配器模式从实现方式上分为两种,类适配器和对象适配器,这两种的区别在于实现方式上的不同,一种采用继承,一种采用组合的方式。

                  另外从使用目的上来说,也可以分为两种,特殊适配器和缺省适配器,这两种的区别在于使用目的上的不同,一种为了复用原有的代码并适配当前的接口,一种为了提供缺省的实现,避免子类需要实现不该实现的方法。

                  首先应该明白一点,适配器模式是补救措施,所以在系统设计过程中请忘掉这个设计模式,这个模式只是在你无可奈何时的补救方式。

                  那么我们什么时候使用这个模式呢?场景通常情况下是,系统中有一套完整的类结构,而我们需要利用其中某一个类的功能(通俗点说可以说是方法),但是我们的客户端只认识另外一个和这个类结构不相关的接口,这时候就是适配器模式发挥的时候了,我们可以将这个现有的类与我们的目标接口进行适配,最终获得一个符合需要的接口并且包含待复用的类的功能的类。

                  接下来我们举一个例子,比如我们在观察者一章中就提到一个问题,就是说观察者模式的一个缺点,即如果一个现有的类没有实现Observer接口,那么我们就无法将这个类作为观察者加入到被观察者的观察者列表中了,这实在太遗憾了。

                  在这个问题中,我们需要得到一个Observer接口的类,但是又想用原有的类的功能,但是我们又改不了这个原来的类的代码,或者原来的类有一个完整的类体系,我们不希望破坏它,那么适配器模式就是你的不二之选了。

                  我们举个具体的例子,比如我们希望将HashMap这个类加到观察者列表里,在被观察者产生变化时,假设我们要清空整个MAP。但是现在加不进去啊,为什么呢?

                  因为Observable的观察者列表只认识Observer这个接口,它不认识HashMap,怎么办呢?

                  这种情况下,我们就可以使用类适配器的方式将我们的HashMap做点手脚,刚才已经说了,类适配器采用继承的方式,那么我们写出如下适配器。

public class HashMapObserverAdapter<K, V> extends HashMap<K, V> implements Observer{

    public void update(Observable o, Object arg) {
        //被观察者变化时,清空Map
        super.clear();
    }

}

                 即我们继承我们希望复用其功能的类,并且实现我们想适配的接口,在这里就是Observer,那么就会产生一个适配器,这个适配器具有原有类(即HashMap)的功能,又具有观察者接口,所以这个适配器现在可以加入到观察者列表了。

                 看,类适配器很简单吧?那么下面我们来看看对象适配器,刚才说了对象适配器是采用组合的方式实现。

                 为什么要采用组合呢?上面的方式不是很好吗?

                 究其根本,是因为JAVA单继承的原因,一个JAVA类只能有一个父类,所以当我们要适配的对象是两个类的时候,你怎么办呢?你难道要将两个类全部写到extends后面吗,如果你这么做了,那么编译器会表示它的不满的。

                 我们还是拿观察者模式那一章的例子来说(观察者模式比较惨,老要适配器模式擦屁股),比如我们现在有一个写好的类,假设就是个实体类吧。如下。

public class User extends BaseEntity{
    private Integer id;
    private String name;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

                 看到了吧,我们的实体类大部分都是继承自BaseEntity的,那现在你怎么办吧,你要想具有被观察者的功能还要继承Observable类,你说你怎么继承吧。

                 你是不是想说,那我的User不继承BaseEntity不就完事了,我把BaseEntity里面的东西全部挪动到User类,或者我不继承Observable了,把Observable里面的东西全部挪到User类里面。

                 这并不是不行,但是这是个很大的隐患,比如我们项目到时候要针对BaseEntity的子类进行扫描,用来做一些事情,这时候如果User没继承BaseEntity,那么你就会遗漏掉这个类,这会破坏你的继承体系,付出太大了。

                 相反,如果你不继承Observable,那么你的User类看起来会非常杂乱,而且假设我现在不仅User类可以被观察了,我的Person类,Employee都能被观察了,你难道要把Observable的代码COPY三次到这三个类里面吗?

                不要忘了刚才说的,适配器模式就是为了帮助我们复用代码的,这里使用适配器模式就可以帮我们复用Observable的代码或者说功能。

                基于上面LZ的讨论,我们做出如下适配器,这里采用的对象适配器。

//我们继承User,组合Observable.
public class ObservableUser extends User{
    
    private Observable observable = new Observable();

    public synchronized void addObserver(Observer o) {
        observable.addObserver(o);
    }

    public synchronized void deleteObserver(Observer o) {
        observable.deleteObserver(o);
    }

    public void notifyObservers() {
        observable.notifyObservers();
    }

    public void notifyObservers(Object arg) {
        observable.notifyObservers(arg);
    }

    public synchronized void deleteObservers() {
        observable.deleteObservers();
    }

    protected synchronized void setChanged() {
        observable.setChanged();
    }

    protected synchronized void clearChanged() {
        observable.clearChanged();
    }

    public synchronized boolean hasChanged() {
        return observable.hasChanged();
    }

    public synchronized int countObservers() {
        return observable.countObservers();
    }
    
    
}

              我们继承User,而不是继承Observable,这个原因刚才已经说过了,我们不能破坏项目中的继承体系,所以现在可观察的User(ObservableUser)依然处于我们实体的继承体系中,另外如果想让ObservableUser具有User的属性,则需要将User的属性改为protected。

              这下好了,我们有了可观察的User了。不过LZ早就说过,设计模式要活用,这里明显不是最好的解决方案。因为我们要是还有Person,Employee类都要具有可观察的功能的话,那其实也相当惨,因为下面那些Observable的方法我们还要再复制一遍。

              提示到这里,不知各位想到更好的解决方案了吗?尤其是新手可以好好思考下。

              LZ这里给出最终相对来说比较好的解决方案,那就是我们定义如下可观察的基类。

//我们扩展BaseEntity,适配出来一个可观察的实体基类
public class BaseObservableEntity extends BaseEntity{

    private Observable observable = new Observable();

    public synchronized void addObserver(Observer o) {
        observable.addObserver(o);
    }

    public synchronized void deleteObserver(Observer o) {
        observable.deleteObserver(o);
    }

    public void notifyObservers() {
        observable.notifyObservers();
    }

    public void notifyObservers(Object arg) {
        observable.notifyObservers(arg);
    }

    public synchronized void deleteObservers() {
        observable.deleteObservers();
    }

    protected synchronized void setChanged() {
        observable.setChanged();
    }

    protected synchronized void clearChanged() {
        observable.clearChanged();
    }

    public synchronized boolean hasChanged() {
        return observable.hasChanged();
    }

    public synchronized int countObservers() {
        return observable.countObservers();
    }
    
}

              这下好了,现在我们的User,Person,Employee要是想具有可被观察的功能,那就改去继承我们适配好的BaseObservableEntity就好了,而且由于BaseObservableEntity继承了BaseEntity,所以他们三个依然处于我们实体的继承体系中,而且由于我们的BaseObservableEntity是新增的扩展基类,所以不会对原来的继承体系造成破坏。

              适配器模式的用法还是比较清晰的,我们以上两种方式都是为了复用现有的代码而采用的适配器模式,LZ刚才说了,根据目的的不同,适配器模式也可以分为两种,那么上述便是第一种,可称为定制适配器,还有另外一种称为缺省适配器

              首先我们得先说下缺省适配器为什么要出现,因为适配器模式大部分情况下是为了补救,所以既然补救,那么肯定是历史原因造成的我们需要使用这个模式。

              我们来看看缺省适配器的历史来由,不知各位还是否记得在第一章总纲中,LZ曾经提到过一个原则,最小接口原则。

              这个原则所表达的思想是说接口的行为应该尽量的少,那么还记得LZ当时说如果你没做到的话会产生什么情况吗?

              结果就是实现这个接口的子类,很可能出现很多方法是空着的情况,因为你的接口设计的过大,导致接口中原本不该出现的方法出现了,结果现在子类根本用不上这个方法,但由于JAVA语言规则的原因,实现一个接口必须实现它的全部方法,所以我们的子类不得不被迫写一堆空方法在那,只为了编译通过。

              所以为了解决这一问题,缺省适配器就出现了。比如我们有如下接口。

public interface Person {
    
    void speak();
    
    void listen();
    
    void work();
    
}

                 这是一个人的接口,这个接口表示了人可以说话,听和工作,假设是两年前的LZ,还在家待业呢,LZ没工作啊,但是LZ也是个人啊,所以LZ要实现这个接口,所以LZ只能把work方法抄下来空着放在那了,假设LZ是个聋哑人,好吧,三个方法都要空着了,但是LZ表示,LZ是人,LZ一定要实现Person接口。

                 当然,上述只是举个例子,但是真实项目当中也会出现类似的情况,那么怎么办呢?

                 这下来了,我们的缺省适配器来了,如下。

public class DefaultPerson implements Person{

    public void speak() {
    }

    public void listen() {
    }

    public void work() {
    }

}

                 我们创造一个Person接口的默认实现,它里面都是一些默认的方法,当然这里因为没什么可写的就空着了,实际当中可能会加入一些默认情况下的操作,比如如果方法返回结果整数,那么我们在缺省适配器中可以默认返回个0。

                 这下好了,LZ只要继承这个默认的适配器(DefaultPerson),然后覆盖掉LZ感兴趣的方法就行了,比如speak和listen,至于work,由于适配器帮我们提供了默认的实现,所以就不需要再写了。

                 这种情况其实蛮多的,因为接口设计的最小化只是理想状态,难免会有一些实现类,对其中某些方法不感兴趣,这时候,如果方法过多,子类也很多,并且子类的大部分方法都是空着的,那么就可以采取这种方式了。

                 当然,这样做违背了里氏替换原则,但是上面的做法原本就违背了接口的最小化原则,所以我们在真正使用时要权衡二者的利弊,到底我们需要的是什么。所以从此也可以看出来,原则只是指导,并不一定也不可能全部满足,所以我们一定要学会取舍。

                 总结下两种实现方式的适配器所使用的场景,两者都是为了将已有类的代码复用并且适配到客户端需要的接口上去。

                 1,第一种类适配器,一般是针对适配目标是接口的情况下使用。

                 2,第二种对象适配器,一般是针对适配目标是类或者是需要复用的对象多于一个的时候使用,这里再专门提示一下,对象适配器有时候是为了将多个类一起适配,所以才不得不使用组合的方式,而且我们采用对象适配器的时候,继承也不是必须的,而是根据实际的类之间的关系来进行处理,上述例子当中一定要直接或间接的继承自BaseEntity是为了不破坏我们原来的继承体系,但有些情况下这并不是必须的。

                 对于第三个缺省适配器,一般是为了弥补接口过大所犯下的过错,但是也请注意衡量利弊,权衡好以后再考虑是否要使用缺省适配器。

                 好了,本次适配器模式的分享就到此结束了,希望各位可以从中得到点收获。

                 最后,感谢您的收看。

                 下期预告,模板方法模式。

版权声明:程序员胖胖胖虎阿 发表于 2022年10月3日 下午9:00。
转载请注明:(八)适配器模式详解 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...