Android:你還在等那個,手把手帶你重構的人出現嗎?
前言
本文斷斷續續寫了兩個禮拜。
以下你就可以看到,一位單槍匹馬的帥哥,是如何以一己之力,重構整座“屎山”的。
這位帥哥一直在徘徊,本文到底該寫給誰看?是隻在乎寫功能的碼農嗎?不了不了,碼農若真的有心提升程式碼質量,就不會在專案中喪心病狂的堆積屎山。
於是乾脆寫寫重構心得、分享重構思路,讓那些有意識在這方面有所提升的帥哥美女們,少走彎路吧!

在此首先感謝主管的信任和支援。在本次大規模重構中,這位帥哥在部門裡兜售並率先使用 —— 自主設計且在 GitHub 開源的 Viabus 架構。
ofollow,noindex">GitHub:KunMinX/android-viabus-architecture
歡迎 Star & Fork,相信你也可以像這位帥哥一樣,5 天完成 60 個類的核心模組的重構!
程式碼是如何越寫越爛的?
你是否經常聽同事自嘲說,“開始還想好好寫,不知怎滴,後面越寫越爛”。
程式碼越寫越爛,果真是個沒有端倪、無法干預的魔咒玄學嗎?
讓我們來看看重構前,專案裡的程式碼是怎麼寫的。
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 會因為過多的依賴,而越寫越臃腫:受“破窗效應”的驅使,別的碼農會因為此處已經有某個依賴,而不假思索的接著往下寫。
到底怎樣才算解耦
所謂解耦,是符合工程設計、符合設計模式原則的編碼。
解耦的本質,我只說一遍:
職責邊界明確,職責邊界明確,職責邊界明確。

viabus_flow_flow.png
符合單一職責原則:
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。相信會有一天,你也可以,高效率的編寫和重構程式碼!
