[设计模式]记一次开源库的重构历程
上周花了几天重写了我之前的IndexableStickyListView库,重构成RecyclerView版本:IndexableRecyclerView。
联系人Demo关键字:Wrapper(包装)模式、Adapter(适配器)模式、Observer(观察者)模式;
老版本的问题
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