设计模式之适配器模式
参考文章:https://www.jianshu.com/p/9d0575311214
定义
适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够一起工作。
使用场景
用电源接口做例子,笔记本电脑的电源一般都是接受5V的电压,但是我们生活中的电线电压一般都是220V的输出。这个时候就出现了不匹配的状况,我们称之为接口不兼容。
此时就需要适配器来进行一个接口转换。有一句话正好体现了这点:任何问题都可以加一个中间层来解决。这个层我们可以理解为这里的Adapter层,通过这层来进行一个接口转换就达到了兼容的目的。
适配器模式分为两种
- 类适配器模式 :通过实现Target接口以及继承Adaptee类来实现接口转换
- 对象适配器模式:实现Target接口和代理Adaptee的某个方法来实现接口转换
角色介绍
-
目标(Target)角色:这就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因此目标不可以是类。
-
源(Adaptee)角色:现在需要适配的接口。
-
适配器(Adapter)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。
在上述电源接口这个示例中,5V电压就是Target接口,220v电压就是Adaptee类,而将电压从220V转换到5V就是Adapter。
类适配器模式
简介
类的适配器模式是把适配的类的API转换成为目标类的API。
UML图
出现背景
从上图可以看出:
- 问题:Target期待调用Request方法,而Adaptee并没有。因此他们的接口不兼容。
- 解决方案:为使Target能够使用Adaptee类里的SpecificRequest方法,故提供一个中间环节Adapter类**(继承Adaptee并实现Target接口)**,把Adaptee的API与Target的API衔接起来(适配)。
Adapter与Adaptee是继承关系,这决定了这个适配器模式是类的
使用步骤
步骤1.创建Target接口
public interface Target {
public void Request();
}
步骤2.创建需要适配的类Adaptee
public class Adaptee {
public void SpecificRequest(){
}
}
步骤3.创建适配器类Adapter
//适配器Adapter继承自Adaptee,同时又实现了目标(Target)接口。
public class Adapter extends Adaptee implements Target {
//目标接口要求调用Request()这个方法名,但源类Adaptee没有方法Request()
//因此适配器补充上这个方法名
//但实际上Request()只是调用源类Adaptee的SpecificRequest()方法的内容
//所以适配器只是将SpecificRequest()方法作了一层封装,封装成Target可以调用的Request()而已
@Override
public void Request() {
this.SpecificRequest();
}
}
步骤4.定义使用目标类,并通过Adapter类调用所需要的方法从而实现目标
public class AdapterPattern {
public static void main(String[] args){
Target mAdapter = new Adapter();
mAdapter.Request();
}
}
这样,就达到了我们的目的。让Target成功使用了Adaptee类的specificRequest方法。
对象适配器模式
简介
与类的适配器模式相同,对象的适配器模式也是把适配的类的API转换成为目标类的API。
UML图
出现背景
从上图可以看出:
- 问题:Target期待调用Request方法,而Adaptee并没有。因此他们的接口不兼容。
- 解决方案:为了使Target能够使用Adaptee类里的SpecificRequest方法,提供一个中间环节Adapter类**(包装了一个Adaptee的实例)**,把Adaptee的API与Target的API衔接起来(适配)。
Adapter与Adaptee是包含关系,因此适配器模式是对象的。
使用步骤
步骤1.创建Target接口
public interface Target {
public void Request();
}
步骤2.创建需要适配的类Adaptee
public class Adaptee {
public void SpecificRequest(){
}
}
步骤3.创建适配器类Adapter(使用包含的方式)
public class Adapter implements Target{
private Adaptee adaptee;
// 通过构造函数传入具体需要适配的被适配类对象
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void Request() {
// 使用委托的方式完成特殊功能
this.adaptee.SpecificRequest();
}
}
步骤4.定义使用目标类,并通过Adapter类调用所需要的方法从而实现目标
public class AdapterPattern {
public static void main(String[] args){
//需要先创建一个被适配类的对象作为参数
Target mAdapter = new Adapter(new Adaptee());
mAdapter.Request();
}
}
适配器模式的优缺点
优点
- 更好的复用性
系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。 - 透明、简单
客户端可以调用同一接口,因而对客户端来说是透明的。这样做更简单 & 更直接 - 更好的扩展性
在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。 - 解耦性
将目标类和适配者类解耦,通过引入一个适配器类重用现有的适配者类,而无需修改原有代码 - 符合开放-关闭原则
同一个适配器可以把适配者类和它的子类都适配到目标接口;可以为不同的目标接口实现不同的适配器,而不需要修改待适配类
缺点
- 过多的使用适配器,会让系统非常零乱,不易整体进行把握
两种适配器模式的优缺点比较
类适配器模式
优点
- 使用方便,代码简化
仅仅引入一个对象,并不需要额外的字段来引用Adaptee实例
缺点
- 高耦合,灵活性低
使用对象继承的方式,是静态的定义方式
对象适配器模式
优点
- 灵活性高、低耦合
采用 “对象组合”的方式,是动态组合方式
缺点
- 使用复杂
需要引入对象实例
适配器模式的应用场景
- 系统需要复用现有类,而该类的接口不符合系统的需求,可以使用适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
- 多个组件功能类似,但接口不统一且可能会经常切换时,可使用适配器模式,使得客户端可以以统一的接口使用它们
ListView中的适配器模式
简述
我们都知道,作为最重要的View之一,ListView需要能够显示各式各样的视图。每个项目中需要的显示效果各不相同,显示的数据类型、数量等也有很大差别。那么如何隔离这种变化就显得尤为重要了。
因此,在此处Android采用了增加一个Adapter层的方式来应对变化。将ListView需要的接口抽象到Adapter中,这样只要用户实现了Adapter的接口,ListView就可以按照用户设定的显示效果、数量、数据来显示特定的Item View。
通过数据集来告知ListView数据的个数( getCount函数 )以及每个数据的类型( getItem函数 ),最重要的是要解决Item View的输出。Item View千变万化,但终究它都是View类型,Adapter统一将Item View输出为View ( getView函数 ),这样就很好的应对了Item View的可变性。
那么ListView是如何通过Adapter模式 ( 不仅仅是Adapter模式 )运作的呢 ?我们可以查看源码。
分析
由于ListView继承自AbsListView,Adapter也定义在这个类中,所以我们查看这个类的源码(部分)。
public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
ViewTreeObserver.OnTouchModeChangeListener,
RemoteViewsAdapter.RemoteAdapterConnectionCallback {
ListAdapter mAdapter ;
// 关联到Window时调用的函数
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
...
// 给适配器注册一个观察者(观察者模式)
if (mAdapter != null && mDataSetObserver == null) {
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
// Data may have changed while we were detached. Refresh.
mDataChanged = true;
mOldItemCount = mItemCount
// 获取Item的数量,调用的是mAdapter的getCount方法
mItemCount = mAdapter.getCount();
}
mIsAttached = true;
}
/**
* 子类需要覆写layoutChildren()函数来布局child view,也就是Item View
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
if (mFastScroller != null && mItemCount != mOldItemCount) {
mFastScroller.onItemCountChanged(mOldItemCount, mItemCount);
}
// 布局Child View
layoutChildren();
mInLayout = false;
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
}
// 获取一个Item View
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
// 从缓存的Item View中获取,ListView的复用机制就在此处
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
...
child = mAdapter.getView(position, scrapView, this);
...
} else {
child = mAdapter.getView(position, null, this);
...
}
return child;
}
}
AbsListView类定义了集合视图的框架,比如Adapter模式的应用、复用Item View的逻辑、布局Item View的逻辑等。子类只需要覆写特定的方法即可实现集合视图的功能,例如ListView。
下面是ListView中相关方法
@Override
protected void layoutChildren() {
// 代码省略
try {
super.layoutChildren();
invalidate();
...
// 根据布局模式来布局Item View
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
if (newSel != null) {
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
} else {
sel = fillFromMiddle(childrenTop, childrenBottom);
}
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
mFirstPosition = 0;
sel = fillFromTop(childrenTop);
adjustViewsUpOrDown();
break;
case LAYOUT_SPECIFIC:
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
break;
case LAYOUT_MOVE_SELECTION:
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
break;
default:
...
break;
}
}
// 从上到下填充Item View [ 只是其中一种填充方式 ]
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
return selectedView;
}
// 添加Item View
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
...
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
ListView覆写了AbsListView中的layoutChilden函数,在该函数中根据布局模式来布局Item View。Item View的个数、样式都通过Adapter对应的方法来获取,获取个数、Item View之后,将这些Item View布局到ListView对应的坐标上,再加上Item View的复用机制,整个ListView就基本运转起来了。
总结
这里的Adapter并不是经典的适配器模式,但是却是对象适配器模式的优秀示例。也很好的体现了面向对象的一些基本原则。这里的Target角色和Adapter角色融合在一起,Adapter中的方法就是目标方法;而Adaptee角色就是ListView的数据集与Item View,Adapter代理数据集,从而获取到数据集的个数、元素。
通过增加Adapter层来将Item View的操作抽象起来,ListView等集合视图通过Adapter对象获得Item的个数、数据元素、Item View等,从而达到适配各种数据、各种Item视图的效果。因为Item View和数据类型千变万化,Android的架构师们将这些变化的部分交给用户来处理,通过getCount、getItem、getView等几个方法抽象出来,也就是将Item View的构造过程交给用户来处理,灵活地运用了适配器模式,达到了无限适配、拥抱变化的目的。
广告时间
我是N0tExpectErr0r,一名广东工业大学的大二学生
欢迎来到我的个人博客,所有文章均在个人博客中同步更新哦
http://blog.N0tExpectErr0r.cn