设计模式知识梳理(1) - 结构型 - 适配器模式

96
泽毛
2018.06.24 20:48* 字数 2406

一、基本概念

1.1 定义

适配器模式某个类的接口 转换成 客户端期望的另一个接口 来表示,让原本因接口不能一起工作的两个类可以协同工作。

经典的适配器模式 可以分为下面三类:

  • 的适配器模式
  • 对象 的适配器模式
  • 接口 的适配器模式

1.2 分类

1.2.1 类的适配器模式

Adapter类继承于SRC类,实现DST接口,完成SRCDST的适配。

  • 原始接口Src
public class SrcClass {

    /**
     * 原始的接口。
     */
    public String srcMethod() {
        return "call srcMethod";
    }
}
  • 客户端可接受的接口Dst
public interface DstClass {

    /**
     * 客户端接受的接口。
     */
    String dstMethod();
}
  • Adapter实现
/**
 * 类适配器模式,继承于原始对象 (SrcClass),并实现了客户端可以接受的接口(DstClass)。
 */
public class Adapter extends SrcClass implements DstClass {

    @Override
    public String dstMethod() {
        return srcMethod();
    }
}

优点:

  • 继承于SRC类,可以根据需求重写SRC类的方法。

缺点:

  • 需要继承于SRC类,导致DST必须是接口。
  • SRC类的方法在Adapter中会暴露出来,增加了使用的成本。

1.2.2 对象的适配器模式

Adapter持有SRC类,实现DST接口,完成SRCDST的适配,它和 类适配器模式 最大的区别在于对于SRC的处理,对象适配器模式采用的是 持有,而后者采用的是 继承

  • 原始接口SRC
public class SrcClass {

    /**
     * 原始的接口。
     */
    public String srcMethod() {
        return "call srcMethod";
    }
}
  • 客户端可接受的接口Dst
public interface DstClass {

    /**
     * 客户端接受的接口。
     */
    String dstMethod();
}
  • Adapter
/**
 * 类适配器模式,Adapter 持有 Src 对象,并实现了 Dst 接口,当调用 Dst 接口声明
 * 的方法时,再调用内部持有的 Src 对象的方法,从而完成适配。
 */
public class Adapter implements DstClass {

    private SrcClass mSrcClass;

    public Adapter(SrcClass srcClass) {
        mSrcClass = srcClass;
    }

    @Override
    public String dstMethod() {
        return mSrcClass.srcMethod();
    }
}

优点:

  • 采用组合替代了继承。
  • 解决了Adapter必须继承于SRC的局限性问题,不再强求DST必须是接口。

1.2.3 接口的适配器模式

与前两者不大一样,该模式用于解决 接口的复用问题,当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现,那么该抽象类的子类可以有选择地覆盖某些方法来实现需求,它适用于 一个接口不想使用其所有方法 的情况。

  • 声明了3个接口方法
public interface IDst {
    
    void dstMethod1();
    
    void dstMethod2();
    
    void dstMethod3();
}
  • Adapter层默认都为空实现
public class DstAdapter implements IDst {

    @Override
    public void dstMethod1() {

    }

    @Override
    public void dstMethod2() {

    }

    @Override
    public void dstMethod3() {

    }
}
  • 客户端根据需要去重写对应的方法,分别为DstAdapterImplDstAdapterImpl2
/**
 * DstAdapterImpl 只关心 1 和 2 方法,因此它只重写了这两个。
 */
public class DstAdapterImpl extends DstAdapter {

    @Override
    public void dstMethod1() {
        Log.d("DstAdapter", "DstAdapterImpl.dstMethod1");
    }

    @Override
    public void dstMethod2() {
        Log.d("DstAdapter", "DstAdapterImpl.dstMethod2");
    }
}
/**
 * DstAdapterImpl2 只关心 1 和 3,因此它只需要重写这两个。
 */
public class DstAdapterImpl2 extends DstAdapter {

    @Override
    public void dstMethod1() {
        Log.d("DstAdapter", "DstAdapterImpl2.dstMethod1");
    }

    @Override
    public void dstMethod3() {
        Log.d("DstAdapter", "DstAdapterImpl2.dstMethod3");
    }
}
  • 模拟调用的场景
public class Client {

    private IDst mDst;

    void addUpdateListener(IDst dst) {
        mDst = dst;
    }

    void call() {
        mDst.dstMethod1();
        mDst.dstMethod2();
        mDst.dstMethod3();
    }
}
public class Simulator {

    public static void simulate() {
        Client client1 = new Client();
        client1.addUpdateListener(new DstAdapterImpl());
        Client client2 = new Client();
        client2.addUpdateListener(new DstAdapterImpl2());
        client1.call();
        client2.call();
    }
    
}

1.3 应用场景

对于 类适配器模式对象适配器模式,它的应用场景为:

  • 想要使用一个已经存在的类,但是它却不符合现有的接口规范,导致无法直接去访问,这时创建一个适配器就能间接去访问这个类中的方法。
  • 我们有一个类,想将其设计为可重用的类,我们可以创建适配器来将这个类来适配其他没有提供合适接口的类。

对于 接口适配器模式,它的应用场景为:

  • 想要使用接口中的某个或某些方法,但是接口中有太多方法,我们要使用时必须实现接口并实现其中的所有方法,可以使用抽象类来实现接口,并不对方法进行实现(仅置空),然后我们再继承这个抽象类来通过重写想用的方法的方式来实现。

二、Android 源码中的应用

2.1 对象适配模式

Adapter 体系

ListView需要可以展示各式各样的视图,但是每个人要显示的效果不同,显示的数据类型也千变万化。这时候就是通过添加一个Adapter层来应对变化。

ListView需要的接口抽象到Adapter对象中,当ListView需要获取视图的样式、数量时,就会调用Adapter的接口,这样就很好地应对了ItemView的可变性。

2.2 接口适配器模式

Android属性动画当中的ValueAnimator类可以通过addListener方法来监听动画的执行情况:

    public void simulate() {
        Animator animator = ValueAnimator.ofInt(0, 1);
        animator.addListener(new Animator.AnimatorListener() {
            
            @Override
            public void onAnimationStart(Animator animation) {}

            @Override
            public void onAnimationEnd(Animator animation) {}

            @Override
            public void onAnimationCancel(Animator animation) {}

            @Override
            public void onAnimationRepeat(Animator animation) {}
        });
        animator.start();
    }

但有些时候,我们并不需要监听所有的状态,只想监听onAnimationStart,那么就可以传入AnimatorListenerAdapter,这时候,该Adapter就是一个接口适配器。

    public void simulate() {
        Animator animator = ValueAnimator.ofInt(0, 1);
        animator.addListener(new AnimatorListenerAdapter() {

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
            }
            
        });
        animator.start();
    }

其内部的实现为:

public abstract class AnimatorListenerAdapter implements Animator.AnimatorListener, Animator.AnimatorPauseListener {

    @Override
    public void onAnimationCancel(Animator animation) {}

    @Override
    public void onAnimationEnd(Animator animation) {}

    @Override
    public void onAnimationRepeat(Animator animation) {}

    @Override
    public void onAnimationStart(Animator animation) {}

    @Override
    public void onAnimationPause(Animator animation) {}

    @Override
    public void onAnimationResume(Animator animation) {}

}

三、项目的使用场景

3.1 信息流数据实体适配

从信息流的每个Item来看,其数据实体分为两个部分:

  • View展示需要的信息。
  • 服务端接口下发的实体。

View展示的信息通常是稳定的,例如对于单图的Item,那么视图需要的信息就只有是标题、图片、跳转url、发布时间等需要展示的信息,这些部分往往都是 固定不变 的。

而视图对应的服务端下发实体,往往是异变的,又或者是每个列表的数据源不相同,这时候就需要进行一层适配,防止当其对应的实体字段发生变化的时候,又需要去修改视图层的代码。

public class NewsItem {

    @IntDef({SrcType.SRC_TYPE_UC, SrcType.SRC_TYPE_TC})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SrcType {
        int SRC_TYPE_UC = 1;
        int SRC_TYPE_TC = 2;
    }

    private @SrcType int mSrcType;
    private UcBean mUcBean;
    private TcBean mTcBean;

    public static NewsItem newInstance(Object object) {
        if (object instanceof UcBean) {
            return new NewsItem((UcBean) object);
        } else if (object instanceof TcBean) {
            return new NewsItem((TcBean) object);
        }
        return null;
    }

    private NewsItem(UcBean ucBean) {
        mUcBean = ucBean;
        mSrcType = SrcType.SRC_TYPE_UC;
    }

    private NewsItem(TcBean tcBean) {
        mTcBean = tcBean;
        mSrcType = SrcType.SRC_TYPE_TC;
    }

    /**
     * 获取其唯一标识值。
     *
     * @return 唯一标志。
     */
    public String getIdentify() {
        if (mSrcType == SrcType.SRC_TYPE_TC) {
            return mUcBean.getId();
        } else if (mSrcType == SrcType.SRC_TYPE_UC) {
            return mTcBean.getAId();
        }
        return "";
    }

    /**
     * 获取标题。
     *
     * @return 标题。
     */
    public String getTitle() {
        if (mSrcType == SrcType.SRC_TYPE_TC) {
            return mUcBean.getUcTitle();
        } else if (mSrcType == SrcType.SRC_TYPE_UC) {
            return mTcBean.getTcTitle();
        }
        return "";
    }

}

3.2 内核的解耦

在使用WebView的时候,往往会直接调用android.webkit.**包下的接口和类,这就导致了接入第三方的内核时,会需要去修改业务层的代码,这其实是非常不划算的。因此需要对WebView的所有接口都进行一层封装。

3.3 信息流数据上报策略

  • 声明上报策略的接口
public interface EventUploader {

    /**
     * 点击后上报。
     */
    void onItemClick(NewsItem newsItem);

    /**
     * 依附到试图后上报。
     */
    void onItemAttach(NewsItem newsItem);

    /**
     * 脱离试图后上报。
     */
    void onItemDetach(NewsItem newsItem);

    /**
     * 调用 convert() 方法后上报。
     */
    void onItemConvert(NewsItem newsItem);
}
  • 基础的实现
public class AbsUploader implements EventUploader {

    @Override
    public void onItemClick(NewsItem newsItem) { }

    @Override
    public void onItemAttach(NewsItem newsItem) {}

    @Override
    public void onItemDetach(NewsItem newsItem) {}

    @Override
    public void onItemConvert(NewsItem newsItem) {}
}
  • 不同的数据源又需要上报数据给定义的Cp,不同的Cp又有不同的上报要求,那么这时候就需要再重写对应的方法即可,其它部分采用默认的实现。
public class UcEventUploader extends AbsUploader {

    @Override
    public void onItemClick(NewsItem newsItem) {
        super.onItemClick(newsItem);
        Log.d("EventUploader", "uc.onItemClick");
    }
}
public class TcEventUploader extends AbsUploader {

    @Override
    public void onItemAttach(NewsItem newsItem) {
        super.onItemAttach(newsItem);
        Log.d("EventUploader", "tc.onItemAttach");
    }

    @Override
    public void onItemDetach(NewsItem newsItem) {
        super.onItemDetach(newsItem);
        Log.d("EventUploader", "tc.onItemDetach");
    }

    @Override
    public void onItemConvert(NewsItem newsItem) {
        super.onItemConvert(newsItem);
        Log.d("EventUploader", "tc.onItemConvert");
    }
}
  • NewsItem中返回不同的上报策略。
public class NewsItem {

    @IntDef({SrcType.SRC_TYPE_UC, SrcType.SRC_TYPE_TC})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SrcType {
        int SRC_TYPE_UC = 1;
        int SRC_TYPE_TC = 2;
    }

    private @SrcType int mSrcType;
    private EventUploader mEventUploader;

    /**
     * 返回上报的策略。
     * 
     * @return 上报策略。
     */
    public EventUploader getUploader() {
        if (mEventUploader != null) {
            return mEventUploader;
        }
        if (mSrcType == SrcType.SRC_TYPE_TC) {
            mEventUploader = new UcEventUploader();
        } else if (mSrcType == SrcType.SRC_TYPE_UC) {
            mEventUploader = new TcEventUploader();
        } else {
            mEventUploader = new AbsUploader();
        }
        return mEventUploader;
    }
}

四、关于学习设计模式的一点思考

我们一直在谈设计模式,特别是在面试的时候,“你用过什么设计模式?”也是一个经常会被问到的问题,但是在学习设计模式的时候,我们往往会陷入一个误区,认为把设计模式的类图、Demo以及优缺点都记下来就算学好了,但其实这是远远不够的,过个一两个月,肯定就忘得干干净净了。

4.1 学习思路

设计模式从本质上来说,是 一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,所以当我们在学习的时候,如果脱离了工程的实践,仅仅满足于记下几个类图和Demo,其实是毫无意义的。

下图是我对于学习设计模式的一些理解,大家可以借鉴一下:

设计模式学习的思路

了解基本的概念,总结成文档

网上的文章、书关于设计模式的太多了,找一些权威的总结成文档,主要是下面几个方面:

  • 定义
  • 类图和Demo
  • 优缺点

这一步完成了,仅仅只是 知道 有这个东西而已,下面才是真正开始 学习 的过程。

第一步:发现源码当中的设计模式

Android的源码其实就是最好的 代码范例,通过阅读源码,我们可以更直观地体会到设计模式的应用场景、以及它究竟解决了什么问题。

第二步:文档总结

在阅读了源码之后,一定要 总结,虽然代码一直都放在那里,但是只有真正写下来的东西才是你自己的,而且我们要想一下,为什么要去读源码,绝不是说从头到尾把流程走一遍,知道它是怎么干的就完了。

而是要总结 它是怎么设计的,为什么要这么设计

第三步:应用到项目当中

走完前两步之后,我们对于这种设计模式的基本概念已经掌握得很熟练了,接下来就是要把它应用到项目当中,在设计的时候,不要只满足于满足现有的需求,要多想一个问题,万一 xxx 发生了变化,我这份代码还可以正常地工作吗?

当真正地用到了项目当中,每天上班的时候看到,就相当于又复习了一遍,还会发愁记不下来吗?

如果遇到问题的时候,没有很好的思路,那么再去参考一下源码,看看Google的大神们是如何解决问题的。

第四步:文档总结

应用完之后,把解决的问题,对应的场景总结下来。

4.2 总结

设计模式与其它的知识不太一样,它不是你花个一两天,看几篇文章,或者听一次分享就能掌握的东西,这是一个不断学习、反馈,再学习的无限循环过程,对于每种设计模式,就像这篇文章一样,我会把它分成三个方面:

  • 理论
  • Android 源码
  • 项目的应用场景

随着你看过的源码越来越多,接触的业务场景越来越多,文档也会更加丰富,等到找工作面试的时候,面试官再问你下面这些问题,你肯定答得比90%的人都要好。

  • 你在项目当中遇到了什么问题,怎么解决的。
  • 你在项目中用到了什么设计模式。
  • 说说你在项目中觉得做得最好的部分。
设计模式
Web note ad 1