一、基本概念
1.1 定义
适配器模式 将 某个类的接口 转换成 客户端期望的另一个接口 来表示,让原本因接口不能一起工作的两个类可以协同工作。
经典的适配器模式 可以分为下面三类:
- 类 的适配器模式
- 对象 的适配器模式
- 接口 的适配器模式
1.2 分类
1.2.1 类的适配器模式
Adapter
类继承于SRC
类,实现DST
接口,完成SRC
到DST
的适配。
- 原始接口
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
接口,完成SRC
到DST
的适配,它和 类适配器模式 最大的区别在于对于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() {
}
}
- 客户端根据需要去重写对应的方法,分别为
DstAdapterImpl
和DstAdapterImpl2
/**
* 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 对象适配模式
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%
的人都要好。
- 你在项目当中遇到了什么问题,怎么解决的。
- 你在项目中用到了什么设计模式。
- 说说你在项目中觉得做得最好的部分。