遺留系統(tǒng)之痛
問(wèn)題
在軟件這個(gè)行業(yè)里,有一個(gè)有意思的名詞叫“祖?zhèn)鞔a”。泛指那些結(jié)構(gòu)混亂的遺留系統(tǒng)代碼。相信大家或多或少在工作中都會(huì)遇到過(guò)遺留系統(tǒng),你是否遇到過(guò)下面的問(wèn)題?
- 應(yīng)用大泥球的結(jié)構(gòu),代碼看起來(lái)都很費(fèi)勁,更別說(shuō)改
- 代碼已經(jīng)改不動(dòng)了,想要重構(gòu),但卻一點(diǎn)信心都沒(méi)有
- 修一個(gè)bug,又引起了另外的bug
原因
遺留系統(tǒng)常常有2個(gè)非常明顯的特點(diǎn)。
- 代碼耦合度高,相互依賴性高
- 沒(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ā)心法
對(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)解依賴三板斧
參數(shù)化方法
假設(shè)你有一個(gè)方法,該方法在內(nèi)部創(chuàng)建了某個(gè)對(duì)象,而你想要通過(guò)替換該對(duì)象來(lái)實(shí)現(xiàn)感知或分離。往往最簡(jiǎn)單的方法就是從外面將你的對(duì)象傳進(jìn)來(lái)
步驟
- 找出目標(biāo)方法,將它復(fù)制一份
- 給其中一份增加一個(gè)參數(shù),并將方法體中相應(yīng)的對(duì)象創(chuàng)建語(yǔ)句去掉,改為使用剛增加的這個(gè)參數(shù)
- 將另一份復(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ù)適配手法
步驟
- 創(chuàng)建將被用于該方法的新接口,該接口越簡(jiǎn)單且能表達(dá)意圖越好。但也要注意,該接口不應(yīng)導(dǎo)致需要對(duì)該方法的代碼作大規(guī)模修改
- 為新接口創(chuàng)建一個(gè)用于產(chǎn)品代碼的實(shí)現(xiàn)
- 為新接口創(chuàng)建一個(gè)用于測(cè)試的“偽造”實(shí)現(xiàn)
- 編寫(xiě)一個(gè)簡(jiǎn)單的測(cè)試用例,將偽對(duì)象傳給該方法
- 對(duì)該方法作必要的修改以使其能使用新的參數(shù)
- 運(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ò)充該接口。
步驟
- 創(chuàng)建一個(gè)新接口,給它起一個(gè)好名字。暫時(shí)不要往里面添加任何方法
- 令你提取接口的目標(biāo)類實(shí)現(xiàn)該接口。這一步不會(huì)破壞任何東西,因?yàn)榻涌谏线€沒(méi)有任何方法。但你也可以編譯確認(rèn)一下
- 將你想要使用偽對(duì)象的地方從引用原類改為引用你新建的接口
- 編譯系統(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
參考
關(guān)于
歡迎關(guān)注我的個(gè)人公眾號(hào)
微信搜索:一碼一浮生,或者搜索公眾號(hào)ID:life2code