美文网首页week.io将来深入安卓开发笔记
[设计模式]记一次开源库的重构历程

[设计模式]记一次开源库的重构历程

作者: YoKey | 来源:发表于2016-10-16 22:18 被阅读3730次

上周花了几天重写了我之前的IndexableStickyListView库,重构成RecyclerView版本:IndexableRecyclerView

关键字:Wrapper(包装)模式、Adapter(适配器)模式、Observer(观察者)模式;

联系人Demo

老版本的问题


1、使用者的实体类需要extends库的IndexEntity。

Java是单继承,多实现;所以继承只有一次机会,而把这个宝贵的机会让给一个第三方库是不合适的!

2、HeaderView的局限较大,只能添加和ListView的Adapter相同布局的HeaderView,或者普通View。

应该可以添加任意布局并且可以和索引相关联的HeaderView/FooterView。

3、没有留给使用者足够的UI定制自由度,比如绑定数据时的菊花,搜索时的菊花等提示信息,库内部提供死一个固定的样式。

作为一个功能为驱动的第三方库,UI的样式应该完全交给使用者自由定制。

4、ListView跟不上时代,需要RecyclerView。

RecyclerView更优雅的设计、以及更强大的功能,迁移到RecyclerView上是应该的。

新版本解决方式


1、Wrapper模式

在老版本的问题1中,库占用了宝贵的继承机会,这种设计是不合理的;作为一个第三方库,除非必要,否则应当以implements去实现继承关系。

而使implements替代extends,我这里使用了装饰者模式(包装模式)。

装饰模式又名包装(Wrapper)模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。

装饰模式以对使用者透明的方式动态地给一个对象附加上更多的责任;装饰模式可以在不创造更多子类的情况下,将对象的功能加以扩展。

具体实现:

  • 原版本:
// 使用者继承IndexEntity:
public class CityEntity extends IndexEntity {
   private String name;
​
   @Override
   public String getName() {
       return name;
   }
​
   @Override
   public void setName(String name) {
       this.name = name;
   }
}

库的IndexEntity里包含一些拼音、首字母等属性:

public abstract class IndexEntity {
   private String firstSpell;
   private String spell;
   ...
}
  • 新版本:
// 使用者实现IndexableEntity:
public class CityEntity implements IndexableEntity {
   private String name;
​
   @Override
   public String getFieldIndexBy() {
       return name; // return 你需要根据该属性排序的field
   }
​
   @Override
   public void setFieldIndexBy(String indexByField) {
       this.name = indexByField; // 同上
   }
}

使用者传递给库的数据源只需要实现该IndexableEntity即可,库把它包装成EntityWrapper,内部数据的处理其实都是EntityWrapper。

class EntityWrapper<T> {
   private String index;
   private String pinyin;
   private T data;
  ...
}

标准的Wrapper模式同样需要实现IndexableEntity,这里并没有实现是为了兼容HeaderView/FooterView的数据源情况,所以可以认为是Wrapper模式的变种。

2、Adapter模式

在老版本的问题2中,HeaderView的局限较大,是因为老版本没有提供从数据源视图的映射。

使用Adapter,可以轻松实现这种映射关系。

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

Android中的ListView对应的ListAdapter / RecyclerView对应的Adapter就是典型的应用场景。

具体实现:

  • 原版本:
mIndexableListView.bindDatas(datas,IndexHeaderEntity headerEntity);
// 只提供数据源,而没有提供视图的定制:
IndexHeaderEntity<CityEntity> gpsHeader = new IndexHeaderEntity<>("定", "GPS自动定位", gpsIndexEntityList);
  • 新版本:
indexableLayout.addHeaderAdapter(IndexableHeaderAdapter adapter)
// Adapter:
public abstract class IndexableHeaderAdapter<T> {
   // 设置数据源
   public IndexableHeaderAdapter(String index, String indexTitle, List<T> datas) {
       ...
   }
   // ItemType,配合RecyclerView的Adapter
   public abstract int getItemViewType();
​   // 创建视图
   public abstract RecyclerView.ViewHolder onCreateContentViewHolder(ViewGroup parent);
​   // 设置视图数据
   public abstract void onBindContentViewHolder(RecyclerView.ViewHolder holder, T entity);
}

​通过HeaderAdapter,库内部经过一些处理,可以使数据源映射到使用者期望的视图。


自由定制的HeaderView

3、Observer模式

在Android的Adapter场景中,一般Adapter模式会搭配Obsever模式一起使用。因为Android中ListView/RecyclerView和Adapter是一个MVC的设计:

ListView/RecyclerView是View,Adapter是Controller,数据源是Model。

既然V与M是分离的,那么当数据有更新时,V显然无法自动更新,Adapter必须实时监控数据变化并刷新V,这里就需要用到Observer(观察者模式)。

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

具体实现:

// 被观察者:
public abstract class IndexableHeaderAdapter<T> {
   private final DataSetObservable mDataSetObservable = new DataSetObservable();
   ...
​
   public void notifyDataSetChanged() {
       mDataSetObservable.notifyChanged();
   }
​
   void registerDataSetObserver(DataSetObserver observer) {
       mDataSetObservable.registerObserver(observer);
   }
​
   void unregisterDataSetObserver(DataSetObserver observer) {
       mDataSetObservable.unregisterObserver(observer);
   }
}
// 注册观察者:
public class IndexableLayout extends FrameLayout {
   private DataSetObserver mHeaderDataSetObserver = new DataSetObserver() {

       @Override
       public void onChanged() {
           if (mRealAdapter != null) {
               mRealAdapter.notifyDataSetChanged();
           }
       }
   };

   public <T> void addHeaderAdapter(IndexableHeaderAdapter<T> adapter) {
       adapter.registerDataSetObserver(mHeaderDataSetObserver);
       ...
   }
}

一旦数据源发生变化:
1、调用Adapter的notifyDataSetChanged()
2、通知所有观察者数据发生变化:observable.notifyChanged()
3、回调所有观察者的onChanged()

这样就完成整个观察过程,上面使用的DataSetOberver,DataSetObservable类是借用了现有的Android内的类,当这些通知的类型不够时,可根据这两个类进行拓展。

观察者模式在处理一对多的依赖关系的同时,做到了优雅的解耦。

另外:

在老版本问题3中,为了给留使用者足够的UI定制自由度,也需要使用Observer模式,比如初始化数据时,库提供一个初始化结束时的回调,以便使用者自由操作UI。

mProgressBar.setVisibility(View.VISIBLE);
adapter.setDatas(mDatas, new IndexableAdapter.IndexCallback<CityEntity>() {
    @Override
    public void onFinished(List<CityEntity> datas) {
        // 数据处理完成后回调
        mProgressBar.setVisibility(View.GONE);
    }
});

总结

一个库的完成不是终点,而是一个起点。

在我们开发的过程中,可能会推翻之前的一些想法,这时库的发展方向(包括代码质量)可能就跑偏了,或者在早些开发时的设计就并不完美。

所以在库完成后,一次重构就很有必要了;这次重构会让你考虑的更全面,一些设计模式的运用也就呼之欲出。

小伙伴们,重构起来吧!在精益求精中进步~

最后,贴上IndexableRecyclerView的项目地址: GitHub

相关文章

  • [设计模式]记一次开源库的重构历程

    上周花了几天重写了我之前的IndexableStickyListView库,重构成RecyclerView版本:I...

  • oc-swift混编之oc调用swift

    前言 记一次oc项目中引用swift开源库处理过程 开源库 测试使用这个开源库 该库太老,现在swif都到4.1了...

  • android提升大法

    1、架构设计 1.1 设计模式 1.2 重构《重构改善既有的代码设计》 1.3 架构模式MVP MVC MVVM ...

  • 大话设计模式 读书笔记

    大话设计模式 book: <设计模式> <设计模式解析> <敏捷软件开发:原则, 模式与实践> <重构-改善既有代...

  • GeekBand C++设计模式 第一周

    1.设计模式简介 课程目标 松耦合设计思想 面向对象设计原则 重构技法改善设计 GOF核心设计模式 设计模式 不断...

  • 设计模式之禅 - 总结感悟

    前言:读《设计模式之禅》有感,之前对设计模式概念比较模糊,看一些开源库源码设计也只是看到些架势,通过学习后回过头细...

  • APP重构之路 Model的设计

    APP重构之路 网络请求框架 APP重构之路 Model的设计 前言 很多的app使用MVC设计模式来将“用户交互...

  • EventBus设计模式剖析(五)策略模式

    EventBus: 由开源组织greenrobot开发的事件发布-订阅总线库。 设计模式: 软件开发中问题的解决套...

  • 构造者模式

    构造者模式(Builder Pattern) 建造者模式是及其常用的一种设计模式,经常提现在一些开源的三方库中来进...

  • 四巨头23种设计模式的意图

    了解设计模式的意图,是在代码重构中浮现并识别设计模式的关键。本文将四巨头在《设计模式》一书的23种设计模式的意图放...

网友评论

  • ChineseBoy:请问如何将外部排好序的数据塞进去,里面就不用排序了,是否可以多提供个接口,毕竟提取每次都提取字母排序不太好
  • ChineseBoy:很好,很不错,在github上面给你提了个小小的bug,希望尽快修复
  • 假装我在笑:大神,怎么右侧字母导航默认不显示#号呢?,如果自己加上#号它就不能排序了
  • 一条_咸鱼_:楼主,我想问一下,我想在左边用一个checkBox表示选中的状态,而且是唯一的,但是使用了你的setOnItemContentClickListener方法就有问题。设置被点击的item为check状态,然后notifiyDataSetChanged,多点了几条之后发现设置为check状态的条目就乱了,应该是你这个recyclerview复用引起的问题吧
    YoKey:@一条_咸鱼_ 微信号:YoKeyword
    一条_咸鱼_:@YoKey 大神你有微信么,我按你说的,用SparseBooleanArray记录positon和check状态,还是有错乱的问题
    YoKey:recyclerview的复用导致的问题,你可以使用SparseBooleanArray来保存 key:下标,value:是否check状态, 在onBindContentViewHolder()里,根据SparseBooleanArray中当前position是否是check状态,重新设置CheckBox的状态即可
  • 王人冉:楼主,请问下,关于联系人加载的时候,如果是加载服务器上数据库中的数据怎么办?一般网络加载数据成功后都是在回调中onSuccess 设置 notifyDataSetChanged,但是你的代码中直接在onCreate中把数据当参数传递了(onCreate时数据还没有呢),我把mNewsAdapter.setDatas(mDataList);
    // FooterView
    indexableLayout.addFooterAdapter(new SimpleFooterAdapter<>(mNewsAdapter, "尾", "我是FooterView", initFavDatas()));写在 onSuccess回调中(网络数据加载成功后的回调),仍然出现先显示Footer(本地数据)再显示网络加载的数据,怎么改?
    YoKey:你可以在onSuccess()里绑定数据 再绑Footer~
  • JackChen1024:canvas.drawText(mIndexList.get(i), getWidth() / 2, mIndexHeight * 0.85f + mIndexHeight * i, mPaint);

    canvas.drawText的x坐标使用getWidth() / 2,好像不太准确,下面的算法可能更精确

    http://blog.csdn.net/axi295309066/article/details/52507242(看图)

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //遍历字母数组,计算坐标,进行绘制
    for (int i = 0; i < LETTERS.length; i++) {
    String letter = LETTERS[i];
    //计算x 坐标
    float x = cellWidth*0.5f - paint.measureText(letter)*0.5f;
    //计算y 坐标
    Rect bounds = new Rect();
    //获取文本的矩形区域
    paint.getTextBounds(letter, 0, letter.length(), bounds);
    float y = cellHeight*0.5f + bounds.height()+ i*cellHeight;
    //绘制文本
    canvas.drawText(letter, x, y, paint);
    }
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //控件高度
    int height = getMeasuredHeight();
    //控件宽度,也为单元格宽度
    cellWidth = getMeasuredWidth();
    //单元格宽度,控件高度除以字母数组长度,此处需要用float 类型
    //10/3 = 3.333 如果用int 接收则为3,此时高度比实际分配的高度小,所以用float 接收
    cellHeight = height*1.0f/LETTERS.length;
    }
    YoKey:@Jack1999 :+1:
  • 梦华芳秋:这个可以用!
  • 93d5b419bfba:厉害了→_→我的哥 刚好项目要用到
  • happy风仔:厉害!
  • HelloVass:youyou的板凳我来抢了

本文标题:[设计模式]记一次开源库的重构历程

本文链接:https://www.haomeiwen.com/subject/mktayttx.html