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

上周花了几天重写了我之前的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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,067评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,622评论 18 399
  • 你好 失败者
    绝望中的失败者阅读 210评论 0 0
  • 由老修女引领,新来的修女跟着她来到刚才办公室另一角的“病房”。门口偏右有张小床,上面躺着一个小男孩。一个银发黑衣的...
    夜影散尽沧桑阅读 429评论 2 1