遺留系統(tǒng)開(kāi)發(fā)之道

遺留系統(tǒng)之痛

問(wèn)題

在軟件這個(gè)行業(yè)里,有一個(gè)有意思的名詞叫“祖?zhèn)鞔a”。泛指那些結(jié)構(gòu)混亂的遺留系統(tǒng)代碼。相信大家或多或少在工作中都會(huì)遇到過(guò)遺留系統(tǒng),你是否遇到過(guò)下面的問(wèn)題?

  1. 應(yīng)用大泥球的結(jié)構(gòu),代碼看起來(lái)都很費(fèi)勁,更別說(shuō)改
  2. 代碼已經(jīng)改不動(dòng)了,想要重構(gòu),但卻一點(diǎn)信心都沒(méi)有
  3. 修一個(gè)bug,又引起了另外的bug

原因

遺留系統(tǒng)常常有2個(gè)非常明顯的特點(diǎn)。

  1. 代碼耦合度高,相互依賴性高
  2. 沒(méi)有足夠的自動(dòng)化測(cè)試覆蓋,完成改動(dòng)后要比較長(zhǎng)的時(shí)間來(lái)反饋事情是否做對(duì)

這使得我們對(duì)代碼修改的成本非常大,并且往往容易出錯(cuò)。

依賴性是軟件開(kāi)發(fā)中最為關(guān)鍵的問(wèn)題之一。在處理遺留代碼的過(guò)程中很大一部分工作都是圍繞著“接觸依賴性以便使改動(dòng)變得更容易”這個(gè)目標(biāo)來(lái)進(jìn)行的

遺留代碼開(kāi)發(fā)心法

image

對(duì)付依賴代碼的工作其實(shí)就是動(dòng)手改,但不是隨意地改。我們必須在做出能夠帶來(lái)價(jià)值的功能性改動(dòng)的同時(shí)使系統(tǒng)中的更多部分覆蓋足夠的測(cè)試。同時(shí)在這個(gè)過(guò)程中也有一些套路及手法可以參考,我們不需要摸著石頭過(guò)河。

選擇恰當(dāng)時(shí)機(jī)

  • 在修改遺留代碼之前,對(duì)覆蓋遺留代碼的場(chǎng)景/服務(wù)增加大型/中型測(cè)試。
  • 在對(duì)功能擴(kuò)展和修改時(shí),對(duì)原有代碼進(jìn)行重構(gòu)并加上對(duì)方法的小型測(cè)試
  • 在修復(fù) Bug 時(shí)增加自動(dòng)化測(cè)試保證問(wèn)題不要再次出現(xiàn)

確定改動(dòng)點(diǎn)

  • 仔細(xì)閱讀被測(cè)代碼、文檔、用例,或找其他同事了解,最大程度理解要修改的功能相關(guān)代碼

  • 參考常用的架構(gòu)模式(MV*)和通用的設(shè)計(jì)原則,作為我們重構(gòu)時(shí)的基準(zhǔn)

找出測(cè)試點(diǎn)

  • 遺留系統(tǒng)中核心算法或者業(yè)務(wù)邏輯一般混雜在其他代碼中,我們需要需要去識(shí)別核心的測(cè)試點(diǎn)

  • 合理設(shè)置測(cè)試策略,避免貿(mào)然修改代碼帶來(lái)的風(fēng)險(xiǎn),最大化重構(gòu)&自動(dòng)化測(cè)試的 ROI

解依賴

解依賴往往是最難的地方。我們通常會(huì)遇到兩個(gè)方面問(wèn)題:一是難以在測(cè)試用例中實(shí)例化目標(biāo)對(duì)象;二是難以在測(cè)試用例中運(yùn)行方法。

這時(shí)候就需要我用運(yùn)用恰當(dāng)?shù)慕庖蕾囀址ㄟM(jìn)行重構(gòu),接觸依賴。遺留代碼解依賴三板斧

編寫(xiě)測(cè)試

  • 取一個(gè)“看名知其意”的測(cè)試名
  • 選擇合適的測(cè)試替身
  • 編寫(xiě)測(cè)試代碼

修改重構(gòu)

對(duì)被測(cè)試保護(hù)起來(lái)的代碼進(jìn)行清理,遵守整潔代碼及消除壞味道,提高代碼可讀性

遺留系統(tǒng)解依賴三板斧

image

參數(shù)化方法

假設(shè)你有一個(gè)方法,該方法在內(nèi)部創(chuàng)建了某個(gè)對(duì)象,而你想要通過(guò)替換該對(duì)象來(lái)實(shí)現(xiàn)感知或分離。往往最簡(jiǎn)單的方法就是從外面將你的對(duì)象傳進(jìn)來(lái)

步驟

  1. 找出目標(biāo)方法,將它復(fù)制一份
  2. 給其中一份增加一個(gè)參數(shù),并將方法體中相應(yīng)的對(duì)象創(chuàng)建語(yǔ)句去掉,改為使用剛增加的這個(gè)參數(shù)
  3. 將另一份復(fù)制的方法體刪掉,代以對(duì)被參數(shù)化了的那個(gè)版本的調(diào)用,記得創(chuàng)建相應(yīng)的對(duì)象作參數(shù)

示例

public class TestCase {
    public void run() {
        TestResult result = new TestResult();
        result.runTest();
    }
}

class TestResult {
    boolean flag = false;

    void runTest() {
        flag = true;
    }
}

重構(gòu)

public class TestCase {
    public void run(TestResult result) {
        result.runTest();
    }
}

class TestResult {
    boolean flag = false;

    void runTest() {
        flag = true;
    }
}

測(cè)試

public class TestCaseTest {

    @Test
    public void should_return_flag_is_true_when_call_run() {
        TestCase testCase = new TestCase();
        TestResult mockTestResult = new TestResult();
        testCase.run(mockTestResult);
        Assert.assertTrue(mockTestResult.flag);
    }
}

參數(shù)適配

當(dāng)無(wú)法對(duì)一個(gè)參數(shù)的類型使用接口提取,或者當(dāng)該參數(shù)難以“偽裝”的時(shí)候,可采用參數(shù)適配手法

步驟

  1. 創(chuàng)建將被用于該方法的新接口,該接口越簡(jiǎn)單且能表達(dá)意圖越好。但也要注意,該接口不應(yīng)導(dǎo)致需要對(duì)該方法的代碼作大規(guī)模修改
  2. 為新接口創(chuàng)建一個(gè)用于產(chǎn)品代碼的實(shí)現(xiàn)
  3. 為新接口創(chuàng)建一個(gè)用于測(cè)試的“偽造”實(shí)現(xiàn)
  4. 編寫(xiě)一個(gè)簡(jiǎn)單的測(cè)試用例,將偽對(duì)象傳給該方法
  5. 對(duì)該方法作必要的修改以使其能使用新的參數(shù)
  6. 運(yùn)行測(cè)試來(lái)確保你能使用偽對(duì)象來(lái)測(cè)試該方法

示例

public class ARMDispatcher {

    List<String> marketBinding = new ArrayList<>();

    public void populate(HttpsParameters parameters) {
        String[] values = parameters.getCipherSuites();
        if (values != null && values.length > 0) {
            marketBinding.add(values[0]);
        }
    }
}

重構(gòu)

public class ARMDispatcher {

    List<String> marketBinding = new ArrayList<>();

    public void populate(ParameterSource parameterSource) {
        String values = parameterSource.getParameterValue();
        if (values != null) {
            marketBinding.add(values);
        }
    }

}

interface ParameterSource {
    String getParameterValue();
}
class HttpParameterSource implements ParameterSource {
    HttpsParameters mHttpsParameters;

    public HttpParameterSource(HttpsParameters httpsParameters) {
        mHttpsParameters = httpsParameters;
    }

    @Override
    public String getParameterValue() {
        String[] values = mHttpsParameters.getCipherSuites();
        if (values != null && values.length > 0) {
            return values[0];
        }
        return "";
    }
}

測(cè)試

class FakeParameterSource implements ParameterSource {

    public String value;

    @Override
    public String getParameterValue() {
        return value;
    }
}
public class ARMDispatcherTest {

    @Test
    public void testPopulate() {
        ARMDispatcher armDispatcher = new ARMDispatcher();
        FakeParameterSource fakeParameterSource = new FakeParameterSource();
        fakeParameterSource.value = "hello world";
        armDispatcher.populate(fakeParameterSource);
        assertThat(armDispatcher.marketBinding.size(),is(1));
        assertThat(armDispatcher.marketBinding.get(0),is("hello world"));
    }
}

接口提取

提取接口時(shí)并不一定要提取類上的所有公有方法,考慮遞增式地?cái)U(kuò)充該接口。

步驟

  1. 創(chuàng)建一個(gè)新接口,給它起一個(gè)好名字。暫時(shí)不要往里面添加任何方法
  2. 令你提取接口的目標(biāo)類實(shí)現(xiàn)該接口。這一步不會(huì)破壞任何東西,因?yàn)榻涌谏线€沒(méi)有任何方法。但你也可以編譯確認(rèn)一下
  3. 將你想要使用偽對(duì)象的地方從引用原類改為引用你新建的接口
  4. 編譯系統(tǒng)。如果編譯器匯報(bào)接口上缺少某某方法,則添加對(duì)應(yīng)的方法(同時(shí)也往偽類上面添加一個(gè)空的實(shí)現(xiàn)),直到編譯通過(guò)

示例

public class PaydayTransaction {
    TransactionLog transactionLog;

    public PaydayTransaction(TransactionLog transactionLog) {
        this.transactionLog = transactionLog;
    }

    public void run() {
        transactionLog.saveTransaction();
    }
}

class TransactionLog {
    public void saveTransaction() {
        //call database
    }

    public void recordError(int code) {

    }
}

重構(gòu)

public interface TransactionRecorder {
    void saveTransaction();
}
public class PaydayTransaction {
    TransactionRecorder transactionRecorder;

    public PaydayTransaction(TransactionRecorder transactionRecorder) {
        this.transactionRecorder = transactionRecorder;
    }

    public void run() {
        transactionRecorder.saveTransaction();
    }
}

class TransactionLog implements TransactionRecorder {
    @Override
    public void saveTransaction() {
        //call database
    }

    public void recordError(int code) {

    }
}

測(cè)試

class FakeTransactionLog implements TransactionRecorder {
    public boolean isSave = false;

    @Override
    public void saveTransaction() {
        isSave = true;
    }
}

public class PaydayTransactionTest {

    @Test
    public void testPayday() {
        FakeTransactionLog fakeTransactionLog = new FakeTransactionLog();
        PaydayTransaction paydayTransaction = new PaydayTransaction(fakeTransactionLog);
        paydayTransaction.run();
        Assert.assertThat(fakeTransactionLog.isSave, is(true));
    }
}

更多解依賴手法的源碼參考,LegacyCode

參考

《修改代碼的藝術(shù)》

關(guān)于

歡迎關(guān)注我的個(gè)人公眾號(hào)

微信搜索:一碼一浮生,或者搜索公眾號(hào)ID:life2code

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,305評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,962評(píng)論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,193評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,411評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,657評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評(píng)論 2 373