接下来会在演绎一些代码重构以及架构设计上的小故事,毫不要脸的用作者自身故事改编
leobert是一位从事Android开发的IT engineer,机缘巧合之下来到了motorfans和一群有梦想的人一起奋斗。
leobert不仅仅对Android客户端开发有过硬的基础,对项目管理,产品运营,架构设计都有一定的了解,在来公司之前,leobert对motorfans做了充分的分析,并对项目实现做了一定的预测。直到leobert真正接触到代码资产。噩梦开始了。
无意于攻击项目最初始的开发者Tony,Tony copy了一部分以前项目中的框架,并设计了一部分抽象体系作为基础框架。
按照经验来说,如果公司高薪聘请了一位架构师负责架构,并将他与实际生产隔离,那么很有可能造就空中楼阁式的架构。而Tony是实际开发者,团队leader,按理来说应该不会出现这样的情况,设计的框架应该是接地气的。
然而真实情况是这样的
- Tony创建了一个庞大的助手类类,将各种可能(或者是他觉得可能)有用的代码全部放在了助手类中,并且没有unit test。
- Tony创建了一个无比复杂的activity基类,他具有这样的特性:
- 创建布局
- 支持自定义布局
- 绑定且必须绑定一个内容fragment
该fragment具有以下特性:
- 具有页面刷新功能和到底部加载功能
- 通过模板调用刷新和加载的webservice
- 通过模板创建列表等集合类型视图的子view
- 以及一些其他零碎的UI交互
除此之外,就没了!
这带来的问题就比较明显了,我们说OOP的三大基本特征:“封装”,“继承”,“多态”;从这一点来看的话,Tony是在尝试处理封装,但是Tony对封装的理解可能有点畸化,先不谈MVP、MVC、MVVM等架构概念,Tony所定义的主要角色只有这些:
- 各种自定义View控件、Dialog等
- Activity页面
- Fragment页面
- 包含各种可能重复使用到的代码段的Utility
封装的目的是:把客观事物封装成抽象的类,并且类可以把自己的数据、方法只让可信的类或者类对象操作,对不可信的进行隐藏。
但是Tony所做的工作就是在描述一系列的客观页面事物,并且并没有任何的对象“配合”概念,即根本没有真正使用到“组合”,他的Activity、Fragment几乎对一切都知晓、对一切特定领域的行为细节无所不知,他们知道DB是怎么做CURD的,他们知道WebAPI请求中各种细节对View所带来的影响,甚至可以说如果没有其他基础项目的支持,这些类还会知晓Http三次握手、断开连接等等等等。
再说继承,Tony的代码中,继承体系非常的薄,只有两代:BaseXXX 和 具体的ABCXXX
举个实际例子:
Tony定义且只定义了两个关于Fragment的抽象基类:
- BasePtrFragment,顾名思义,这是一个抽象Fragment、具有下拉、上拉的行为操作。如果具体的页面不允许存在上拉下拉操作就通过覆写方法来禁止。实际情景中直接实现它的子类并没几个。
- 继承BasePTRFragment的BasePTRListFragment,更多的都是该类的子类。
那么可以判断:Tony认为在描述人类的时候,并不需要定义:Human、Male、Female
三个类,只需要定义Human
,并且存在成员方法boolean isMale()
就可以了,当需要定义Boy
这样的类的时候,直接覆写方法就行了。我并不能说这是错的,但这是非常不合适的。当然这不是最致命的。
最致命的还在于封装,我怀疑Tony在自觉继承中不需要定义Male,Female
并不能说是错的这一点上进一步认为自己的封装已经成功了。
我们还以Human为例子,我们忽略掉医学、生物学的严谨知识系统,可能有这样一层基类XXXXAnimal
,它依赖了消化系统,传入一些食物(肉类、植物类)得到脂肪、蛋白质等产物(忽略掉一系列不知道、忘掉的知识),在Human中,我们注入的依赖(具体的消化系统)和牛类中注入的会有所区别(牛不止一个胃);哪怕忽略掉此处的物种差异,毕竟Tony认为一个Fragment都是有上拉下拉的、如果哪个类不具备该属性覆写成员方法就行。OK进一步往下思考人的消化系统也是有所差别的,成人能够接受的食物和一个月的baby是不一致的。那么是在Human中依赖一个HumanDigestiveSystem
呢还是直接描述一下消化系统的具体细节呢?
我猜这时候诸位一定毫不犹豫的选择依赖一个HumanDigestiveSystem
,毕竟已经拆解的很赤裸裸了,而且消化系统的细节太复杂了,想脱离对象直接写也太TMD开玩笑了。
而Tony呢?他在实际生产中选择的是在Human中直接写消化系统的细节,以BasePTRListFragment为例,他在其中描述了一些内容,我抽取重点:
- 模板方法-获取页面刷新接口地址,http方法行为
- 模板方法-序列化接口返回的数据、返回一个List数据集
- 模板方法-获取列表控件adapter
- webapi调用的callback实现类,解析数据见2,解析到的数据填充到adapter
- 下拉的事件回调中调用一个类似如下伪代码的方法
protected void refreshPage() {
String method = getApiInvokeMethod();
String url = getApiUrl();
Map params = getApiParams();
WebApi.request(method,url,new Callback() {
void onSuccess(byte[] res) {
boolean succsee= 解析res取出基本json结构中的返回码判断成功失败;
if(success){
List> data = parseResponse(res)
adapter.resetData(data);
.....
} else {
提示错误信息;
显示空视图
}
};
void onFailure(int httpCode,byte[] res) {
ToastUtils.httpFailure(httpCode);
onRefreshFailure();//错误视图
};
})
}
等等。
看起来这个抽象类描述了下拉刷新式列表页面的数据刷新流程。但如果他就此收手,那么一切都还是可接受的。实际情况中绝大多数页面下拉不仅仅是牵涉一个接口。
于是Tony通过覆写refreshPage方法,除了callSuper,还将额外要请求的接口细节全部写一遍。我的天、我已经不想再回忆那种代码了。
那是一个描述不了实际情况的抽象Fragment,具有3k行代码;
那是一系列重写父类的具体行为的子类,重写的内容是模板方法过程;
简而言之就是拿一个并不合适的BasePTRListFragment强行做基类
哪怕退一步说,就算是那种只有一个web接口的页面,我们真的特别需要这个抽象类吗?或许吧,有一个大约100行的抽象类描述一下过程细节也就足够了。
实在是太晚了,我也不想再去描述哪些令人懊恼的代码了,文末给出Leobert的一些经验:
- 系统最初的设计、不一定能够直接面面俱到,也不一定要面面俱到,但是重点一定要明确;----定义好消化系统类(先忽略掉食道胃小肠大肠等),表现出Human可以通过消化系统进食、消化的特征即可(同理还有运动系统、循环系统等),不要直接在Human中尝试写消化的过程细节(模板方法),这不是描述Human的重点。
- 组合优于继承,这是对上面一条的一个补充,如果一个类中的一些细节具有差异化,值得考虑是否有封装的必要;----定义健康的消化系统和有病灶的消化系统(前提是已经定义消化系统)要比:忽略消化系统类、定义消化系统健康的人和消化系统有病灶的人要好;在消化系统中依赖胃、肠等类实例,通过他们是否有病灶要比:继承消化系统类、重写类特征要好
- 不停的利用里氏代换原则思考集成体系是否合理。
考虑到本篇所介绍的项目背景,其他内容不强行往上靠。
持续更新,但有空再说