移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)-MVP重構(gòu)示例篇

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(13)-??????一鏡到底访雪!MVP重構(gòu)示例篇

前言

上一篇移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(12)- 編譯調(diào)試篇 介紹敛纲,經(jīng)過(guò)了解耦糜值、分庫(kù)及編譯調(diào)試的優(yōu)化邓夕,一段時(shí)間內(nèi)括饶,CloudDisk的開發(fā)效率得到了很大的提升冯乘。

但隨著業(yè)務(wù)的演進(jìn)入录,由于歷史的原因蛤奥,代碼中存在很多Activity及Controller的上帝類聋丝。今天我們將拿File Bundle作為例子扣蜻,為大家總結(jié)重構(gòu)的流程颈嚼,演示如何一步一步將上帝類重構(gòu)為MVP架構(gòu)环鲤。

視頻演示地址: mp.weixin.qq.com/s/zjeln_eqA…

重構(gòu)流程

1. 梳理業(yè)務(wù)邏輯

通過(guò)往往這一步是最難的。由于人員更迭碳柱、產(chǎn)品的迭代藤滥,給這一步帶來(lái)很大的挑戰(zhàn)梗搅。我們可以嘗試從一下幾方面來(lái)補(bǔ)全信息。

  1. 找人:產(chǎn)品經(jīng)理衰猛、設(shè)計(jì)人員迟蜜、測(cè)試人員進(jìn)行確認(rèn)和答疑
  2. 找文檔:查看原有的需求文檔、設(shè)計(jì)文檔啡省、測(cè)試用例娜睛、設(shè)計(jì)稿
  3. 看代碼:從原有的代碼設(shè)計(jì)中去梳理業(yè)務(wù)

經(jīng)過(guò)梳理確認(rèn),F(xiàn)ile Bundle的現(xiàn)有的業(yè)務(wù)如下:

文件大小轉(zhuǎn)換為以B冕杠、K微姊、M酸茴、G單位顯示

2.分析原有的代碼設(shè)計(jì)

我們以主要上帝類FileFragment為例分预,代碼如下:

@AndroidEntryPoint
public class FileFragment extends Fragment {

    @Inject
    FileController fileController;

    private RecyclerView fileListRecycleView;
    private TextView tvMessage;

    public static FileFragment newInstance() {
        FileFragment fragment = new FileFragment();
        Bundle args = new Bundle();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.fragment_file, container, false);
        fileListRecycleView = view.findViewById(R.id.file_list);
        tvMessage = view.findViewById(R.id.tv_message);
        tvMessage.setOnClickListener(v -> getFileList());
        return view;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getFileList();
    }

    private void getFileList() {
        new Thread(() -> {
            Message message = new Message();
            try {
                List<FileInfo> infoList = fileController.getFileList();
                message.what = 1;
                message.obj = infoList;
            } catch (NetworkErrorException e) {
                message.what = 0;
                message.obj = "NetworkErrorException";
                e.printStackTrace();
            }
            mHandler.sendMessage(message);
        }).start();
    }

    Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            if (msg.what == 1) {
                showTip(false);
                //顯示網(wǎng)絡(luò)數(shù)據(jù)
                List<FileInfo> infoList = (List<FileInfo>) msg.obj;
                FileListAdapter fileListAdapter = new FileListAdapter(infoList, getActivity());
                fileListRecycleView.addItemDecoration(new DividerItemDecoration(
                        getActivity(), DividerItemDecoration.VERTICAL));
                //設(shè)置布局顯示格式
                fileListRecycleView.setLayoutManager(new LinearLayoutManager(getActivity()));
                fileListRecycleView.setAdapter(fileListAdapter);
            } else if (msg.what == 0) {
                showTip(true);
                //顯示異常提醒數(shù)據(jù)
                tvMessage.setText(msg.obj.toString());
            } else {
                showTip(true);
                //顯示空數(shù)據(jù)
                tvMessage.setText("empty data");
            }
            return false;
        }
    });

    public void showTip(boolean show) {
        if (show) {
            tvMessage.setVisibility(View.VISIBLE);
            fileListRecycleView.setVisibility(View.GONE);
        } else {
            tvMessage.setVisibility(View.GONE);
            fileListRecycleView.setVisibility(View.VISIBLE);
        }
    }
}
復(fù)制代碼

從代碼中我們可以看到主要的一些設(shè)計(jì)問(wèn)題,如下:

  1. 主要的獲取文件薪捍、異常邏輯判斷笼痹、界面刷新控制都是在一個(gè)類里面,不利于后續(xù)的擴(kuò)張及修改維護(hù)酪穿。我們希望類的職責(zé)更加單一凳干,邏輯和視圖能夠進(jìn)行分離。
  2. 存在粗暴的new Thread進(jìn)行管理
  3. Handler 存在內(nèi)存泄露風(fēng)險(xiǎn)
  4. 存在規(guī)范問(wèn)題被济,例如empty data字符串沒(méi)有使用xml進(jìn)行管理救赐、代碼中由無(wú)效的導(dǎo)包等
  5. 代碼中沒(méi)有任何守護(hù)測(cè)試

完整的所有代碼見(jiàn)Github

3. 補(bǔ)充守護(hù)測(cè)試

參考重構(gòu)篇中,我們制定的策略只磷,我們可以先做大型的測(cè)試经磅,做為守護(hù)測(cè)試。

@RunWith(AndroidJUnit4.class)
@LargeTest
@HiltAndroidTest
@Config(application = HiltTestApplication.class)
public class FileFragmentTest {

    @Rule
    public HiltAndroidRule hiltRule = new HiltAndroidRule(this);

    @Test
    public void show_show_file_list_when_get_success() {
        //given
        ActivityScenario<DebugActivity> scenario = ActivityScenario.launch(DebugActivity.class);
        scenario.onActivity(activity -> {
            //then
            onView(withText("遺留代碼重構(gòu).pdf")).check(matches(isDisplayed()));
            onView(withText("100.00K")).check(matches(isDisplayed()));
            onView(withText("系統(tǒng)組件化.pdf")).check(matches(isDisplayed()));
            onView(withText("9.67K")).check(matches(isDisplayed()));
        });
    }
}
復(fù)制代碼

我們發(fā)現(xiàn)這個(gè)用不會(huì)通過(guò)钮追,我們主要面臨一下2個(gè)問(wèn)題预厌。

  1. 網(wǎng)絡(luò)請(qǐng)求是異步的,異步邏輯可能在測(cè)試執(zhí)行完還沒(méi)有觸發(fā)到
  2. 網(wǎng)絡(luò)數(shù)據(jù)是動(dòng)態(tài)的元媚,我們斷言的數(shù)據(jù)沒(méi)辦法確定

所以我們采用的做法就是Mock轧叽,我不穩(wěn)定的依賴Mock掉。我們同樣使用Shadow來(lái)進(jìn)行多獲取網(wǎng)絡(luò)數(shù)據(jù)方法進(jìn)行Mock刊棕,代碼如下:

@Implements(FileFragment.class)
public class ShadowFileFragment {

    @RealObject
    public FileFragment fileFragment;

    enum State {
        SUCCESS,
        ERROR,
        EMPTY
    }

    public static State state = State.SUCCESS;

    @Implementation
    protected void getFileList() {
        System.out.println("shadow .... .....");
        Message message = new Message();
        if (state == State.SUCCESS) {
            ArrayList<FileInfo> infoList = new ArrayList<>();
            infoList.add(new FileInfo("遺留代碼重構(gòu).pdf", 102400));
            infoList.add(new FileInfo("系統(tǒng)組件化.pdf", 9900));
            message.what = 1;
            message.obj = infoList;
        } else if (state == State.ERROR) {
            message.what = 0;
            message.obj = "NetworkErrorException";
        } else if (state == State.EMPTY) {
            message.what = 1;
            message.obj = null;
        }
        fileFragment.mHandler.sendMessage(message);
    }
}
復(fù)制代碼

調(diào)整后的測(cè)試用例如下:

 @Test
    public void show_show_file_list_when_get_success() {
        //given
        ShadowFileFragment.state = ShadowFileFragment.State.SUCCESS;
        //when
        ActivityScenario<DebugActivity> scenario = ActivityScenario.launch(DebugActivity.class);
        scenario.onActivity(activity -> {
            //then
            onView(withText("遺留代碼重構(gòu).pdf")).check(matches(isDisplayed()));
            onView(withText("100.00K")).check(matches(isDisplayed()));
            onView(withText("系統(tǒng)組件化.pdf")).check(matches(isDisplayed()));
            onView(withText("9.67K")).check(matches(isDisplayed()));
        });
    }

    @Test
    public void show_show_error_tip_when_net_work_exception() {
        //given
        ShadowFileFragment.state = ShadowFileFragment.State.ERROR;
        //when
        ActivityScenario<DebugActivity> scenario = ActivityScenario.launch(DebugActivity.class);
        scenario.onActivity(activity -> {
            //then
            onView(withText("NetworkErrorException")).check(matches(isDisplayed()));
        });
    }

    @Test
    public void show_show_empty_tip_when_not_has_data() {
        //given
        ShadowFileFragment.state = ShadowFileFragment.State.EMPTY;
        //when
        ActivityScenario<DebugActivity> scenario = ActivityScenario.launch(DebugActivity.class);
        scenario.onActivity(activity -> {
            //then
            onView(withText("empty data")).check(matches(isDisplayed()));
        });
    }
復(fù)制代碼

中間我們用允許測(cè)試腳步時(shí)發(fā)現(xiàn)炭晒,之前的舊代碼還有一處邏輯的錯(cuò)誤。數(shù)據(jù)為空的判斷應(yīng)該加在網(wǎng)絡(luò)數(shù)據(jù)回調(diào)中甥角。

Caused by: java.lang.NullPointerException
    at com.cloud.disk.bundle.file.FileFragment$1.handleMessage(FileFragment.java:96)
    at android.os.Handler.dispatchMessage(Handler.java:102)
復(fù)制代碼

我們同步將異常邏輯進(jìn)行修改网严,如下:

image

最后我們完成了基本的守護(hù)測(cè)試,運(yùn)行結(jié)果如下:

image

完整的所有代碼見(jiàn)Github

4. 簡(jiǎn)單設(shè)計(jì)

MVP架構(gòu)

  1. 業(yè)務(wù)邏輯和視圖分離
  2. Presenter和View之間通過(guò)接口交互
  3. 為了更高效管理線程蜈膨,團(tuán)隊(duì)決定使用Rxjava進(jìn)行線程統(tǒng)一管理屿笼。架構(gòu)風(fēng)格參考architecture-samples

接口設(shè)計(jì)

  1. Contract接口設(shè)計(jì)
public interface FileListContract {

    interface View  {
        showFileList(List<FileInfo> fileList);
        showNetWorkException(String errorMessage);
        showEmptyData();
    }

    interface Presenter {

        void getFileList();
    }
}

復(fù)制代碼
  1. 數(shù)據(jù)接口設(shè)計(jì)
public interface FileDataSource {
     Flowable<List<FileInfo>> getFileList();
}
復(fù)制代碼

5.小步安全重構(gòu)

  • 抽取FileFragment的業(yè)務(wù)邏輯到FilePresenter
  • FileFragment 提取UI接口
  • 抽取FileDataSource

重構(gòu)手法包含提取接口牺荠、移動(dòng)方法、移動(dòng)類驴一、抽取方法休雌、內(nèi)聯(lián)、提取變量等等肝断。過(guò)程還需要根據(jù)重構(gòu)適當(dāng)調(diào)整測(cè)試用例杈曲。

詳細(xì)的演示見(jiàn)視頻

  • 補(bǔ)充測(cè)試用例
  1. 補(bǔ)充FileUtils計(jì)算文件大小測(cè)試
@RunWith(JUnit4.class)
@SmallTest
public class FileUtilsTest {

    @Test
    public void should_return_B_unit_when_file_size_in_its_range() {
        //given
        long fileSize = 100;
        //when
        String format = FileUtils.formatFileSize(fileSize);
        //then
        assertEquals("100.00B", format);
    }

    @Test
    public void should_return_K_unit_when_file_size_in_its_range() {
        //given
        long fileSize = 1034;
        //when
        String format = FileUtils.formatFileSize(fileSize);
        //then
        assertEquals("1.01K", format);
    }

    @Test
    public void should_return_M_unit_when_file_size_in_its_range() {
        //given
        long fileSize = 1084000;
        //when
        String format = FileUtils.formatFileSize(fileSize);
        //then
        assertEquals("1.03M", format);
    }

    @Test
    public void should_return_G_unit_when_file_size_in_its_range() {
        //given
        long fileSize = 1114000000;
        //when
        String format = FileUtils.formatFileSize(fileSize);
        //then
        assertEquals("1.04G", format);
    }
}
復(fù)制代碼
  1. 補(bǔ)充Presenter業(yè)務(wù)邏輯測(cè)試
@RunWith(JUnit4.class)
@MediumTest
public class FilePresenterImplTest {

    @Rule
    public RxSchedulerRule rule = new RxSchedulerRule();

    @Test
    public void should_return_file_list_when_call_data_source_success() throws NetworkErrorException {
        //given
        FileListContract.View mockView = mock(FileListContract.View.class);
        FileDataSource mockFileDataSource = mock(FileDataSource.class);
        List<FileInfo> fileList = new ArrayList<>();
        fileList.add(new FileInfo("遺留代碼重構(gòu).pdf", 102400));
        fileList.add(new FileInfo("系統(tǒng)組件化.pdf", 9900));
        when(mockFileDataSource.getFileList()).thenReturn(Flowable.fromArray(fileList));
        FileListContract.FilePresenter filePresenter = new FilePresenterImpl(mockFileDataSource, mockView);
        //when
        filePresenter.getFileList();
        //then
        verify(mockView).showFileList(anyList());
    }

    @Test
    public void should_show_empty_data_when_call_data_source_return_empty() throws NetworkErrorException {
        //given
        FileListContract.View mockView = mock(FileListContract.View.class);
        FileDataSource mockFileDataSource = mock(FileDataSource.class);
        when(mockFileDataSource.getFileList()).thenReturn(Flowable.fromArray(new ArrayList<>()));
        FileListContract.FilePresenter filePresenter = new FilePresenterImpl(mockFileDataSource, mockView);
        //when
        filePresenter.getFileList();
        //then
        verify(mockView).showEmptyData();
    }

    @Test
    public void should_show_network_exception_when_call_data_source_return_net_error() throws NetworkErrorException {
        //given
        FileListContract.View mockView = mock(FileListContract.View.class);
        FileDataSource mockFileDataSource = mock(FileDataSource.class);
        when(mockFileDataSource.getFileList()).thenThrow(new NetworkErrorException());
        FileListContract.FilePresenter filePresenter = new FilePresenterImpl(mockFileDataSource, mockView);
        //when
        filePresenter.getFileList();
        //then
        verify(mockView).showNetWorkException("NetworkErrorException");
    }
}
復(fù)制代碼

我們可以通過(guò)查看Coverage查看邏輯的覆蓋情況,判斷是否有重要的邏輯遺漏胸懈。

最后我們運(yùn)行file bundle所有的測(cè)試担扑,報(bào)告如下:

6.集成驗(yàn)收測(cè)試

  1. file bundle模塊發(fā)布1.0.1版本
implementation 'com.cloud.disk.bundle:file:1.0.1'
復(fù)制代碼
  1. 執(zhí)行守護(hù)測(cè)試
image
  1. 運(yùn)行檢查

總結(jié)

本篇介紹了文件模塊團(tuán)隊(duì)將文件主頁(yè)重構(gòu)為MVP架構(gòu),并且補(bǔ)充了自動(dòng)化測(cè)試趣钱。經(jīng)過(guò)重構(gòu)后涌献,團(tuán)隊(duì)的開發(fā)效率和版本質(zhì)量有了明顯的提升。有了文件模塊的打樣首有,給其他團(tuán)隊(duì)帶來(lái)了很大的信心燕垃。

接下來(lái),我們將繼續(xù)分享動(dòng)態(tài)模塊的重構(gòu)之旅井联。與文件模塊不一樣的是動(dòng)態(tài)模塊決定采用Kotlin+MVVM架構(gòu)卜壕。

下一篇,單體移動(dòng)應(yīng)用“模塊化”演進(jìn)之旅(14)- Kotlin+MVVM重構(gòu)示例篇

CloudDisk示例代碼

CloudDisk

系列鏈接

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(1)- 開篇

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(2)-架構(gòu)篇

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(3)-示例篇

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(4)-分析篇

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(5)- 重構(gòu)方法篇

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(6)- 測(cè)試篇

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(7)- 解耦重構(gòu)演示篇(一)+視頻演示

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(8)- 依賴注入篇

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(9)- 路由篇

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(10)- 解耦重構(gòu)演示篇(二)

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(11)- 制品管理篇

移動(dòng)應(yīng)用遺留系統(tǒng)重構(gòu)(12)- 編譯調(diào)試篇

大綱

關(guān)于

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末烙常,一起剝皮案震驚了整個(gè)濱河市轴捎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蚕脏,老刑警劉巖侦副,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蝗锥,居然都是意外死亡跃洛,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門终议,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)汇竭,“玉大人,你說(shuō)我怎么就攤上這事穴张∠噶牵” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵皂甘,是天一觀的道長(zhǎng)玻驻。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么璧瞬? 我笑而不...
    開封第一講書人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任户辫,我火速辦了婚禮,結(jié)果婚禮上嗤锉,老公的妹妹穿的比我還像新娘渔欢。我一直安慰自己,他們只是感情好瘟忱,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開白布奥额。 她就那樣靜靜地躺著,像睡著了一般访诱。 火紅的嫁衣襯著肌膚如雪垫挨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評(píng)論 1 312
  • 那天触菜,我揣著相機(jī)與錄音九榔,去河邊找鬼。 笑死玫氢,一個(gè)胖子當(dāng)著我的面吹牛帚屉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播漾峡,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼喻旷!你這毒婦竟也來(lái)了生逸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤且预,失蹤者是張志新(化名)和其女友劉穎槽袄,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锋谐,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡遍尺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涮拗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乾戏。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖三热,靈堂內(nèi)的尸體忽然破棺而出鼓择,到底是詐尸還是另有隱情,我是刑警寧澤就漾,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布呐能,位于F島的核電站,受9級(jí)特大地震影響抑堡,放射性物質(zhì)發(fā)生泄漏摆出。R本人自食惡果不足惜朗徊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望偎漫。 院中可真熱鬧荣倾,春花似錦、人聲如沸骑丸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)通危。三九已至铸豁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間菊碟,已是汗流浹背节芥。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逆害,地道東北人头镊。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像魄幕,于是被迫代替她去往敵國(guó)和親相艇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容