Android:你還在等那個,手把手帶你重構的人出現嗎?

KunMinX發表於2018-10-22

前言

高能預警:本文有一點長,建議收藏後再看。

以下你就可以看到,一位單槍匹馬的帥哥,是如何以一己之力,重構整座“屎山”的。

這位帥哥一直在徘徊,本文到底該寫給誰看?是隻在乎寫功能的碼農嗎?不了不了,碼農若真的有心提升程式碼質量,就不會在專案中喪心病狂的堆積屎山。

Android:你還在等那個,手把手帶你重構的人出現嗎?

於是乾脆寫寫重構心得、分享重構思路,讓那些有意識在這方面有所提升的帥哥美女們,少走些彎路!

在此首先感謝主管的信任與支援。本次重構中,帥哥在部門內部兜售並率先使用某架構,5 天內完成 60 個類的核心模組的重構。(不要慌,架構已在 GitHub 開源,文末連結給出。)

以下正文。

程式碼是如何越寫越爛的?

你是否經常聽同事自嘲,“開始還想好好寫,不知怎滴,後面越寫越爛”?

程式碼越寫越爛,果真是個沒有端倪、無法干預的魔咒玄學嗎?

Android:你還在等那個,手把手帶你重構的人出現嗎?

讓我們來快速瀏覽一下 重構前 專案裡的程式碼是怎麼寫的。

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 個地區呢???

Android:你還在等那個,手把手帶你重構的人出現嗎?

抽象,順應的是“開閉原則”

這是一幫對“抽象”無感的碼農。

他們聽到“抽象”,就像不愛鍛鍊的我聽到父母、朋友勸我“健身”一樣被動。(笑)

正如我並不真的理解健身的意義所在,他們也當抽象是“耳邊風”。

“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();
        }
    }
}

複製程式碼

程式碼是如何剪不斷理還亂的?

聽說過“程式碼耦合”和“解耦”的人很多,但真正理解這是怎麼一回事的,恐怕只有你 ~

Android:你還在等那個,手把手帶你重構的人出現嗎?

因為哪怕你不知,你也即將見證一位帥哥如何手把手帶你解耦 ~

我們先來看下重構前的程式碼!

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 倍!

Android:你還在等那個,手把手帶你重構的人出現嗎?

軟體工程同理。

由於 UI 和業務職責邊界明確,且相互通過介面通訊,使得 UI 和業務的編寫者能夠真正的分工

寫 UI 的人,不會被業務的編寫打斷,他可以一氣呵成的寫自己的 UI。寫業務的人,同樣不會被打斷,他可以專注於業務邏輯、資料結構和演算法的優化。

寫 UI 和寫業務的人,都可以自己實現介面,去獨立的完成單元測試,完全不必依賴和等候對方的實現。

最後,在職責邊界明確的情況下,UI 就算寫 100 個 UI 邏輯,那也是 UI,業務就算寫 100 個業務,那也是業務,純種,所以不會雜亂,何況我們還可以藉助“介面隔離原則”繼續往下分工!

總結

綜上,本文介紹了兩個重構思路:
1.順應開閉原則,對定製化功能進行抽象。
2.順應單一職責、最小知識、依賴倒置原則,讓職責邊界明確,防止程式碼耦合。

看完這篇文章,如你覺得有所收穫和啟發,請不吝點贊,你的點贊就是對我最大的支援!

Android:你還在等那個,手把手帶你重構的人出現嗎?

本次專案重構用到的,符合設計模式原則的 viabus 架構,已在 GitHub 開源:
GitHub:KunMinX/android-viabus-architecture

更多文章

Android:四大架構的優缺點,你真的瞭解嗎?

Viabus – 年輕人的第一款架構

wiki – 1分鐘掌握 ViaBus 架構的使用

相關文章