拒绝纸上谈兵--萌新开发手册(踩坑分享之封装)
废话不多说,下面开始:
说起封装,有的人觉得很简单,有的人会觉得无从下手,无论是哪种情况,想要写出比好的封装,最简单的方法就是被坑了。为什么这么说呢,因为只有当你自己被自己写的代码坑到时,你才知道这里该封装,该如何封装,坑的越痛,记忆越深刻,别问我怎么知道的,我不想说。当然,既然我在写这篇文章,目的就是希望萌新们少踩点坑,有些东西如果能够一开始就做好,岂不美哉。
上面说的都是比较虚的东西,下面我们来点干货,到底在真正的项目中,我们在那些地方需要封装?应该怎么去封装?
1.对第三方库的封装
使用了蛮多第三方库,也加了不少群,发现很多萌新(甚至是工作了近一年的准初级程序猿)在使用别人的库时,都喜欢直接照着文档就在自己的项目中到处使用,完全没有意识到这么写的隐患。如果一个库需有很多配置参数,相信大部分人还是知道,应该自己写个工具类来进行统一配置,但是当一个库的用法比较简单时,许多人就喜欢直接到处写了,这样其实会有很多问题的。
首先,耦合过强不易替换。不对第三方进行封装,最直接的影响就是不能够快速替换,以我们最常见的图片加载库来说,早几年比较流行的图片加载框架是一个叫做ImageLoader的库。试想一下,一个app中,使用图片加载的地方,少则十几处,多则几十处,如果你每个地方都引用了ImageLoader这个类,那么当你某一天需要换掉这个库的时候会发生什么?没错,你需要到处修改,虽然studio有重构快捷键和全局查找功能,可以方便的找到所有引用的地方,但是明明可以靠代码解决的问题,又何必给自己挖坑呢。
其次,不易修改。有些库,可能本身配置就比较少,刚好它的默认配置就能满足你当前的需求,这时,很多人也就直接用了。那么,你们有没有想过,后面你发现需要修改某一个属性配置时要怎么办呢?没错和上面类似的,你又要去到处该,简直就是地狱了。
那么我们该如何对第三方进行封装呢?这里只说几点我认为的基本要求,1.第三方库的所有类只能出现在自己的封装类中,项目里其他任何地方不应该对第三方库产生引用,只有这样才能做到,你想替换这个库时,只进行最少的代码修改。2.如果项目本身对一个库的配置没有太多变化的时候,尽量不要把配置过多的暴露到封装类之外,这样做可以保证封装类调用的简洁性。
下面以一个下拉刷新库为例进行简单封装
刷新库SmartRefreshLayout--https://github.com/scwang90/SmartRefreshLayout
许多萌新,基本上拿到一个库就开始照着别人的文档往项目加了,比如这样用
public class RefreshActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
//下面示例中的值等于默认值
SmartRefreshLayout refreshLayout = (SmartRefreshLayout)findViewById(R.id.refreshLayout);
refreshLayout.setPrimaryColorsId(R.color.colorPrimary, android.R.color.white);
refreshLayout.setDragRate(0.5f);//显示下拉高度/手指真实下拉高度
refreshLayout.setRefreshHeader(new ClassicsHeader(this));//设置Header
refreshLayout.setRefreshFooter(new ClassicsFooter(this));//设置Footer
refreshLayout.autoRefresh();//自动刷新
}
}
调试了一下,没什么问题,然后就开始在项目各处使用。突然有一天,产品经理发话了,这个刷新样式不好看,重新设计,然后你就一脸懵逼了,你这个设置写的到处都是,只能去一个一个改了。当然这个问题其实是个很基础的问题,基本上有个把月经验的同学都知道需要统一配置,所以也许他们会这么写:
public class RefreshActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
//下面示例中的值等于默认值
SmartRefreshLayout refreshLayout = (SmartRefreshLayout)findViewById(R.id.refreshLayout);
RefreshLayoutUtil.refreshLayoutConfig(refreshLayout ,this);
refreshLayout.autoRefresh();//自动刷新
}
}
=========================分割线==================================
public class RefreshLayoutUtil {
public static void refreshLayoutConfig (SmartRefreshLayout refreshLayout, Context context) {
refreshLayout.setPrimaryColorsId(R.color.colorPrimary, android.R.color.white);
refreshLayout.setDragRate(0.5f);//显示下拉高度/手指真实下拉高度
refreshLayout.setRefreshHeader(new ClassicsHeader(context));//设置Header
refreshLayout.setRefreshFooter(new ClassicsFooter(context));//设置Footer
}
}
这也就是我上面说的对第三库进行统一封装的一部分,我们这里采用的是对通用配置进行统一封装,好了现在产品经理告诉我,你想改几次样式?来来,随便改,皱下眉头算我输。
拥有了上面的小技巧,大家终于能轻松一点。然而,你刚躺下,被子都没捂热,突然产品经理一个电话,XX你的app刷新就崩溃了。然后你就开始debug看了,最后发现不是你的问题,是这个库本身就有bug,然后你打算上github上提给作者,一看,我去,作者竟然已经宣布停止维护了。那怎么办?有些同学会选择把库下载下来,自己试着修改,不过碍于水平有限,大部分人在尝试了一下修改然后无果后都会选择换成另一个库。
好嘛,我们现在又来换库试试,去掉库依赖,我去,竟然有几十个类和xml报错,这次是真的绝望了,看来只能删代码跑路了。。。
开个玩笑,我们肯定不可能跑路的,只能硬着头皮改。关键是改之前你要考虑,下次再出这个问题怎么办呢?当然还是通过封装来解决了:
public class RefreshLayout extends SmartRefreshLayout {
public RefreshLayout (Context context) {
super(context);
}
public RefreshLayout (Context context, AttributeSet attrs) {
super(context, attrs);
}
public RefreshLayout (Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnRefreshListener (final RefreshListener refreshListener) {
setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh (com.scwang.smartrefresh.layout.api.RefreshLayout refreshlayout) {
refreshListener.onRefresh((RefreshLayout) refreshlayout);
}
});
setOnLoadmoreListener(new OnLoadmoreListener() {
@Override
public void onLoadmore (com.scwang.smartrefresh.layout.api.RefreshLayout refreshlayout) {
refreshListener.onLoading((RefreshLayout) refreshlayout);
}
});
}
public void startRefresh () {
super.autoRefresh();
}
public void startLoadMore () {
super.autoLoadmore();
}
public interface RefreshListener {
void onRefresh (RefreshLayout refreshLayout);
void onLoading (RefreshLayout refreshLayout);
}
public void finishRefreshing () {
if (!isRefreshing()) {
return;
}
super.finishRefresh();
}
public SmartRefreshLayout finishLoadmore () {
if (!isLoading()) {
return null;
}
return super.finishLoadmore();
}
.......
省略部分配置代码
.......
}
可能有的同学一看这代码就疯了,说,这不是闲的蛋疼嘛,为什么要自己继承一次,而且还把父类的方法重新封装了一次?
其实一开始我已经说得很清楚了,对于第三库的封装就是要避免外界直接引用第三库的任何类或方法,试想一下,如果我们一开始就是这么写的,当你去掉了这个刷新库的依赖时哪里还会报错?没错,如果你遵照着上面的原则进行封装,你会发现仅仅只有这个类会报错,其他任何类和xml布局都不会报错,不报错也就说明其他地方不用做任何修改,你只需要接入你的新库,然后修改自己这个封装类,也许你的新库中刷新方法不叫autoRefresh,就叫refresh,不过没什么关系,你只需要把super.autoRefresh改成super.refresh就行了。其他的类其实依然是调用你的封装方法startRefresh,所以立即就能生效了。
上面说的是对第三方控件的封装,下面来说说对第三方工具类类型库的封装,其实原理都一样,只要具备了上面的封装思想,再来封装工具类也就举一反三了。下面以一个图片选择库为例:
https://github.com/LuckSiege/PictureSelector
public static void chooseImage(Activity activity,int maxCount){
PictureSelector.create(activity)
.openGallery(PictureMimeType.ofImage())//全部.PictureMimeType.ofAll()、图片.ofImage()、视频.ofVideo()
// .theme()//主题样式(不设置为默认样式) 也可参考demo values/styles下 例如:R.style.picture.white.style
.maxSelectNum(maxCount)// 最大图片选择数量 int
.minSelectNum(0)// 最小选择数量 int
.imageSpanCount(4)// 每行显示个数 int
.selectionMode(PictureConfig.MULTIPLE)// 多选 or 单选 PictureConfig.MULTIPLE or PictureConfig.SINGLE
.previewImage(false)// 是否可预览图片 true or false
.isCamera(false)// 是否显示拍照按钮 true or false
.isZoomAnim(true)// 图片列表点击 缩放效果 默认true
.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效
.setOutputCameraPath("/CustomPath")// 自定义拍照保存路径,可不填
.enableCrop(false)// 是否裁剪 true or false
.compress(true)// 是否压缩 true or false
.compressMode(PictureConfig.LUBAN_COMPRESS_MODE)//系统自带 or 鲁班压缩 PictureConfig.SYSTEM_COMPRESS_MODE or LUBAN_COMPRESS_MODE
// .glideOverride()// int glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度
.withAspectRatio(1,1)// int 裁剪比例 如16:9 3:2 3:4 1:1 可自定义
.hideBottomControls(true)// 是否显示uCrop工具栏,默认不显示 true or false
.isGif(true)// 是否显示gif图片 true or false
.freeStyleCropEnabled(true)// 裁剪框是否可拖拽 true or false
.circleDimmedLayer(false)// 是否圆形裁剪 true or false
.showCropFrame(true)// 是否显示裁剪矩形边框 圆形裁剪时建议设为false true or false
.showCropGrid(false)// 是否显示裁剪矩形网格 圆形裁剪时建议设为false true or false
.openClickSound(false)// 是否开启点击声音 true or false
.cropCompressQuality(80)// 裁剪压缩质量 默认90 int
.compressMaxKB(200)//压缩最大值kb compressGrade()为Luban.CUSTOM_GEAR有效 int
// .compressWH() // 压缩宽高比 compressGrade()为Luban.CUSTOM_GEAR有效 int
// .cropWH()// 裁剪宽高比,设置如果大于图片本身宽高则无效 int
.rotateEnabled(false) // 裁剪是否可旋转图片 true or false
.scaleEnabled(true)// 裁剪是否可放大缩小图片 true or false
.forResult(PictureConfig.CHOOSE_REQUEST);//结果回调onActivityResult code
}
好了,完了。这也叫封装?没错,这也叫封装了。只是相对来说,我们这种封装算是比较死的,因为各种属性都没有给出修改的方法,但是你要相信就算是你仅仅这样简单封装了一下,也绝对好过你到处去引用第三方库的类很多。原因嘛就不再重复了,上面已经说得很清楚了,这个封装虽然简陋,但是你想让他变灵活的话大可以自己修改下。对于像这种属性比较多的库建议使用builder模式进行封装,这样调用和修改都比较简单,具体封装代码这里就不再写了,有兴趣的同学可以自己尝试下。
注意:并不是所有库都一定要进行封装,有些库可能会由于某些原因没有办法进行很好的封装,比如Butter knife,rxjava。当然我这里说的不能很好的封装并不是指都完全不封装,而是指可能无法完全按照我上面说的几个原则进行封装,比如rxjava,他涉及很多类和方法,你要完全达到使用时不直接调用它的类基本上要封装很多代码,这时我们可以退而求其次,只封装一些通用配置属性。而像Butter knife这个库则基本上处于完全无法封装的状态。
说句题外话,在有其他解决方案的时候强烈不建议使用类似于Butter knife这种库。Butter knife除了能偷懒少些几个findView以外,基本上毫无卵用,但是这种库最大的问题就在于耦合很高,你的项目一旦加入了这种东西,想再去掉就很麻烦了,这也是我为什么一直不用Butter knife的原因,如果你真的很讨厌写findView,我建议你使用官方的dataBinding或者使用kotlin开发,他们都能解决findView的问题,而且效果远比Butter knife好多了。当然了,这里纯属个人见解,如果你觉得Butter knife好用的话,那就继续用吧
如何判断一个第三方库该不该封装?能不能封装?很简单,如果一个库需要的封装代码过多时,就可以考虑只简单封装一下,让外界引用几个第三方库的类也没关系。当然了,你要会风险评估,如果这个库有大概率会被替换掉的话,就要一开始就封装好。目前来说,像rxjava这种,完全封装的话,意义就不大,首先它涉及的东西太多,要封装肯定需要自己写很多东西,其次这种库基本没什么可替代的,所以对于你来说一般只会考虑用不用,如果你本来用了,现在要去掉它的话,不管你封不封装,基本上都要大面积修改代码,那花大力气封装也就是浪费时间了。
2.项目中的逻辑封装
这里来说说对项目中部分逻辑的封装,其实有了上面对第三方库的封装,那么对自己项目中什么东西该封装,大家心里应该都有点数了,还是直接举个栗子,请看如下代码:
EditText nameTV = findView(R.id.name);
EditText sexTV = findView(R.id.sex);
EditText ageTV = findView(R.id.age);
nameTV.setFocusable(false);
nameTV.setClickable(false);
sexTV.setFocusable(false);
sexTV.setClickable(false);
ageTV.setFocusable(false);
ageTV.setClickable(false);
这是一个很常见的需求,某些界面可能既是编辑页面又是详情界面,这时可能就需要我们动态改变输入框的可点击状态,上面这么写看上去似乎除了比较丑陋以外并没有太大问题,但是现在需求又双叒变了,进入编辑时我们要清空每个输入框文本并更改提示文字,好嘛,你可能不得不这么改:
EditText nameTV = findView(R.id.name);
EditText sexTV = findView(R.id.sex);
EditText ageTV = findView(R.id.age);
nameTV.setFocusable(false);
nameTV.setClickable(false);
nameTV.setText("");
nameTV.setHint("请输入");
sexTV.setFocusable(false);
sexTV.setClickable(false);
sexTV.setText("");
sexTV.setHint("请输入");
ageTV.setFocusable(false);
ageTV.setClickable(false);
ageTV.setText("");
ageTV.setHint("请输入");
有木有感觉很蛋疼?没有?那我告诉你,我这个界面其实有15个输入框,好嘛,你去改改看。那么我们换一种方法来写试试:
EditText nameTV = findView(R.id.name);
EditText sexTV = findView(R.id.sex);
EditText ageTV = findView(R.id.age);
editTextConfig(nameTV,sexTV,ageTV);
=========================分割线=======================================
private void editTextConfig (EditText... editTexts) {
if (editTexts == null || editTexts.length == 0) {
return;
}
for (EditText editText : editTexts) {
editText.setFocusable(false);
editText.setClickable(false);
editText.setText("");
editText.setHint("请输入");
}
}
这下你想加任何配置都只需要修改editTextConfig方法即可,你15个控件也罢,只需要在参数列表传入就行。可以看出,这里我们仅仅是封装一个简单的内部方法,就大大提高了代码的可维护性,相信大家看了这个例子已经明白封装有多么重要了。
好了,该讲的也差不多讲完了,项目中的封装,这个真的要靠自己去实践,我这里也只能分享一个简单的例子,希望大家能够举一反三。最后就说说我个人觉得项目中那些东西是必须封装的:
- 复杂的逻辑代码。把相同的复杂逻辑代码到处写,改的是后绝对虐哭你。
- 出现频率非常高的代码。例如对项目中的所有金额进行小数两位四舍五入,这个逻辑本身很简单,代码也只有一行的样子,当时如果你到处写的话,突然产品经理抽风要精确到3位,你怎么办?所以说,出现频率很高的代码,哪怕只有一行,也应该封装一次。
- 特殊的业务逻辑代码。某些项目中特有的业务逻辑,可能本身出现次数不是太多4到5次吧,逻辑也不复杂,20行左右吧,你是不是又想偷懒不封装?是不是觉得反正才那么几处,大不了就是复制粘贴嘛。ok,你改完上线,然后又双叒被产品经理骂了,原来这块逻辑本来出现在5个地方,结果你只改了4个,漏掉了一个地方没改。
以上的就是基本上来讲必须要封装的,不过最后一条的定义其实比较模糊,这个只能自己根据经验去判断。现在你应该清楚了,程序员封装不是仅仅为了偷懒,少些代码。不封装的话,多复制几遍都只是小问题,冰山一角而已,真正后期维护才是整个冰山。
好了,本文到这里就结束了,以上观点纯属个人看法,如有不同见解欢迎指教。后面我还会继续分享我的踩坑日记,欢迎大家持续关注
我的开源项目:MVP框架库