java/android 设计模式学习笔记(6)---适配器模式

  这篇来介绍一下适配器模式(Adapter Pattern),适配器模式在开发中使用的频率也是很高的,像 ListView 和 RecyclerView 的 Adapter 等都是使用的适配器模式。在我们的实际生活中也有很多类似于适配器的例子,比如香港的插座和大陆的插座就是两种格式的,为了能够成功适配,一般会在中间加上一个电源适配器,形如:

  

这样就能够将原来不符合的现有系统和目标系统通过适配器成功连接。

  说到底,适配器模式是将原来不兼容的两个类融合在一起,它有点类似于粘合剂,将不同的东西通过一种转换使得它们能够协作起来。碰到要在两个完全没有关系的类之间进行交互,第一个解决方案是修改各自类的接口,但是如果无法修改源代码或者其他原因导致无法更改接口,此时怎么办?这种情况我们往往会使用一个 Adapter ,在这两个接口之间创建一个粘合剂接口,将原本无法协作的类进行兼容,而且不用修改原来两个模块的代码,符合开闭原则。

  PS:对技术感兴趣的同鞋加群544645972一起交流

设计模式总目录

  java/android 设计模式学习笔记目录

特点

  适配器模式把一个类的接口换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

  所以,这个模式可以通过创建适配器进行接口转换,让不兼容的接口兼容,这可以让客户实现解耦。如果在一段时间之后,我们想要改变接口,适配器可以将改变的部分封装起来,客户就不必为了应对不同的接口而每次跟着修改。

  适配器模式的使用场景可以有以下几种:

  1. 系统需要使用现有的类,而此类的接口不符合系统的需要,即接口不兼容;
  2. 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大联系的一些类,包括一些可能在将来引进的类一起工作;
  3. 需要一个统一的输出接口,而输入端的类型不可预知。

UML类图

  适配器模式在实际使用过程中有“两种”方式:对象适配器和类适配器。

类适配器模式

  首先看一下类适配器模式的 uml 类图:

  

类适配器是通过实现 ITarget 接口以及继承 Adaptee 类来实现接口转换,目标接口需要的是 operation1() 的操作,而 Adaptee 类只能提供一个 operation2() 的操作,因此就出现了不兼容的情况,此时通过 Adapter 实现一个 operation1() 函数将 Adaptee 的 operation2() 转换为 ITarget 需要的操作,以此实现兼容。类适配器模式有三个角色:

  • Target:目标角色,也就是所期待得到的接口,由于这里讨论的是类适配器模式,因此目标不可以是类;
  • Adaptee:现在需要适配的接口;
  • Adapter:适配器角色,适配器把源接口转换成目标接口,所以这一个角色必须是具体类。

对象适配器模式

  对象适配器模式 uml 类图:

  

uml 类图和类适配器模式基本一样,区别就在于对象适配器模式与 Adaptee 的关系是 Dependency,而类适配器是 Generalization ,一个是依赖,一个是继承。所以 Adapter 类会持有一个 Adaptee 对象的引用,并且通过 operation1() 方法将该 Adaptee 对象与 ITarget 接口的相关操作衔接起来。

  这种实现方式直接将要被适配的对象传递到 Adapter 中,使用组合的形式实现接口兼容的效果,这种模式比类适配器模式更加灵活,它的另一个好处是被适配对象中的方法不会暴露出来,而类适配器由于继承了被适配对象,因此,被适配对象类的函数在 Adapter 类中也都含有,这使得 Adapter 类出现了一些奇怪的接口,用于使用成本较高。因此,对象适配器模式更加灵活和实用。

对比

  类适配器模式使用的是继承的方式,而对象适配器模式则使用的是组合的方法。从设计模式的角度来说,对象适配器模式遵循 OO 设计原则的“多用组合,少用继承”,这是一个优点,但是类适配器模式有一个好处是它不需要重新实现整个被适配者的行为,毕竟类适配器模式使用的是继承的方式,当然这么做的坏处就是失去了使用组合的弹性。

  所以在实际过程中需要根据使用情况而定,如果 Adaptee 类的行为很复杂,但是 Adapter 适配器类并不需要这些大部分的无关行为,那么使用对象适配器模式是合适的,但是如果需要重新实现大部分 Adaptee 的行为,那么就要考虑是否使用类适配器模式了。

示例与源码

类适配器模式

  我们以最上面说到的香港的英式三角插座和大陆的三角插座为例,来构造类适配器模式,首先是两个插座格式类:

IChinaOutlet.class

public interface IChinaOutlet {
    public String getChinaType();
}

ChinaOutlet.class

public class ChinaOutlet implements IChinaOutlet{
    @Override
    public String getChinaType() {
        return "Chinese three - pin socket";
    }
}

上面是中式插座的输出格式,然后是香港的英式插座输出格式:

HKOutlet.class

public class HKOutlet {
    public String getHKType() {
        return "British three - pin socket";
    }
}

为了将香港的英式插座转换为中式插座,我们需要构造一个 Adapter 类,目的是进行插座格式的转换:

OutletAdapter.class

public class OutletAdapter extends HKOutlet implements IChinaOutlet{
    @Override
    public String getChinaType() {
        String type = getHKType();
        type = type.replace("Chinese", "British");
        return type;
    }
}

这样就实现了插座接口的转换,例子很简单,明了。当然这个例子很简单,要的就是要学会这个思想:在不修改原来类的基础上,将原来类进行扩展后使用在新的目标系统上。

对象适配器模式

  对象适配器模式就以我几年前写过的一个 View 作为例子:android一个转盘效果的容器viewgroup,这个例子就是典型的“需要统一的输出接口,而输入端的类型不可预知”情形,需要输出的是一个个 View ,而输入的数据是未知的。原先的处理方式是使用动态 addChild 的方式添加子 View,然后使用removeChild 方法删除子 View :

...
public void addChild(final View view) throws NumberOverFlowException{
    if(childNum < maxNum){
        //每次添加子view的时候都要重新计算location数组
        location.add(new FloatWithFlag());
        TurnPlateViewUtil.getLocationByNum(location);
        view.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                listener.onClick((String)arg0.getTag());
            }
        });
        view.setOnLongClickListener(new OnLongClickListener() {

            @Override
            public boolean onLongClick(View arg0) {
                initPopUpWindow();
                window.showAsDropDown(arg0);
                viewIsBeingLongClick = arg0;
                return false;
            }
        });
        addView(view);
        childNum++;
    }else{
        throw new NumberOverFlowException(maxNum);
    }
}

public void removeChild(final View view){
    try{
        this.removeView(view);
        location.remove(0);
        childNum--;
        TurnPlateViewUtil.getLocationByNum(location);
        requestLayout();
    }catch(Exception e){

    }
}
...

使用这种方式会造成外部对子 View 的操纵很繁琐,换位思考一下,如果 ListView 需要以 addView 和 removeView 的方式去处理,那是极其头疼的,所以现在我们可以换一种思维进行改进,学习 ListView 的 Adapter 思想,我们也使用适配器的方式进行处理,为了方便这里就直接继承 BaseAdapter 吧,改造后的代码如下:

/**
 * 设置适配器
 * @param adapter
 */
public void setAdapter(BaseAdapter adapter) throws NumberOverFlowException {
    this.adapter = adapter;
    if (adapter.getCount() > MAX_NUM) {
        throw new NumberOverFlowException(adapter.getCount());
    }
    adapter.registerDataSetObserver(new DataSetObserver() {
        @Override
        public void onChanged() {
            super.onChanged();
            onDataSetChanged();
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();
            onDataSetChanged();
        }
    });
    initChild();
}

/**
 * 数据源发生变更,需要重新绘制布局
 */
private void onDataSetChanged(){
    initChild();
}
...
private void initChild() {
    removeAllViews();
    location.clear();

    for (int i=0; i < adapter.getCount(); i++) {
        //每次添加子view的时候都要重新计算location数组
        location.add(new FloatWithFlag());
        TurnPlateViewUtil.getLocationByNum(location);
        View view = adapter.getView(i, null, this);
        view.setTag(i);
        view.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                listener.onClick((String)arg0.getTag());
            }
        });
        view.setOnLongClickListener(new OnLongClickListener() {

            @Override
            public boolean onLongClick(View arg0) {
                initPopUpWindow();
                window.showAsDropDown(arg0);
                viewIsBeingLongClick = arg0;
                return false;
            }
        });
        addView(view);
    }
}

外部使用时直接继承 BaseAdapter 类,然后在对应方法中返回对应 View 即可,这样就实现了“不同的输入,同样的输出”:

private class TurnPlateViewAdapter extends BaseAdapter{

    @Override
    public int getCount() {
        return 5;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
        drawable.setBounds(0, 0,drawable.getMinimumHeight() , drawable.getMinimumHeight());
        TextView textview = new TextView(MainActivity.this);
        textview.setTextColor(getResources().getColor(android.R.color.white));
        textview.setText(R.string.text);
        textview.setCompoundDrawables(null, drawable, null, null);
        textview.setTag(tag++ +"");
        return textview;
    }
}

这样,外部修改输入数据之后,通知 adapter 数据源变更,因为已经注册观察者,所以 TurnplateView 自然而然可以收到通知,并且刷新界面,最后实现效果和以前一样:

  由此感慨,在最初学习 android 的时候,listView 的 adapter 知道怎么使用,但是并没有去深究为什么这么使用,其实里面很多地方都透着设计模式的思想,源码真可谓是第一手学习资料。

总结

  Adapter 模式的经典实现在于将原本不兼容的接口融合在一起,使之能够很好的进行合作。但是,在实际开发中, Adapter 模式也会可以根据实际情况进行适当的变更,最典型的就是 ListView 和 RecyclerView 了,这种设计方式使得整个 UI 架构变得非常灵活,能够拥抱变化。所以在实际使用的时候,遵循上面说过的三种场景:

  1. 系统需要使用现有的类,而此类的接口不符合系统的需要,即接口不兼容;
  2. 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大联系的一些类,包括一些可能在将来引进的类一起工作;
  3. 需要一个统一的输出接口,而输入端的类型不可预知。

根据情况进行变化,将适配器模式灵活运用在实际开发中。

  总结下来,Adapter 模式的优点基本已经明确了:

  • 更好的复用性
  • 系统需要使用现有的类,而此类的接口不符合系统的需要,那么通过适配器模式就可以让这些功能得到更好的复用;

  • 更好的扩展性
  • 在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。

总结一下就是对扩展开放和对修改关闭的开闭原则吧。

  当然适配器模式也有一些缺点,如果在一个系统中过多的使用适配器模式,会让系统非常零乱,不易整体把握。例如,明明看到调用的是 A 接口,其实内部被适配成 B 类的实现,这样就增加了维护性,过多的使用就显得很没有必要了,不如直接对系统进行重构。

源码下载

  https://github.com/zhaozepeng/Design-Patterns/tree/master/AdapterPattern

引用

http://www.android100.org/html/201506/20/155883.html

https://en.wikipedia.org/wiki/Adapter_pattern

时间: 2024-10-26 09:18:07

java/android 设计模式学习笔记(6)---适配器模式的相关文章

java/android 设计模式学习笔记(14)---外观模式

这篇博客来介绍外观模式(Facade Pattern),外观模式也称为门面模式,它在开发过程中运用频率非常高,尤其是第三方 SDK 基本很大概率都会使用外观模式.通过一个外观类使得整个子系统只有一个统一的高层的接口,这样能够降低用户的使用成本,也对用户屏蔽了很多实现细节.当然,在我们的开发过程中,外观模式也是我们封装 API 的常用手段,例如网络模块.ImageLoader 模块等.其实我们在开发过程中可能已经使用过很多次外观模式,只是没有从理论层面去了解它. 转载请注明出处:http://bl

java/android 设计模式学习笔记(9)---代理模式

这篇博客我们来介绍一下代理模式(Proxy Pattern),代理模式也成为委托模式,是一个非常重要的设计模式,不少设计模式也都会有代理模式的影子.代理在我们日常生活中也很常见,比如上网时连接的代理服务器地址,更比如我们平时租房子,将找房子的过程代理给中介等等,都是代理模式在日常生活中的使用例子. 代理模式中的代理对象能够连接任何事物:一个网络连接,一个占用很多内存的大对象,一个文件,或者是一些复制起来代价很高甚至根本不可能复制的一些资源.总之,代理是一个由客户端调用去访问幕后真正服务的包装对象

java/android 设计模式学习笔记(2)---观察者模式

这篇来讲一下观察者模式,观察者模式在实际项目中使用的也是非常频繁的,它最常用的地方是GUI系统.订阅--发布系统等.因为这个模式的一个重要作用就是解耦,使得它们之间的依赖性更小,甚至做到毫无依赖.以GUI系统来说,应用的UI具有易变性,尤其是前期随着业务的改变或者产品的需求修改,应用界面也经常性变化,但是业务逻辑基本变化不大,此时,GUI系统需要一套机制来应对这种情况,使得UI层与具体的业务逻辑解耦,观察者模式此时就派上用场了. PS:对技术感兴趣的同鞋加群544645972一起交流. 设计模式

java/android 设计模式学习笔记(10)---建造者模式

这篇博客我们来介绍一下建造者模式(Builder Pattern),建造者模式又被称为生成器模式,是创造性模式之一,与工厂方法模式和抽象工厂模式不同,后两者的目的是为了实现多态性,而 Builder 模式的目的则是为了将对象的构建与展示分离.Builder 模式是一步一步创建一个复杂对象的创建型模式,它允许用户在不知道内部构建细节的情况下,可以更精细地控制对象的构造流程.一个复杂的对象有大量的组成部分,比如汽车它有车轮.方向盘.发动机.以及各种各样的小零件,要将这些部件装配成一辆汽车,这个装配过

java/android 设计模式学习笔记(一)---单例模式

前段时间公司一些同事在讨论单例模式(我是最渣的一个,都插不上嘴 T__T ),这个模式使用的频率很高,也可能是很多人最熟悉的设计模式,当然单例模式也算是最简单的设计模式之一吧,简单归简单,但是在实际使用的时候也会有一些坑. PS:对技术感兴趣的同鞋加群544645972一起交流 设计模式总目录 java/android 设计模式学习笔记目录 特点 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例. 单例模式的使用很广泛,比如:线程池(threadpool).缓存(cache).对

java/android 设计模式学习笔记(7)---装饰者模式

这篇将会介绍装饰者模式(Decorator Pattern),装饰者模式也称为包装模式(Wrapper Pattern),结构型模式之一,其使用一种对客户端透明的方式来动态的扩展对象的功能,同时它也是继承关系的一种替代方案之一,但比继承更加灵活.在现实生活中也可以看到很多装饰者模式的例子,或者可以大胆的说装饰者模式无处不在,就拿一件东西来说,可以给它披上无数层不一样的外壳,但是这件东西还是这件东西,外壳不过是用来扩展这个东西的功能而已,这就是装饰者模式,装饰者的这个角色也许各不相同但是被装饰的对

java/android 设计模式学习笔记(13)---享元模式

这篇我们来介绍一下享元模式(Flyweight Pattern),Flyweight 代表轻量级的意思,享元模式是对象池的一种实现.享元模式用来尽可能减少内存使用量,它适合用于可能存在大量重复对象的场景,缓存可共享的对象,来达到对象共享和避免创建过多对象的效果,这样一来就可以提升性能,避免内存移除和频繁 GC 等. 享元模式的一个经典使用案例是文本系统中图形显示所用的数据结构,一个文本系统能够显示的字符种类就是那么几十上百个,那么就定义这么些基础字符对象,存储每个字符的显示外形和其他的格式化数据

java/android 设计模式学习笔记(12)---组合模式

这篇我们来介绍一下组合模式(Composite Pattern),它也称为部分整体模式(Part-Whole Pattern),结构型模式之一.组合模式比较简单,它将一组相似的对象看作一个对象处理,并根据一个树状结构来组合对象,然后提供一个统一的方法去访问相应的对象,以此忽略掉对象与对象集合之间的差别.这个最典型的例子就是数据结构中的树了,如果一个节点有子节点,那么它就是枝干节点,如果没有子节点,那么它就是叶子节点,那么怎么把枝干节点和叶子节点统一当作一种对象处理呢?这就需要用到组合模式了. 转

java/android 设计模式学习笔记(16)---命令模式

这篇博客我们来介绍一下命令模式(Command Pattern),它是行为型设计模式之一.命令模式相对于其他的设计模式更为灵活多变,我们接触比较多的命令模式个例无非就是程序菜单命令,如在操作系统中,我们点击关机命令,系统就会执行一系列的操作,如先是暂停处理事件,保存系统的一些配置,然后结束程序进程,最后调用内核命令关闭计算机等,对于这一系列的命令,用户不用去管,用户只需点击系统的关机按钮即可完成如上一系列的命令.而我们的命令模式其实也与之相同,将一系列的方法调用封装,用户只需调用一个方法执行,那