天问

  • Blog
  • 热修复与动态加载
  • python爬虫
  • 专栏
    • 热修复与动态加载
    • NDK开发
    • 设计模式与架构
    • React Native
  • 好文转载
    • 计算机技术
    • 投资理财
    • 摄影与拍照技巧
  • 投稿
  • 关于本站及隐私政策
  • Github
StevenQ
为今天工作,为明天投资,为未来孵化一些东西!
  1. 首页
  2. 计算机技术
  3. 设计模式
  4. 正文

【设计模式之】适配器模式

2019年1月5日 469点热度 1人点赞 0条评论

GoF定义

将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

应用场景

适配器模式使用最广泛的就是如下2个场景:

  • 系统需要使用一些已存在的类,但是这些类接口不符合当前系统需要,即接口不兼容
  • 需要一个统一的输出接口,而输入端类型不可预知。通过Adapter抽象将变化的部分隔离出去

对于第一个场景在现实生活中太常见了,比如笔记本的电源适配器,家用电压一般是220v的,但是笔记本的输入电压一般是5v的,所以笔记本的电源线上都会有一个电源适配器用来将原本不兼容的接口(220v)转换为当前笔记本能够使用的5v的电压。在比如手机充电线接口转换器,现在安卓手机主流的都是Type-C接口,但是苹果手机的充电线接口是Lightning Dock接口。所以如果哪一天你的苹果手机充电线坏掉了,就可以尝试使用type-c转苹果转接头将安卓充电线适配为苹果充电线。下面的代码示例模拟了这个过程:

interface IAndroidUsb {
    void connectWithAndroidUsb();
}

interface IAppleUsb {
    void connectWithAppleUsb();
}

class AndroidCharger implements IAndroidUsb {

    @Override
    public void connectWithAndroidUsb() {
        System.out.println("使用安卓型号的数据线进行连接");
    }
}

class AppleCharger implements IAppleUsb {

    @Override
    public void connectWithAppleUsb() {
        System.out.println("使用苹果型号的数据线进行连接");
    }
}

可以看到安卓型号的充电线和苹果型号充电线接口是不同的,一般情况下我们这么使用:

class XiaoMiMobile {
    private IAndroidUsb mAndroidUsb;

    public XiaoMiMobile(IAndroidUsb iAndroidUsb) {
        mAndroidUsb = iAndroidUsb;
    }

    public void charge() {
        System.out.println("=====================================");
        System.out.println("准备给小米型号手机充电");
        mAndroidUsb.connectWithAndroidUsb();
        System.out.println("开始给小米型号手机充电");
    }
}

class AppleMobile {
    private IAppleUsb mAppleUsb;

    public AppleMobile(IAppleUsb iAndroidUsb) {
        mAppleUsb = iAndroidUsb;
    }

    public void charge() {
        System.out.println("=====================================");
        System.out.println("准备给苹果型号手机充电");
        mAppleUsb.connectWithAppleUsb();
        System.out.println("开始给苹果型号手机充电");
    }
}

测试代码:

XiaoMiMobile xiaoMiMobile = new XiaoMiMobile(new AndroidCharger());
xiaoMiMobile.charge();
AppleMobile appleMobile = new AppleMobile(new AppleCharger());
appleMobile.charge();

对于小米手机我们使用安卓型号的Type-C接口充电,苹果手机使用苹果型号的Lightning Dock接口充电。程序输出如下:

=====================================
准备给小米型号手机充电
使用安卓型号的数据线进行连接
开始给小米型号手机充电
=====================================
准备给苹果型号手机充电
使用苹果型号的数据线进行连接
开始给苹果型号手机充电

符合预期,完全没问题。但是假设现在苹果型号的充电线一时找不到了或弄坏了,需要考虑使用安卓型号的充电线给苹果手机充电,但是通过前面的代码我们知道,这2者接口不兼容,苹果手机AppleMobile内部持有的是IAppleUsb接口,而安卓型号的充电线是IAndroidUsb类型。接口不兼容,就像实际生活中一个是Type-C接口,一个是Lightning Dock接口一样。完全插不上去啊。这个时候适配器就可以派上用场了。可以用一个Type-C转Lightning Dock接口转换器。如下图所示:

代码如下:

class AppleUsbTransformAdapter implements IAppleUsb {
    private IAndroidUsb mAndroidUsb;

    public AppleUsbTransformAdapter(IAndroidUsb androidUsb) {
        mAndroidUsb = androidUsb;
    }

    @Override
    public void connectWithAppleUsb() {
        //将苹果的数据线适配为安卓型号的
        mAndroidUsb.connectWithAndroidUsb();
    }
}

现在就可以使用这个适配器用安卓型号的充电线给苹果手机充电了,测试代码如下:

 AppleMobile appleMobile = new AppleMobile(new AppleUsbTransformAdapter(new AndroidCharger()));
 appleMobile.charge();

程序运行结果如下:

=====================================
准备给苹果型号手机充电
使用安卓型号的数据线进行连接
开始给苹果型号手机充电

可以看到已经成功的复用已有的安卓数据线给苹果手机充电了。

这个转换器充当了连接Type-C数据线和苹果手机之间的一个中间层,也就是说适配器一头连接着Type-C接口,另一头转为了Lightning Dock接口后连接苹果手机。因此可以据此总结出Adapter模式的特点,反映在代码上:首先Adapter需要持有一个被适配者Adaptee的引用(如Type-C型号数据线IAndroidUsb),其次还需要实现客户端所期待的Target接口(如IAppleUsb接口),这样该Adapter才属于Target类型的对象才能被目标(如苹果手机)使用。实际上这种形式属于适配器模式的对象适配器模式,还有一种是类适配器模式,这2者之间的区别在于对象适配器模式使用组合形式关联被适配接口,而类适配器模式直接继承被适配类。

个人觉得这种场景的适配器模式和软件工程领域的一句名言非常相似:任何问题都可以通过引入一个中间层来解决。通过引入一个Adapter中间层,在该中间层中实现将原来不兼容的Adaptee类型转换为客户端兼容的Target类型,同时因为该中间层持有Adaptee的引用或直接继承自Adaptee,所以Adapter中间层可以复用Adaptee的功能。

对于适配器模式真正的用武之地个人觉得不是第一个场景,而是第2个场景。因为聪明的同学可能已经意识到前面举的示例在实际生活中我们可能不会这么做,在实际生活中,如果苹果手机数据线坏了或弄丢了,我们往往是买一个新的,对应到代码层面就是重构代码。已存在的接口不兼容现有的客户端的调用,说明原先的设计在某些地方可能不合理,需要重构。

而第二个场景的应用在安卓中使用的非常广泛。具体分析参见下面的知名项目举例。

使用适配器模式的知名项目举例

最典型的莫过于安卓ListView的使用了,ListView的每一个条目视图Item View需要展示的效果在不同场合下是不同的,也就是说Item View需要显示的View类型,数量是不可预估的,因此安卓系统设计者对ListView就使用了适配器模式,ListView的Item View通过Adapter来提供。一般我们使用ListView的代码是这样的。

先定义一个Adapter

public class MyAdapter extends BaseAdapter {

    private Context mContext;
    private LinkedList<Data> mData;

    public MyAdapter() {
    }

    public MyAdapter(LinkedList<Data> mData, Context mContext) {
        this.mData = mData;
        this.mContext = mContext;
    }

    @Override
    public int getCount() {
        return mData.size();
    }

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

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null) {
            convertView = LayoutInflater.from(mContext).inflate(R.layout.item_list, parent, false);
            holder = new ViewHolder();
            holder.imgIcon = (ImageView) convertView.findViewById(R.id.img_icon);
            holder.tvContent = (TextView) convertView.findViewById(R.id.tv_content);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        holder.imgIcon.setImageResource(mData.get(position).getImgId());
        holder.tvContent.setText(mData.get(position).getContent());
        return convertView;
    }

   
    public void add(Data data) {
        if (mData == null) {
            mData = new LinkedList<>();
        }
        mData.add(data);
        notifyDataSetChanged();
    }

    
    public void add(int position,Data data){
        if (mData == null) {
            mData = new LinkedList<>();
        }
        mData.add(position, data);
        notifyDataSetChanged();
    }

    public void remove(Data data) {
        if(mData != null) {
            mData.remove(data);
        }
        notifyDataSetChanged();
    }

    public void remove(int position) {
        if(mData != null) {
            mData.remove(position);
        }
        notifyDataSetChanged();
    }

    public void clear() {
        if(mData != null) {
            mData.clear();
        }
        notifyDataSetChanged();
    }

    private class ViewHolder {
        ImageView imgIcon;
        TextView tvContent;
    }

}

然后创建该Adapter的实例,使用ListView的setAdapter方法将该Adapter设置为ListView依赖的适配器即可。这里的关键就是BaseAdapter的getView方法。该方法返回的是ListView控件每个Item视图。很明显不同的页面或者说不同的场景ListView每一行的Item视图是不一样的,但是不管怎样这些视图一定是View类型。所以这里的场景就是输入无法预知,但是输出统一。安卓系统的设计者就将这些Item视图的输入源交给用户自己去控制,让用户通过自定义BaseAdapter重写getView方法来提供千变万化的输入源,但是输出通过getView方法统一返回View类型。ListView对外提供一个依赖注入接口setAdapter来将用户定义的Adapter和自身建立关联,然后内部就能通过该Adapter的getView方法获取到用户定义的Item视图。进而将其绘制展现出来。这一点可以通过跟进ListView的相关实现来证实。

public class ListView extends AbsListView {
    @Override
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        resetList();
        mRecycler.clear();
        //将传入的adapter赋值给自己的内部成员mAdapter
        if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }
        //省略其他代码...
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;
        int childState = 0;

        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
                || heightMode == MeasureSpec.UNSPECIFIED)) {
            //这里是关键通过obtainView获取子View
            final View child = obtainView(0, mIsScrap);

            // Lay out child directly against the parent measure spec so that
            // we can obtain exected minimum width and height.
            measureScrapChild(child, 0, widthMeasureSpec, heightSize);

            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                mRecycler.addScrapView(child, 0);
            }
        }

        if (widthMode == MeasureSpec.UNSPECIFIED) {
            widthSize = mListPadding.left + mListPadding.right + childWidth +
                    getVerticalScrollbarWidth();
        } else {
            widthSize |= (childState & MEASURED_STATE_MASK);
        }

        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

        setMeasuredDimension(widthSize, heightSize);

        mWidthMeasureSpec = widthMeasureSpec;
    }
}


public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
        ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
        ViewTreeObserver.OnTouchModeChangeListener,
        RemoteViewsAdapter.RemoteAdapterConnectionCallback {

    //省略若干不相关代码...
    
    /**
     * The adapter containing the data to be displayed by this view
     */
    ListAdapter mAdapter;
    
    //省略若干不相关代码...

    /**
     * Get a view and have it show the data associated with the specified
     * position. This is called when we have already discovered that the view is
     * not available for reuse in the recycle bin. The only choices left are
     * converting an old view or making a new one.
     *
     * @param position The position to display
     * @param isScrap  Array of at least 1 boolean, the first entry will become true if
     *                 the returned view was taken from the "temporary detached" scrap heap, false if
     *                 otherwise.
     * @return A view displaying the data associated with the specified position
     */
    View obtainView(int position, boolean[] isScrap) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

        isScrap[0] = false;

        // Check whether we have a transient state view. Attempt to re-bind the
        // data and discard the view if we fail.
        final View transientView = mRecycler.getTransientStateView(position);
        if (transientView != null) {
            final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

            // If the view type hasn't changed, attempt to re-bind the data.
            if (params.viewType == mAdapter.getItemViewType(position)) {
                // 这里是关键通过mmAdapter的getView方法来获取Item视图
                final View updatedView = mAdapter.getView(position, transientView, this);

                // If we failed to re-bind the data, scrap the obtained view.
                if (updatedView != transientView) {
                    setItemViewLayoutParams(updatedView, position);
                    mRecycler.addScrapView(updatedView, position);
                }
            }

            isScrap[0] = true;

            // Finish the temporary detach started in addScrapView().
            transientView.dispatchFinishTemporaryDetach();
            return transientView;
        }

        final View scrapView = mRecycler.getScrapView(position);
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                // Failed to re-bind the data, return scrap to the heap.
                mRecycler.addScrapView(scrapView, position);
            } else {
                if (child.isTemporarilyDetached()) {
                    isScrap[0] = true;

                    // Finish the temporary detach started in addScrapView().
                    child.dispatchFinishTemporaryDetach();
                } else {
                    // we set isScrap to "true" only if the view is temporarily detached.
                    // if the view is fully detached, it is as good as a view created by the
                    // adapter
                    isScrap[0] = false;
                }

            }
        }

        if (mCacheColorHint != 0) {
            child.setDrawingCacheBackgroundColor(mCacheColorHint);
        }

        if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
        }

        setItemViewLayoutParams(child, position);

        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
            if (mAccessibilityDelegate == null) {
                mAccessibilityDelegate = new ListItemAccessibilityDelegate();
            }
            if (child.getAccessibilityDelegate() == null) {
                child.setAccessibilityDelegate(mAccessibilityDelegate);
            }
        }

        Trace.traceEnd(Trace.TRACE_TAG_VIEW);

        return child;
    }

    //省略若干不相关代码...
}

从上面的源码可以看到,ListView继承自AbsListView,所以也继承了其内部成员mAdapter和obtainView方法,当我们通过setAdapter方法设置自定义的Adapter时会将该该Adapter对象赋值给mAdapter成员。在ListView的onMeasure视图绘制函数中会通过obtainView方法来获取当前位置的Item视图,而obtainView内部实际上就是通过调用mAdapter的getView方法来得到用户在xml文件指定的Item视图的内容和相关数据。

从上面的示例可以看到这种场景的关键如下:

  • 用户的输入源无法预知或类型千变万化无法穷尽,但是用户的输出(也即框架的某个输入)可以用某一个统一类型类T表示
  • 定义一个接口或者抽象类IAdapter,在该接口中定义一个方法method返回上述类型T
  • 需要使用类型T的类中关联IAdapter接口,同时提供一个依赖注入接口让用户可以设置IAdapter的具体实现
  • 在需要用到类型T的地方,调用IAdapter接口的method方法得到类型T。

这样通过IAdapter这个适配层将变化的部分隔离出去,每个输入源不同的用户只需要实现IAdapter接口,重写其method方法即可实现不同输入源但输出相同类型T的效果。个人觉得这里的Adapter模式和策略模式有些类似,不同的IAdapter的具体实现就类似不同的策略。

适配器模式的好处

  • 对于第一个场景,此时适配器模式实际上属于事后修补,因此无好处而言,其作用是将原来设计的类适配为能兼容当前需求的接口
  • 对于第二个场景,其好处就是隔离变化,增强系统的可扩展性。将变化部分或难以预估部分和确定的部分分离开来,具体来说就是将变化的部分通过Adapter层隔离出去,框架层只依赖Adapter抽象,在Adapter抽象中会定义一个方法返回我们需要的那个确定的类型,用户可以随意实现Adapter中的逻辑,但是最终都会返回上述的确定类型的对象。就像安卓系统的ListView的Item一样,每个Item的实现可以多种多样,但是最终都可以概括为View类型。
本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 设计模式
最后更新:2019年1月5日

天问

追梦追自由,穷则独善其身,达则兼济天下

打赏 点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2022 天问 ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang