如何重構(gòu)過大類
Hi,我是阿昌
,今天學(xué)習(xí)記錄的是關(guān)于如何重構(gòu)過大類
的內(nèi)容。
在過去的代碼里一定會遇到一種典型的代碼壞味道,那就是“過大類
”。
在產(chǎn)品迭代的過程中,由于缺少規(guī)范和守護(hù),單個類很容易急劇膨脹,有的甚至達(dá)到幾萬行的規(guī)模。過大的類會導(dǎo)致發(fā)散式的修改問題,只要需求有變化,這個類就得做相應(yīng)修改。
所以才有了有時候的“不得已而為之”的做法:為了不讓修改引起新的問題,通過復(fù)制黏貼來擴(kuò)展功能。
一、“過大類”的典型問題
“過大類”最常見的情況就是將所有的業(yè)務(wù)邏輯都寫在同一個界面
之中。
來看看后面這段示例代碼。
public class LoginActivity extends AppCompatActivity {
//省略相關(guān)代碼... ...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loginButton.setOnClickListener(v -> {
String username = usernameEditText.getText().toString();
String password = passwordEditText.getText().toString();
//用戶登錄
LogUtils.log("login...", username);
try {
//驗(yàn)證賬號及密碼
if (isValid(username) || isValid(password)) {
callBack.filed("invalid");
return;
}
//通過服務(wù)器判斷賬戶及密碼的有效性x
boolean result = checkFromServer(username, password);
if (result) {
UserController.isLogin = true;
UserController.currentUserInfo = new UserInfo();
UserController.currentUserInfo.username = username;
//登錄成功保持本地的信息
SharedPreferencesUtils.put(this, username, password);
} else {
Log.d("login failed");
}
} catch (NetworkErrorException networkErrorException) {
Log.d("networkErrorException");
}
});
}
private static boolean isValid(String str) {
if (str == null || TextUtils.isEmpty(str)) {
return false;
}
return true;
}
private boolean checkFromServer(String username, String password) {
//通過網(wǎng)絡(luò)請求服務(wù)數(shù)據(jù)
String result = httpUtil.post(username, password);
//解析Json對象
try {
JSONObject jsonObject = new JSONObject(result);
return jsonObject.getBoolean("result");
} catch (JSONException e) {
e.printStackTrace();
}
return false;
}
public static final String FILE_NAME = "share_data";
public static void put(Context context, String key, Object object) {
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
//... ...
editor.apply();
}
//省略相關(guān)代碼... ...
}
從上面的示例代碼中可以看出,創(chuàng)建頁面時初始化數(shù)據(jù)后,當(dāng)用戶點(diǎn)擊了登錄按鈕觸發(fā)數(shù)據(jù)的校驗(yàn)后,通過網(wǎng)絡(luò)請求校驗(yàn)數(shù)據(jù)的正確性,最后進(jìn)行本地的持續(xù)化數(shù)據(jù)存儲。
登錄頁面不僅僅承載了 UI 控件的初始化和管理,還需要負(fù)責(zé)登錄網(wǎng)絡(luò)請求、數(shù)據(jù)校驗(yàn)及結(jié)果處理、數(shù)據(jù)的持久化存儲等功能。
假如現(xiàn)在有這么幾個產(chǎn)品的需求要增加,應(yīng)該如何修改代碼進(jìn)行功能擴(kuò)展呢?
-
UI 上要做一些優(yōu)化
,當(dāng)?shù)卿浭r要彈出提示框提醒用戶。 - 需要對數(shù)據(jù)存儲進(jìn)行升級,所有
數(shù)據(jù)要存儲到數(shù)據(jù)庫
中。 - 用戶名的規(guī)則升級為僅支持電話和郵箱格式,需要在
本地做校驗(yàn)
。
可以看出基于這樣的設(shè)計,不管是 UI 還是校驗(yàn)規(guī)則上有需求變化,抑或是數(shù)據(jù)持久化或網(wǎng)絡(luò)框架有變化,都需要對登錄頁面進(jìn)行修改。
當(dāng)大量的邏輯耦合
在一起時,如果沒有任何自動化測試守護(hù),那么就會大大增加修改代碼的風(fēng)險。而且,要是基于這個代碼再持續(xù)不斷地添加新功能 ,就會陷入代碼越來越差、但又越來越不敢修改代碼的死循環(huán)之中。
二、重構(gòu)策略
隨著業(yè)務(wù)需求和代碼規(guī)模的不斷膨脹,針對過大類的重構(gòu)策略就是分而治之。
通過分層
將不同維度的變化控制在獨(dú)立的邊界中,使之能夠獨(dú)立的演化,從而減少修改代碼時彼此之間產(chǎn)生的影響。
從前面的例子可以識別出典型的 3 個不同維度的變化場景:
- 第一個是 UI 上的變化;
- UI 上的變化,如主題或排版的設(shè)計,不會對數(shù)據(jù)業(yè)務(wù)產(chǎn)生影響,此時如果有
獨(dú)立的 UI 層
,在擴(kuò)展、修改時就能減少對其他邏輯代碼的影響。一般在常見的分層架構(gòu)模式下,會有獨(dú)立的 View 層來承載獨(dú)立的 UI 變化。
- UI 上的變化,如主題或排版的設(shè)計,不會對數(shù)據(jù)業(yè)務(wù)產(chǎn)生影響,此時如果有
- 第二個是業(yè)務(wù)數(shù)據(jù)邏輯的變化;
- 業(yè)務(wù)數(shù)據(jù)邏輯的變化也一樣,一些數(shù)據(jù)的校驗(yàn)、計算、組裝規(guī)則也都是容易發(fā)生變化的維度。同樣在常見的分層架構(gòu)中也有
獨(dú)立的業(yè)務(wù)邏輯處理層
。
- 業(yè)務(wù)數(shù)據(jù)邏輯的變化也一樣,一些數(shù)據(jù)的校驗(yàn)、計算、組裝規(guī)則也都是容易發(fā)生變化的維度。同樣在常見的分層架構(gòu)中也有
- 第三個維度是基礎(chǔ)設(shè)施框架的變化。
- 基礎(chǔ)設(shè)施框架,比如持久化的框架,可能會從前期輕量的配置存儲需求演化為數(shù)據(jù)庫的存儲;網(wǎng)絡(luò)請求框架則可能會隨著技術(shù)棧的升級替換為新的框架。如果此時所有對于基礎(chǔ)設(shè)施的調(diào)用都散落在各個 UI 的入口上,那么修改變更的成本就會非常高。
下面以 MVP
(Model-View-Presenter)這種分層架構(gòu)為例,來看看 MVP 的架構(gòu)是如何進(jìn)行分層設(shè)計和交互的。
在 MVP 模式中,模型層提供數(shù)據(jù),視圖層負(fù)責(zé)顯示,表現(xiàn)層負(fù)責(zé)邏輯的處理。
MVP 架構(gòu)在視圖層與表現(xiàn)層的交互過程中都會定義對應(yīng)的接口,以使彼此之間的依賴更加穩(wěn)定。
由于模型與視圖完全分離,可以在修改視圖時不影響模型。
同時也可以將一個表現(xiàn)層用于多個視圖,且不需要改變表現(xiàn)層的邏輯。這個特點(diǎn)非常有用,因?yàn)橐晥D的變化總是比模型的變化更加頻繁。
另外,使用接口依賴能更好地提高代碼的可測試性,例如在對表現(xiàn)層進(jìn)行分層測試
時,只需要驗(yàn)證視圖層的接口有沒有正常被調(diào)用即可。
相比對幾百行的方法進(jìn)行測試,職責(zé)更加單一的分層能讓編寫自動化測試的工作變得更簡單。
以上面那個新增需求為例,進(jìn)行重構(gòu)后的代碼擴(kuò)展方式, 可以參考這張表格。
可以看出,分而治之
的策略將需求變化隔離在了不同的分層
之中,這樣需求變化就只在一個可控的邊界里,可以減少相互影響
。
三、重構(gòu)流程
回到一開始提出的問題,如何更高效、更高質(zhì)量地完成組件內(nèi)分層架構(gòu)的重構(gòu)?
將組件內(nèi)分層架構(gòu)的重構(gòu)流程按 3 個維度分為了 7 個步驟。
1、業(yè)務(wù)分析
對于遺留系統(tǒng)來說,比較常見的問題就是需求的上下文中容易存在斷層,所以第一步就是盡可能地了解、分析原有的業(yè)務(wù)需求
。
只有更清楚地挖掘原有的需求設(shè)計,才不會因?yàn)槔斫馍系牟町惓霈F(xiàn)錯誤的代碼調(diào)整。
參考 3 種常用的方式來理解需求。
- 第一種方式就是找人:通過與相關(guān)干系人(如與產(chǎn)品經(jīng)理、設(shè)計人員、測試人員)
溝通
,對需求進(jìn)行確認(rèn)和答疑,這是最直接有效的方式。 - 第二種方式就是看文檔但有時候你會發(fā)現(xiàn)如果人員流動大的話,可能相關(guān)干系人也不清楚原有的設(shè)計,這時可以參考看文檔的方法??梢酝ㄟ^查看相關(guān)的文檔(如查看原有的需求文檔、設(shè)計文檔、測試用例、設(shè)計稿),幫助更好地去理解原有的需求。當(dāng)然這里也有可能存在沒有文檔或者文檔的內(nèi)容已經(jīng)過時的問題,
- 第三種方法——看代碼。代碼肯定反映了最新的代碼需求,如果有自動化測試代碼,還可以通過測試用例的輸入和輸出來輔助理解需求。一般可以從最上層的 UI 頁面代碼看起,逐步根據(jù)代碼的調(diào)用棧查看相關(guān)的邏輯。通常來說,業(yè)務(wù)分析這一步有兩個重要的場景要梳理清楚:
- 第一個是用戶正常的使用場景;
- 第二個是用戶異常的使用場景。這些場景都將是后面補(bǔ)充自動化驗(yàn)收測試的重要輸入。還是以前面登陸的代碼為例,用戶正常的使用場景應(yīng)該包括:
- 輸入正確的賬號密碼,點(diǎn)擊登錄正常驗(yàn)證。
- 輸入錯誤的賬號密碼,點(diǎn)擊登錄提示失敗。
- .……
- 異常的使用場景應(yīng)該包括:
- 當(dāng)用戶點(diǎn)擊登錄后,但因?yàn)槭謾C(jī)出現(xiàn)網(wǎng)絡(luò)異常,需要提示網(wǎng)絡(luò)異常。
- 當(dāng)用戶點(diǎn)擊登錄后,但服務(wù)器返回異常的錯誤時,需要提示相應(yīng)的錯誤碼。
- ……
2、代碼分析
業(yè)務(wù)分析之后就是代碼分析,通過這一步,一方面是要了解原有的業(yè)務(wù),另外一方面要去診斷現(xiàn)有代碼中有哪些優(yōu)化點(diǎn)。
通常除了像“過大類”這種明顯的問題,可能也會存在代碼規(guī)范、方法復(fù)雜度、循環(huán)依賴、代碼潛在漏洞等問題。
需要盡可能將這些問題都識別出來,作為后續(xù)重構(gòu)的輸入。
推薦幾個常用的類檢查工具。
-
第一個是 Lint。Lint 是 Android Studio 自帶的代碼掃描分析工具,它可以幫助我們發(fā)現(xiàn)代碼結(jié)構(gòu)或質(zhì)量問題。Lint 發(fā)現(xiàn)的每個問題都有描述信息和等級,我們可以很方便地定位問題,同時按照嚴(yán)重程度來解決。
-
第二個是 Sonar。Sonar 也提供了 SonarLint 作為 IDE 的插件。通過該插件可以幫助我們識別代碼中的基礎(chǔ)壞味道、代碼復(fù)雜度以及潛在的缺陷等問題。關(guān)于 Lint 的使用,你只需要在你的項目中選擇 Code->Inspect Code 菜單后運(yùn)行檢查,就可以在 Problems 窗口中查看具體的問題列表了。
關(guān)于 SonarLint 插件,你需要先從 IDE 中搜索安裝該插件。安裝成功后右擊鼠標(biāo)選擇菜單欄中的 “Analyze with SonarLint” 可以觸發(fā)掃描。
具體的問題列表可以在 SonarLint 窗口中查看。
在這一步,建議至少將工具檢查出來的 Error 級別問題也納入重構(gòu)修改,特別是一些圈復(fù)雜度高的類和方法,都可以重點(diǎn)記錄下來,這些都是后續(xù)做重構(gòu)需要重點(diǎn)關(guān)注的內(nèi)容。
3、補(bǔ)充自動化驗(yàn)收測試
經(jīng)過前面的業(yè)務(wù)分析和代碼分析后,來看第三步,這是為第一步業(yè)務(wù)分析梳理出來的用戶場景補(bǔ)充自動化驗(yàn)收測試。
為什么需要先補(bǔ)充自動化驗(yàn)收測試呢?
因?yàn)橹挥杏辛藴y試的覆蓋,后面第五步在進(jìn)行小步安全重構(gòu)時,才能頻繁借助這些測試來驗(yàn)證重構(gòu)有沒有破壞原有的業(yè)務(wù)邏輯,這樣能更好地發(fā)現(xiàn)和減少因?yàn)橹貥?gòu)修改代碼而引起新的問題。
這一步通常是覆蓋中大型的自動化測試,可以借助 Espresso 或 Robolectric 框架。
例如前面那個登錄的例子,我將梳理出來的用戶場景,變成自動化的驗(yàn)收測試用例。
public class LoginActivityTest{
public void should_login_sucees_when_input_correct_username_and_password(){//... ...}
public void should_login_failed_when_input_error_username_and_password(){//... ...}
public void should_show_network_error_tip_when_current_network_is_exception(){//... ...}
public void should_show_error_code_when_server_is_error(){//... ...}
//... ...
}
注意,這一步需要將前面第一步的業(yè)務(wù)分析場景全部覆蓋,并且所有的用例需要執(zhí)行通過。
4、簡單設(shè)計
補(bǔ)充好自動化驗(yàn)收測試后,接下來就是進(jìn)行“簡單設(shè)計”了。
這一步讓我們在開始動手重構(gòu)前,想清楚重構(gòu)后的代碼將會是什么樣子,以終為始才能讓我們的目標(biāo)更加清晰,讓過程更加可度量。
經(jīng)常聽到一句半開玩笑的話,就是“代碼重構(gòu)以后又變成另外一個遺留系統(tǒng)”,其實(shí),這很可能就是因?yàn)槲覀儧]有先進(jìn)行設(shè)計,缺乏清晰的重構(gòu)目標(biāo)。
那么這一步怎么來做呢?
可以根據(jù)選擇的架構(gòu)模式,定義出核心的類、接口和數(shù)據(jù)模型,這些關(guān)鍵的要素能支撐起整個架構(gòu)的模式。
以登錄這個例子來講,假設(shè)希望重構(gòu)為 MVP 架構(gòu),那么首先是整體的核心類的設(shè)計。
//View
public class LoginActivity implement LoginContract.LoginView
//Presenter
public class LoginPresenter
//Model
public class UserInfo
其次是核心的交互接口。
//interface
public interface LoginContract {
interface LoginView {
success(UserInfo userInfo);
failed(String errorMessage);
}
}
通過簡單設(shè)計這一步,要定義出支持未來架構(gòu)的核心的類、接口和數(shù)據(jù)模型。
5、小步安全重構(gòu)
接下來是小步安全重構(gòu)。
在重構(gòu)的過程中,要最大限度運(yùn)用五類遺留系統(tǒng)典型的代碼壞味道的安全重構(gòu)手法,減少人工直接修改代碼的頻率,盡可能做到小步提交,并借助測試進(jìn)行頻繁地驗(yàn)證,逐步將代碼修改為新設(shè)計的架構(gòu)模式。
這樣既能提高重構(gòu)的效率,通過自動化又能有效避免手工挪動代碼帶來的潛在錯誤。
在執(zhí)行這個步驟中,有 3 個關(guān)鍵要點(diǎn)需要特別注意。
- 第一個是
小步
,將整個重構(gòu)分解為小的步驟,例如通過一次重構(gòu)將業(yè)務(wù)邏輯移動到 Presenter 類或是將原有的 View 實(shí)現(xiàn)替換為接口回調(diào)的形式。每一次小的重構(gòu)后可以通過版本管理工具進(jìn)行保存,這樣方便我們及時將代碼進(jìn)行回滾。 - 第二個是
頻繁運(yùn)行測試
。每當(dāng)有一次小的重構(gòu)完成后都需要頻繁執(zhí)行測試,如果這個時候測試有異常,就證明我們的重構(gòu)破壞了原有的功能,需要進(jìn)行排查。通過這樣的反饋,我們可以在更早期發(fā)現(xiàn)問題并及時處理。 - 第三個是
使用 IDE 的安全重構(gòu)功能
。使用自動化重構(gòu)可以有效減少人為修改代碼帶來的風(fēng)險,并且效率也會更高。這一步需要將所有的代碼按照第四步中的設(shè)計,完成所有的代碼重構(gòu),并且要保證編寫的自動化驗(yàn)收測試全部運(yùn)行通過。
6、補(bǔ)充中小型測試
當(dāng)重構(gòu)完成后,此時的代碼可測性更高,是補(bǔ)充中小型測試的最佳時機(jī)。
通過補(bǔ)充用例可以固化重構(gòu)后的代碼邏輯,避免后續(xù)代碼邏輯被破壞。
此外,中小型自動化測試的執(zhí)行時間更快,更能提前反饋問題。
通常來說,在這一步要給重構(gòu)后新增的類補(bǔ)充測試。還是以前面登錄為例,重構(gòu)后新增了一個 LoginPresenter 的類,那么就要對里面的 login 方法進(jìn)行更細(xì)粒度的測試,覆蓋方法內(nèi)部更細(xì)的分支條件和異常條件。
就像后面代碼演示的這樣,要補(bǔ)充驗(yàn)證 username、password 的校驗(yàn)和模擬 Exception 的小型測試。
class LoginPresenter{
boolean boolean(String username,String password){
if(isValid(username)|| isValid(password)){
return false;
}
try{
XXX.login(username,password);
}catch(NetWorkException e)
{
//... ...
}
}
}
這一步可以借助測試覆蓋率工具,來檢查重構(gòu)后代碼的核心業(yè)務(wù)邏輯是否有覆蓋測試。
當(dāng)然這里我們不一定要求 100% 的覆蓋率,具體要結(jié)合業(yè)務(wù)和代碼來進(jìn)行評估。
7、集成驗(yàn)收
是最后檢查整個重構(gòu)的結(jié)果,只有集成了才算是真的完成了重構(gòu)。
這一步,不僅要保證重構(gòu)后的代碼獨(dú)立編譯調(diào)試通過,還要保證所有的自動化測試和集成驗(yàn)收測試也能運(yùn)行通過。
通常來說,如果前面的 6 個步驟做到位,那么最后的集成階段應(yīng)該不會有太多的問題。
這也是經(jīng)常說的“質(zhì)量內(nèi)建”,雖然前面增加了投入,但能有效減少后期的返工。
在實(shí)際的過程中,要注意避免出現(xiàn)長生命周期的重構(gòu)分支,否則可能會在最后集成時出現(xiàn)大量的代碼沖突。
此外,中大型的重構(gòu)也應(yīng)該合理拆分任務(wù),讓每一個小步的重構(gòu)都能滿足集成的條件。
如果過程質(zhì)量做得好,其實(shí)我覺得更好的方式就是直接基于主干重構(gòu),避免拉長期的重構(gòu)分支。
四、總結(jié)
重構(gòu)的流程和關(guān)鍵的要點(diǎn)都總結(jié)到了下面這張圖中。
-
分析階段的兩個步驟讓以始為終,深入了解需求和代碼現(xiàn)狀;
-
重構(gòu)階段的四個步驟讓能更加安全、高效地完成代碼調(diào)整;
-
驗(yàn)收階段則提醒我們,只有集成才是真正地完成了重構(gòu)工作。
文章來源:http://www.zghlxwxcb.cn/news/detail-425172.html
“Talk is cheap, show me the code”文章來源地址http://www.zghlxwxcb.cn/news/detail-425172.html
到了這里,關(guān)于Day936.如何重構(gòu)過大類 -系統(tǒng)重構(gòu)實(shí)戰(zhàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!