如何更好地重構和組織后端代碼
Hi,我是阿昌
,今天學習記錄是關于如何更好地重構和組織后端代碼
的內(nèi)容。
如果說在氣泡上下文中開發(fā)新的需求,類似于老城區(qū)旁邊建設一個新城區(qū),那么在遺留系統(tǒng)中開發(fā)新的需求,就類似于在老城區(qū)內(nèi)部開發(fā)新的樓盤。
這就必然要涉及到拆遷的問題。拆遷終歸是一個聲勢浩大的工程,居民要先搬到別的地方,再拆除舊的建筑,蓋起新的樓宇,一番折騰之后,老居民才能搬進新家。
不過軟件的好處就在于它是“軟”的,不需要這么費勁兒。
可以很容易地復制、刪除和添加新的代碼,輕松地實現(xiàn)一個架構的變遷。
一、修繕者模式
絞殺植物模式
適合于用新的
系統(tǒng)和服務,替換舊的系統(tǒng)或舊系統(tǒng)中的一個模塊。
在舊系統(tǒng)內(nèi)部,也可以使用類似的思想來替換一個模塊,只不過這個模塊仍然位于舊系統(tǒng)中,而不是外部。把這種方式叫做 修繕者模式。
在修繕時,通過開關隔離舊系統(tǒng)待修繕的部分,并采用新的方式修改。
在修繕的過程中,模塊仍然能通過開關
對外提供完整功能。
這就好比是在老城區(qū)中修路,如果斷路施工對交通的影響就太大了。
更常見的做法是修繕其中的半條路,留另外半條來維持交通。不過,這必然會造成一定的擁堵。但在軟件中就好辦多了,可以將道路(待修繕的模塊)“復制”出來一份,以保障通行正常。等原道路修繕好之后,再刪除掉復制出來的道路即可。
用修繕者模式去修復過一個性能問題。一個 API 的請求特別慢,在本地修好后,在生產(chǎn)環(huán)境改觀不大。
推測這應該是數(shù)據(jù)分布導致的問題,本地環(huán)境的數(shù)據(jù)分布無法準確模擬生產(chǎn)環(huán)境。但當時的安全策略不允許訪問生產(chǎn)數(shù)據(jù)庫。
于是,接下來做調(diào)優(yōu)時,并沒有直接修改這個 API,而是將 API 復制了一份
出來,一個用來維持老的功能,一個用來性能調(diào)優(yōu)。
同時添加了一個針對這個 API 的 Filter,根據(jù)開關來決定要調(diào)用哪個 API。通過收集調(diào)優(yōu) API 中的日志,不斷地優(yōu)化,直到解決性能問題。
這時再清理掉舊 API、Filter 和開關。這樣做的好處是,由于你無法預測修繕過程中會產(chǎn)生哪些問題,這種通過開關保留回退余地的方法,顯然是更靈活的。
如何實現(xiàn)前端的增量演進和隨時回退,其實也是這種修繕者模式的思想。
將所有要修改的頁面復制出來一份,然后再加入開關
,就可以放心地重構頁面了。
在沒有單元測試的情況下,通過修繕者的方式來重構的。把代碼復制出來,重構完之后,通過開關在調(diào)用端切換,以完成 A/B 測試,從而實現(xiàn)安全地重構。
// 舊方法
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
// 新方法
public List<Cell> getFlaggedCells() {
return gameBoard.stream().filter(c -> c.isFlagged()).collect(toList());
}
// 調(diào)用端
List<int[]> cells;
List<Cell> cellsRefactored;
if (toggleOff) {
cells = getThem();
// 其他代碼
}
else {
cellsRefactored = getFlaggedCells();
// 其他代碼
}
二、抽象分支
這種優(yōu)雅的方式就是,把要重構的方法重構成一個方法對象,然后提取出一個接口,待重構的方法是接口的一個實現(xiàn),重構后的方法是另一個實現(xiàn)
。按這種方式重構之后的代碼如下所示:
public interface CellsProvider {
List<int[]> getCells();
}
public class OldCellsProvider implements CellsProvider {
@Override
public List<int[]> getCells() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
}
public class NewCellsProvider implements CellsProvider {
@Override
public List<int[]> getCells() {
return gameBoard.stream().filter(c -> c.isFlagged()).map(c -> c.getArray()).collect(toList());
}
}
在調(diào)用端,只需要通過工廠模式
,來根據(jù)開關得到 CellIndexesProvider 的不同實現(xiàn)
,其余的代碼都保持不變。在通過 A/B 測試之后,再刪除舊的實現(xiàn)和開關。
這種方法不但可以進行安全地重構,還可以用新的實現(xiàn)替換舊的實現(xiàn),完成功能或技術的升級。把這種模式叫做抽象分支(Branch by Absctration)。
當進行大的技術改動時,通常需要花費較長的時間。比如用 MyBatis 替換 Hibernate,或用 Kafka 替換 RabbitMQ。
傳統(tǒng)的做法是,在當前的產(chǎn)品代碼分支上創(chuàng)建一個新的分支,大規(guī)模去重寫。
這個分支發(fā)布之前要經(jīng)歷很長一段時間,直到最后全部修改完成后,才能把分支合并到產(chǎn)品代碼分支上。
更糟糕的是,這樣做合并時的代碼沖突會非常嚴重,而且架構調(diào)整后,首次上線大概率會出問題,交付風險非常高,無法做到增量演進。
為了解決這樣的問題,Martin Fowler 提出了抽象分支模式。
可以在不創(chuàng)建真實分支的情況下,通過技術手段,將大的重構項目分解成多個小步驟,每個小步驟都不會破壞功能,都是可以交付的,這樣就可以逐步完成架構的調(diào)整。
它的基本步驟是這樣的。先為舊實現(xiàn)創(chuàng)建一個抽象層
,讓舊的模塊去實現(xiàn)這個抽象層。
注意,這里的抽象層并不一定是接口,有可能是一系列接口或抽象類。
然后,讓部分調(diào)用端代碼依賴這個抽象層,而不是舊的模塊。
同樣要注意,這個替換是逐步進行的,不是一次性全部替換掉。
等全部調(diào)用端都依賴抽象層后,開始編寫新的實現(xiàn),并讓部分模塊使用新的實現(xiàn)。
這個過程也是逐步進行的,一方面可以更好地驗證新實現(xiàn),另一方面也可以隨時回退。
當全部調(diào)用端都使用新的實現(xiàn)后,再刪除舊的實現(xiàn)。
有的時候你需要讓新舊實現(xiàn)同時存在,對不同的調(diào)用端提供不同的實現(xiàn),這也是很常見的情況。
由于新代碼一直可以工作,因此你可以不斷提交、不斷交付、不斷驗證。
在實際工作中,抽象分支的運用還是非常廣泛的。一個技術改動,在初始化 Redis 的時候,改為從配置文件中讀取密碼,而不是從數(shù)據(jù)庫中讀取密碼。
對于這樣一個替換,可能直接三下五除二就完成了,但領悟了抽象分支之后,發(fā)現(xiàn)可以用更加優(yōu)雅的方式實現(xiàn)這個替換。一篇博客,可以當做加餐。
三、擴張與收縮模式
有的時候要修改的是接口本身(這里的接口是指方法的參數(shù)和返回值),這時候就不太容易通過抽象分支去替換了。
以前返回的是 List,而現(xiàn)在想打破這個接口,返回 List。
因為 List 仍然存在嚴重的基本類型偏執(zhí)的壞味道,而且本來已經(jīng)提取了 Cell 類,又通過 getArray 返回數(shù)組,簡直是多此一舉。
這時可以使用擴張 - 收縮(expand-contract)模式,也叫并行修改(Parallel Change)模式。它一般包含三個步驟,即擴張、遷移和收縮。
這里的擴張是指建立新的接口,它相比原來舊的代碼新增了一些東西,因此叫做“擴張
”;而收縮是指刪除舊的接口,它比之前減少了一些東西,因此叫“收縮
”。
一般來說,它會在類的內(nèi)部新建一些方法,以提供新的接口(即擴張)
,然后再逐步讓調(diào)用端使用新的接口(即遷移)
,當所有調(diào)用端都使用新的接口后,就刪除舊的接口(即收縮)。
拿剛才這個例子來說,提取完方法對象后的代碼如下所示:
public class CellsProvider {
public List<int[]> getCells() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
}
可以在這個方法對象中進行擴張
,新增一個方法
,以提供不同的接口:
public class CellsProvider {
public List<int[]> getCells() {
// 舊方法
}
public List<Cell> getFlaggedCells() {
return theList.stream().filter(c -> c.isFlagged()).collect(toList());
}
}
然后,讓調(diào)用端都調(diào)用這個新的 getFlaggedCells 方法,而不是舊的 getCells 方法。
在替換的過程中,新老方法是同時存在的,這也是為什么這個模式也叫并行修改。
等所有調(diào)用端都修改完畢,就可以刪掉舊方法了。
在老城區(qū)改造的過程中,這種擴張與收縮模式也是很常見的。城市完成了一次取暖線路改造,從以前的小區(qū)鍋爐房供暖改成了全市的熱力供暖。
施工方并沒有將小區(qū)內(nèi)舊的供暖管道直接連到市政熱力的管線上,而是在舊的管線旁邊新鋪了一條管線(即擴張),連接到市政管線。
在供暖期,兩條管線是并行運行的,一旦新管線發(fā)生問題,可以很快地切回舊的小區(qū)供暖。等并行運行一段時間后,判斷新管線沒問題了,再重新挖溝,拆除舊管線(即收縮)。
有的時候市民不理解為什么天天挖坑,但實際上這么做,都是為了保障供暖的安全性和高可用性啊。
四、再談接縫
在抽象分支中,我們提取的接口其實是一個接縫。
沒錯,接縫不但可以用來在測試中替換已有的實現(xiàn),它本身其實也是一個業(yè)務變化的方向。
在開發(fā)過程中,需要時刻去關注接縫,關注這種可能會產(chǎn)生變化的地方。
比如項目中使用了 RabbitMQ 作為消息中間件,發(fā)送和接受消息的代碼和 RabbitMQ 的 SDK 緊密耦合,這會帶來兩方面隱患,一方面當你想替換 MQ 的時候,需要修改全部調(diào)用點,另一方面,它也不好寫測試。
當意識到它其實是一個接縫的時候,就可以很輕松地通過一系列接口來隔離 SDK。
當需要替換 MQ 的時候,只需要提供一套新的實現(xiàn)類。這時的實現(xiàn)類應該叫做適配器(Adaptor)
,它其實也起到了防腐層
的作用。而在單元測試中,可以通過測試替身構建一組 Fake 的實現(xiàn)類,以提供內(nèi)存中的 MQ 功能。這樣的方案,既優(yōu)雅又靈活。除了代碼中蘊含著很多接縫,架構中也存在接縫。
延續(xù)上面 MQ 替換的例子,因為有很多在途的消息還沒有處理,這種技術遷移很難做到不停機地絲滑切換。
這時可以利用這個架構接縫,使用事件攔截模式,將發(fā)往 RabbitMQ 中的消息也同步發(fā)給新的 MQ(比如 Kafaka)。
同時,消費端可以通過冪等 API,來消除重復消費造成的問題。這樣一來,系統(tǒng)中就有兩個消息中間件同時存在,同時提供消息機制。
當基礎設施搭建好之后,就可以實現(xiàn)新老 MQ 的無縫切換了。
五、總結
- 修繕者模式和絞殺植物類似,可以用來改善單體內(nèi)的某個模塊。
- 抽象分支模式可以通過一個抽象,優(yōu)雅地替換舊的實現(xiàn)。
- 擴張收縮模式主要用于接口無法向后兼容的情況,一張一縮,一個接口就改造完了。
- 同時,除了代碼中的接縫,架構中也存在接縫,可以利用它們來實現(xiàn)架構中的替換。
無論是絞殺植物、修繕者、抽象分支還是擴張收縮,它們在實施的過程中,都允許新舊實現(xiàn)并存
,這種思想叫做并行運行
(Parallel Run)。這是貫徹增量演進原則
的基本思想,希望能牢牢記住。
說的絞殺植物、氣泡上下文、修繕者、抽象分支、擴張收縮、并行運行等模式,其實概念上都差不多,之所以叫不同的名字,是因為它們解決的是不同的問題。
比如絞殺植物模式解決的是新老系統(tǒng)的替換,修繕者模式解決的是一個服務內(nèi)部模塊的替換,而氣泡上下文專門用于將新需求和老系統(tǒng)隔離開來。
這就像不同的設計模式雖然叫不同的名字,但構造型模式用來解決不同場景下的對象構造,行為型模式用來處理不同場景下的行為選擇。
必須深刻理解這些模式,才能做出正確的選擇。
最后,王健對于各種模式的高度抽象,他的十六字心法如余音繞梁,三日不絕。文章來源:http://www.zghlxwxcb.cn/news/detail-434777.html
舊的不變,新的創(chuàng)建,一步切換,舊的,再見。文章來源地址http://www.zghlxwxcb.cn/news/detail-434777.html
到了這里,關于Day962.如何更好地重構和組織后端代碼 -遺留系統(tǒng)現(xiàn)代化實戰(zhàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!