Android:你还在等那个,手把手带你重构的人出现吗?
前言
本文的编写,前前后后筹备了两个礼拜。
以下你就可以看到,一位单枪匹马的帅哥,是如何以一己之力,重构整座“屎山”的。
这位帅哥一直在徘徊,本文到底该写给谁看?是只在乎写功能的码农吗?不了不了,码农若真的有心提升代码质量,就不会在项目中丧心病狂的堆积屎山。
于是干脆写写重构心得、分享重构思路,让那些有意识在这方面有所提升的帅哥美女们,少走弯路吧!
在此首先感谢主管的信任与支持。本次重构中,帅哥在部门内部兜售并率先使用自主设计的架构,5 天内完成 60 个类的核心模块的重构。(不要慌,架构已在 GitHub 开源,文末链接给出。)
以下正文。
代码是如何越写越烂的?
你是否经常听同事自嘲,“开始还想好好写,不知怎滴,后面越写越烂”?
代码越写越烂,果真是个没有端倪、无法干预的魔咒玄学吗?
让我们来快速浏览一下 重构前 项目里的代码是怎么写的。
protected void initView() { PagerAdapter pagerAdapter = new PagerAdapter(); viewPagerFix.setOffscreenPageLimit(4); viewPagerFix.setAdapter(pagerAdapter); mFragmentBinding.tabLayout.setTabData(pagerAdapter.titles); mFragmentBinding.tabLayout.setOnTabSelectListener(new OnTabSelectListener() { @Override public void onTabSelect(int position) { viewPagerFix.setCurrentItem(position); } @Override public void onTabReselect(int position) { } }); viewPagerFix.addOnPageChangeListener(new ViewPagerFix.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { KeyboardUtils.hideSoftInput(getActivity()); } @Override public void onPageSelected(int position) { mFragmentBinding.tabLayout.setCurrentTab(position); if (mViewModel.getXXXDetailTouchManager().isZZBG()) { zzbgPageSelected(position); } else if (mViewModel.getXXXDetailTouchManager().isYBJZ()) { switch (position) { case 0: case 1: mViewModel.removeAllArrows(); if (mAttachmentFragment != null) { mAttachmentFragment.hideClickHighLight(ALBUM_ALL); } break; case 2: if (mAttachmentFragment != null) { mAttachmentFragment.initAttachTitle(); } mViewModel.showAllArrows(); break; default: break; } } else { switch (position) { case 0: case 1: case 2: mViewModel.removeAllArrows(); //hideBottomLayout(); if (mAttachmentFragment != null) { mAttachmentFragment.hideClickHighLight(ALBUM_ALL); } break; case 3: if (mAttachmentFragment != null) { mAttachmentFragment.initAttachTitle(); } mViewModel.showAllArrows(); break; default: break; } } } @Override public void onPageScrollStateChanged(int state) { } }); viewPagerFix.setCurrentItem(0); mFragmentBinding.headContainer.getTitleView().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mViewModel.getXXXDetailTouchManager().isZZBG()) { return; } mViewModel.changeWyhcrwMajorState(); EventBus.getDefault().post(new RefreshItemEventBus( mViewModel.getXXXDetailTouchManager().getCurrentWyhcrw())); } }); } private void zzbgPageSelected(int position) { if (mScreenNum == 3) { switch (position) { case 0: case 1: mViewModel.removeAllArrows(); if (mAttachmentFragment != null) { mAttachmentFragment.hideClickHighLight(ALBUM_ALL); } break; case 2: mViewModel.showAllArrows(); break; default: break; } } else { switch (position) { case 0: mViewModel.removeAllArrows(); if (mAttachmentFragment != null) { mAttachmentFragment.hideClickHighLight(ALBUM_ALL); } break; case 1: mViewModel.showAllArrows(); break; default: break; } } ; } /** * viewPager适配器 */ private class PagerAdapter extends FragmentPagerAdapter { String[] titles; PagerAdapter() { super(getChildFragmentManager()); if (mViewModel.getXXXDetailTouchManager().isZZBG()) { if (mScreenNum == 3) { titles = getResources().getStringArray(R.array.XXX_detail_tabs_for_no_tbjt); } else { titles = getResources().getStringArray(R.array.XXX_detail_tabs_for_zzbg); } } else if (mViewModel.getXXXDetailTouchManager().isYBJZ()) { titles = getResources().getStringArray(R.array.XXX_detail_tabs_for_ybjz); } else { titles = getResources().getStringArray(R.array.XXX_detail_tabs); } } @Override public Fragment getItem(int position) { if (mViewModel.getXXXDetailTouchManager().isZZBG()) { return zzbgGetItem(position); } else if (mViewModel.getXXXDetailTouchManager().isYBJZ()) { switch (position) { case 0: if (mXXXTuBanPicFragment == null) { mXXXTuBanPicFragment = XXXTuBanPicFragment.newInstance( mViewModel.getUniqueCode(), mViewModel.getXXXTouchManger() ); } return mXXXTuBanPicFragment; case 1: if (mRecordFragment == null) { mRecordFragment = XXXRecordFragment.newInstance(mViewModel.getXXXDetailTouchManager()); } return mRecordFragment; default: if (mAttachmentFragment == null) { mAttachmentFragment = XXXAttachmentFragment.newInstance( mViewModel.getAttachments(), mViewModel.getOriginalAttachments(), mViewModel.getUniqueCode(), mViewModel.getXXXTouchManger(), XXXDetailFragment.this ); } return mAttachmentFragment; } } else { switch (position) { case 0: if (mXXXTuBanPicFragment == null) { mXXXTuBanPicFragment = XXXTuBanPicFragment.newInstance( mViewModel.getUniqueCode(), mViewModel.getXXXTouchManger() ); } return mXXXTuBanPicFragment; case 1: if (mAttributeFragment == null) { mAttributeFragment = XXXAttributeFragment.newInstance( mViewModel.getUniqueCode(), mViewModel.getXXXTouchManger() ); } return mAttributeFragment; case 2: if (mRecordFragment == null) { mRecordFragment = XXXRecordFragment.newInstance(mViewModel.getXXXDetailTouchManager()); } return mRecordFragment; default: if (mAttachmentFragment == null) { mAttachmentFragment = XXXAttachmentFragment.newInstance( mViewModel.getAttachments(), mViewModel.getOriginalAttachments(), mViewModel.getUniqueCode(), mViewModel.getXXXTouchManger(), XXXDetailFragment.this ); } return mAttachmentFragment; } } } private Fragment zzbgGetItem(int position) { if (mScreenNum == 3) { switch (position) { case 0: if (mAttributeFragment == null) { mAttributeFragment = XXXAttributeFragment.newInstance( mViewModel.getUniqueCode(), mViewModel.getXXXTouchManger() ); } return mAttributeFragment; case 1: if (mRecordFragment == null) { mRecordFragment = XXXRecordFragment.newInstance( mViewModel.getXXXDetailTouchManager()); } return mRecordFragment; default: if (mAttachmentFragment == null) { mAttachmentFragment = XXXAttachmentFragment.newInstance( mViewModel.getAttachments(), mViewModel.getOriginalAttachments(), mViewModel.getUniqueCode(), mViewModel.getXXXTouchManger(), XXXDetailFragment.this ); } return mAttachmentFragment; } } else { switch (position) { case 0: if (mRecordFragment == null) { mRecordFragment = XXXRecordFragment.newInstance( mViewModel.getXXXDetailTouchManager()); } return mRecordFragment; default: if (mAttachmentFragment == null) { mAttachmentFragment = XXXAttachmentFragment.newInstance( mViewModel.getAttachments(), mViewModel.getOriginalAttachments(), mViewModel.getUniqueCode(), mViewModel.getXXXTouchManger(), XXXDetailFragment.this ); } return mAttachmentFragment; } } } @Override public Object instantiateItem(ViewGroup container, int position) { Object object = super.instantiateItem(container, position); if (mViewModel.getXXXDetailTouchManager().isZZBG()) { if (mScreenNum == 3) { switch (position) { case 0: mAttributeFragment = (XXXAttributeFragment) object; break; case 1: mRecordFragment = (XXXRecordFragment) object; break; default: mAttachmentFragment = (XXXAttachmentFragment) object; break; } } else { switch (position) { case 0: mRecordFragment = (XXXRecordFragment) object; break; default: mAttachmentFragment = (XXXAttachmentFragment) object; break; } } return object; } else if (mViewModel.getXXXDetailTouchManager().isYBJZ()) { switch (position) { case 0: mXXXTuBanPicFragment = (XXXTuBanPicFragment) object; break; case 1: mRecordFragment = (XXXRecordFragment) object; break; default: mAttachmentFragment = (XXXAttachmentFragment) object; break; } return object; } else { switch (position) { case 0: mXXXTuBanPicFragment = (XXXTuBanPicFragment) object; break; case 1: mAttributeFragment = (XXXAttributeFragment) object; break; case 2: mRecordFragment = (XXXRecordFragment) object; break; default: mAttachmentFragment = (XXXAttachmentFragment) object; break; } return object; } } @Override public int getCount() { if (mViewModel != null) { if (mViewModel.getXXXDetailTouchManager().isZZBG()) { if (mScreenNum == 3) { return 3; } return 2; } if (mViewModel.getXXXDetailTouchManager().isYBJZ()) { return 3; } else { return 4; } } return 0; } }
(为保护隐私,模块类名已替换为“XXX”)
可以看到,该主页目前服务于 3 个地区,每个地区对子页面的展示都有定制需求。
if else switch if else switch,只在乎功能实现的码农就是这么写的。
一个地区 50 行,那要是 10 个地区呢?公司领导放话要支持全国 100 个乡镇地区!那 100 个地区呢???
抽象,顺应的是“开闭原则”
这是一帮对“抽象”无感的码农。
他们听到“抽象”,就像不爱锻炼的人听到父母、朋友劝其“健身”一样被动。
正如他们不理解健身的意义所在,他们也当抽象是“耳边风”。
“100 个地区”这种,天然的就是用工厂模式来抽象和定制,这原本是一目了然、毫无疑问的事。
重构后的代码,主页抬头特意标注了警告。
/ * 友情提示:本类涂有防腐药品,切勿触碰,切勿触碰,切勿触碰! * <p> * 地区定制功能,包括特色的布局等,请继承于 AbstractDetailChildFragmentManager 单独编写! */ public class XXXDetailFragment extends BaseFragment implements IResponse { protected void initView() { initViewPagerManager(); PagerAdapter pagerAdapter = new PagerAdapter(); viewPagerFix.setOffscreenPageLimit(4); viewPagerFix.setAdapter(pagerAdapter); mFragmentBinding.tabLayout.setTabData(pagerAdapter.titles); mFragmentBinding.tabLayout.setOnTabSelectListener(new OnTabSelectListener() { @Override public void onTabSelect(int position) { viewPagerFix.setCurrentItem(position); } @Override public void onTabReselect(int position) { } }); viewPagerFix.addOnPageChangeListener(new ViewPagerFix.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { KeyboardUtils.hideSoftInput(getActivity()); } @Override public void onPageSelected(int position) { mFragmentBinding.tabLayout.setCurrentTab(position); mDetailChildFragmentManager.onPageSelected(position); } @Override public void onPageScrollStateChanged(int state) { } }); } /** * viewPager适配器 */ private class PagerAdapter extends FragmentPagerAdapter { String[] titles; PagerAdapter() { super(getChildFragmentManager()); titles = mDetailChildFragmentManager.getTitles(); } @Override public Fragment getItem(int position) { return mDetailChildFragmentManager.getItem(position); } @Override public Object instantiateItem(ViewGroup container, int position) { Object object = super.instantiateItem(container, position); return mDetailChildFragmentManager.instantiateItem(container, position, object); } @Override public int getCount() { return mDetailChildFragmentManager.getCount(); } } }
代码是如何剪不断理还乱的?
听说过“代码耦合”和“解耦”的人很多,但真正理解这是怎么一回事的,恐怕只有你 ~
因为哪怕你不知,你也即将见证一位帅哥如何手把手带你解耦 ~
我们先来看下重构前的代码!
public interface XXXListNavigator { void updateRecyclerView(); void showProgressDialog(); void dismissProgressDialog(); void updateListView(); void updateLayerWrapperList(List<LayerWrapper> list); boolean isAnimationFinish(); void resetCount(); } public class XXXListViewModel extends BaseViewModel { public void multiAddOrRemove(ArrayList<String> bsms, boolean isAdd) { if (null != mNavigator) { mNavigator.showProgressDialog(); } if (null == mMultiAddOrRemoveUseCase) { mMultiAddOrRemoveUseCase = new MultiAddOrRemoveUseCase(); } mUseCaseHandler.execute(mMultiAddOrRemoveUseCase, new MultiAddOrRemoveUseCase.RequestValues(isAdd, bsms, mLayerWrapperObservableField.get()), new UseCase.UseCaseCallback<MultiAddOrRemoveUseCase.ResponseValue>() { @Override public void onSuccess(MultiAddOrRemoveUseCase.ResponseValue response) { ToastUtils.showShort(getApplicationContext(), "操作成功"); clearData(); loadData(true, true); if (null != mNavigator) { mNavigator.dismissProgressDialog(); } } @Override public void onError() { ToastUtils.showShort(getApplicationContext(), "操作失败"); if (null != mNavigator) { mNavigator.dismissProgressDialog(); } } }); } }
可以看到,UI 过度暴露了“处理 UI 逻辑所依赖的过程 API”,并在业务中直接干预了 UI 逻辑,这是典型的 MVP 写法,这造成了耦合。一旦 UI 的需求有变动,View 和 Presenter 的编写者都会受到牵连。
而且,职责过多造成了依赖过多,这个 Presenter 会因为过多的依赖,而越写越臃肿:受“破窗效应”的驱使,别的码农会因为此处已经有某个依赖,而不假思索的接着往下写。
到底怎样才算解耦
所谓解耦,是符合工程设计、符合设计模式原则的编码。
解耦的本质,我只说一遍:
职责边界明确,职责边界明确,职责边界明确。
符合单一职责原则:
UI 的职责仅限于“展示”,也就是发送请求、处理 UI 逻辑。业务的职责仅限于“提供数据”,也就是接收请求、处理业务逻辑、响应结果数据。
符合依赖倒置原则、最小知识原则:
UI 不需要知道数据是经过怎样的周转得来的,它只需发送请求,并在拿到结果数据后,自己内部消化 UI 逻辑。业务只需处理数据并响应数据给 UI,它不需要知道 UI 会怎样使用数据,更无权干预。
综上,无论是 UI 还是业务,都不应过度暴露内部逻辑 API 而受控于人,它们应只暴露请求 API,来响应外部的请求。过程逻辑应只在自己内部独立消化。
public class XXXListBusinessProxy extends BaseBusiness<XXXBus> implements IXXXListFragmentRequest { @Override public void multiAddOrRemove(final XXXListDTO dto) { handleRequest((e) -> { ... if (TextUtils.isEmpty(existBsms)) { sendMessage(e, new Result(XXXDataResultCode.XXX_LIST_FRAGMENT_MULTI_ADD_OR_REMOVE, false)); } else { wyhcJgDBManager.insertAllTaskOfMine(existBsms, layersConfig); sendMessage(e, new Result(XXXDataResultCode.XXX_LIST_FRAGMENT_MULTI_ADD_OR_REMOVE, true)); } return null; }); } @Override public void refreshPatternOfXXXList(final XXXListDTO dto) { handleRequest((e) -> { ... count.setMyXXXCount(wyhcJgDBManager.getMyXXXPatternCount()); return new Result(XXXDataResultCode.XXX_LIST_FRAGMENT_REFRESH_COUNT, count); }); } @Override public void changeXXXPatternOfMine(final XXXListDTO dto) { handleRequest((e) -> { if (toMine) { ... } else { ... sendMessage(e, new Result(XXXDataResultCode.XXX_LIST_FRAGMENT_GET_ALL_PATTERN_OF_MINE, count)); } return null; }); } } public class XXXListFragment extends BaseFragment implements IResponse { XXXBus.XXX().queryList(mDto); XXXBus.XXX().multiAddOrRemove(mDto); XXXBus.XXX().queryPattern(mDto); ... @Override public void onResult(Result testResult) { String code = (String) testResult.getResultCode(); switch (code) { case XXXDataResultCode.XXX_LIST_FRAGMENT_REFRESH_LIST: updateRecyclerView((List<Wyhcrw>) testResult.getResultObject()); if (isNeedUpdateCount()) { ... } else { finishLoading(); } break; case XXXDataResultCode.XXX_LIST_FRAGMENT_MULTI_ADD_OR_REMOVE: if ((boolean) testResult.getResultObject()) { loadData(true, true); } else { ToastUtils.showShort(getContext(), "操作失败"); } dismissProgressDialog(); break; case XXXDataResultCode.XXX_LIST_FRAGMENT_REFRESH_PATTERN: ... break; default: } } }
解耦有什么好处?
解耦的好处,福特最有话语权。
100 多年前,福特发明了世界上第一条流水线,让工人职责边界明确,从而得以分工和专注各自领域。
原先装配一辆车需 700 小时,通过流水线分工后,平均一辆 12.5 小时,这使得生产效率提升了近 60 倍!
软件工程同理。
由于 UI 和业务职责边界明确,且相互通过接口通信,使得 UI 和业务的编写者能够真正的分工。
写 UI 的人,不会被业务的编写打断,他可以一气呵成的写自己的 UI。写业务的人,同样不会被打断,他可以专注于业务逻辑、数据结构和算法的优化。
写 UI 和写业务的人,都可以自己实现接口,去独立的完成单元测试,完全不必依赖和等候对方的实现。
最后,在职责边界明确的情况下,UI 就算写 100 个 UI 逻辑,那也是 UI,业务就算写 100 个业务,那也是业务,纯种,所以不会杂乱,何况我们还可以借助“接口隔离原则”继续往下分工!
...
总结
综上,本文介绍了两个重构思路:
1.顺应开闭原则,对定制化功能进行抽象。
2.顺应单一职责、最小知识、依赖倒置原则,让职责边界明确,防止代码耦合。
本次项目重构用到的,符合设计模式原则的 viabus 架构,已在 GitHub 开源。
GitHub:KunMinX/android-viabus-architecture
欢迎 Star & Fork。相信会有一天,你也可以,高效率的编写和重构代码!
更多文章
Android:四大架构的优缺点,你真的了解吗?
Viabus - 年轻人的第一款架构
wiki - 1分钟掌握 ViaBus 架构的使用
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
这些深度学习术语,你了解多少?(上)
对于一个新手来说,深度学习术语可能非常难以理解。本表试图解释深度学习常用术语并链接到原始参考,以帮助读者深入了解特定主题。 深度学习与“一般”的机器学习术语之间的界限非常模糊。例如,我这里不包括“交叉验证”,因为它是一种通用技术,用于整个机器学习。但是,我加入了softmax或word2vec等术语,因为它们通常与深度学习相关,即使它们不是深度学习技术。 激活函数 为了让神经网络学习复杂的决策边界,我们将非线性激活函数应用于其某些层。常用的函数包括sigmoid、tanh、ReLU(整流线性单元)及它们的变体。 Adadelta Adadelta是一种基于梯度下降的学习算法,可以随时间调整每个参数的学习速率。它被认为是对Adagrad的改进,因为Adagrad对超参数很敏感,并且可能过于快速的降低学习速度。Adadelta类似于rmspro
- 下一篇
WPF:WebBrowser提示 为帮助保护你的安全,您的Web浏览器已经限制此文件显示可能访问您的计算机的活动内容
原文: WPF:WebBrowser提示 为帮助保护你的安全,您的Web浏览器已经限制此文件显示可能访问您的计算机的活动内容 版权声明:本文为博主原创文章,未经博主允许可以随意转载 https://blog.csdn.net/songqingwei1988/article/details/50427159 RT,近日使用百度地图API,需要在本地做一个html文件承载,加载本地文件时出现该异常,百度了一下,搜到一个适用于IIS的方案 1.将文件放入IIS,使用网络路径,即将下面的路径改为IIS地址 ChooseBrowser.Navigate(new Uri(Environment.CurrentDirectory + "/Map/CreatMap.html", UriKind.Absolute)); 可惜我是CS架构软件,使用不了啊... 后来看网上有很多解决方案,这个最有效. 将html文件设置属性设置为Resource, 调用代码改为: Uri uri = new Uri(@"pack://application:,,,/Map/CreatMap.html"); Stream ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS7设置SWAP分区,小内存服务器的救世主