代码坏味道之非必要的
:notebook: 本文已歸檔到:「blog」
翻譯自:https://sourcemaking.com/refactoring/smells/dispensables
非必要的(Dispensables)這組壞味道意味著:這樣的代碼可有可無,它的存在反而影響整體代碼的整潔和可讀性。
- 冗余類
- 夸夸其談未來性
- 純稚的數據類
- 過多的注釋
- 重復代碼
- 擴展閱讀
- 參考資料
冗余類
冗余類(Lazy Class)
理解和維護總是費時費力的。如果一個類不值得你花費精力,它就應該被刪除。
問題原因
也許一個類的初始設計是一個功能完全的類,然而隨著代碼的變遷,變得沒什么用了。 又或者類起初的設計是為了支持未來的功能擴展,然而卻一直未派上用場。
解決方法
- 沒什么用的類可以運用 將類內聯化(Inline Class) 來干掉。
- 如果子類用處不大,試試 折疊繼承體系(Collapse Hierarchy) 。
收益
- 減少代碼量
- 易于維護
何時忽略
- 有時,創建冗余類是為了描述未來開發的意圖。在這種情況下,嘗試在代碼中保持清晰和簡單之間的平衡。
重構方法說明
將類內聯化(Inline Class)
問題
某個類沒有做太多事情。
解決
將這個類的所有特性搬移到另一個類中,然后移除原類。
折疊繼承體系(Collapse Hierarchy)
問題
超類和子類之間無太大區別。
解決
將它們合為一體。
夸夸其談未來性
夸夸其談未來性(Speculative Generality)
存在未被使用的類、函數、字段或參數。
問題原因
有時,代碼僅僅為了支持未來的特性而產生,然而卻一直未實現。結果,代碼變得難以理解和維護。
解決方法
- 如果你的某個抽象類其實沒有太大作用,請運用 折疊繼承體系(Collapse Hierarch) 。
- 不必要的委托可運用 將類內聯化(Inline Class) 消除。
- 無用的函數可運用 內聯函數(Inline Method) 消除。
- 函數中有無用的參數應該運用 移除參數(Remove Parameter) 消除。
- 無用字段可以直接刪除。
收益
- 減少代碼量。
- 更易維護。
何時忽略
- 如果你在一個框架上工作,創建框架本身沒有使用的功能是非常合理的,只要框架的用戶需要這個功能。
- 刪除元素之前,請確保它們不在單元測試中使用。如果測試需要從類中獲取某些內部信息或執行特殊的測試相關操作,就會發生這種情況。
重構方法說明
折疊繼承體系(Collapse Hierarchy)
問題
超類和子類之間無太大區別。
解決
將它們合為一體。
將類內聯化(Inline Class)
問題
某個類沒有做太多事情。
解決
將這個類的所有特性搬移到另一個類中,然后移除原類。
內聯函數(Inline Method)
問題
一個函數的本體比函數名更清楚易懂。
class PizzaDelivery {//...int getRating() {return moreThanFiveLateDeliveries() ? 2 : 1;}boolean moreThanFiveLateDeliveries() {return numberOfLateDeliveries > 5;} } 復制代碼解決
在函數調用點插入函數本體,然后移除該函數。
class PizzaDelivery {//...int getRating() {return numberOfLateDeliveries > 5 ? 2 : 1;} } 復制代碼移除參數(Remove Parameter)
問題
函數本體不再需要某個參數。
解決
將該參數去除。
純稚的數據類
純稚的數據類(Data Class) 指的是只包含字段和訪問它們的 getter 和 setter 函數的類。這些僅僅是供其他類使用的數據容器。這些類不包含任何附加功能,并且不能對自己擁有的數據進行獨立操作。
問題原因
當一個新創建的類只包含幾個公共字段(甚至可能幾個 getters / setters)是很正常的。但是對象的真正力量在于它們可以包含作用于數據的行為類型或操作。
解決方法
- 如果一個類有公共字段,你應該運用 封裝字段(Encapsulated Field) 來隱藏字段的直接訪問方式。
- 如果這些類含容器類的字段,你應該檢查它們是不是得到了恰當的封裝;如果沒有,就運用 封裝集合(Encapsulated Collection) 把它們封裝起來。
- 找出這些 getter/setter 函數被其他類運用的地點。嘗試以 搬移函數(Move Method) 把那些調用行為搬移到 純稚的數據類(Data Class) 來。如果無法搬移這個函數,就運用 提煉函數(Extract Method) 產生一個可搬移的函數。
- 在類已經充滿了深思熟慮的函數之后,你可能想要擺脫舊的數據訪問方法,以提供適應面較廣的類數據訪問接口。為此,可以運用 移除設置函數(Remove Setting Method) 和 隱藏函數(Hide Method) 。
收益
- 提高代碼的可讀性和組織性。特定數據的操作現在被集中在一個地方,而不是在分散在代碼各處。
- 幫助你發現客戶端代碼的重復處。
重構方法說明
封裝字段(Encapsulated Field)
問題
你的類中存在 public 字段。
class Person {public String name; } 復制代碼解決
將它聲明為 private,并提供相應的訪問函數。
class Person {private String name;public String getName() {return name;}public void setName(String arg) {name = arg;} } 復制代碼封裝集合(Encapsulated Collection)
問題
有個函數返回一個集合。
解決
讓該函數返回該集合的一個只讀副本,并在這個類中提供添加、移除集合元素的函數。
搬移函數(Move Method)
問題
你的程序中,有個函數與其所駐類之外的另一個類進行更多交流:調用后者,或被后者調用。
解決
在該函數最常引用的類中建立一個有著類似行為的新函數。將舊函數變成一個單純的委托函數,或是舊函數完全移除。
提煉函數(Extract Method)
問題
你有一段代碼可以組織在一起。
void printOwing() {printBanner();//print detailsSystem.out.println("name: " + name);System.out.println("amount: " + getOutstanding()); } 復制代碼解決
移動這段代碼到一個新的函數中,使用函數的調用來替代老代碼。
void printOwing() {printBanner();printDetails(getOutstanding()); }void printDetails(double outstanding) {System.out.println("name: " + name);System.out.println("amount: " + outstanding); } 復制代碼移除設置函數(Remove Setting Method)
問題
類中的某個字段應該在對象創建時被設值,然后就不再改變。
解決
去掉該字段的所有設值函數。
隱藏函數(Hide Method)
問題
有一個函數,從來沒有被其他任何類用到。
解決
將這個函數修改為 private。
過多的注釋
過多的注釋(Comments)
注釋本身并不是壞事。但是常常有這樣的情況:一段代碼中出現長長的注釋,而它之所以存在,是因為代碼很糟糕。
問題原因
注釋的作者意識到自己的代碼不直觀或不明顯,所以想使用注釋來說明自己的意圖。這種情況下,注釋就像是爛代碼的除臭劑。
最好的注釋是為函數或類起一個恰當的名字。
如果你覺得一個代碼片段沒有注釋就無法理解,請先嘗試重構,試著讓所有注釋都變得多余。
解決方法
- 如果一個注釋是為了解釋一個復雜的表達式,可以運用 提煉變量(Extract Variable) 將表達式切分為易理解的子表達式。
- 如果你需要通過注釋來解釋一段代碼做了什么,請試試 提煉函數(Extract Method) 。
- 如果函數已經被提煉,但仍需要注釋函數做了什么,試試運用 函數改名(Rename Method) 來為函數起一個可以自解釋的名字。
- 如果需要對系統某狀態進行斷言,請運用 引入斷言(Introduce Assertion) 。
收益
- 代碼變得更直觀和明顯。
何時忽略
注釋有時候很有用:
- 當解釋為什么某事物要以特殊方式實現時。
- 當解釋某種復雜算法時。
- 當你實在不知可以做些什么時。
重構方法說明
提煉變量(Extract Variable)
問題
你有個難以理解的表達式。
void renderBanner() {if ((platform.toUpperCase().indexOf("MAC") > -1) &&(browser.toUpperCase().indexOf("IE") > -1) &&wasInitialized() && resize > 0 ){// do something} } 復制代碼解決
將表達式的結果或它的子表達式的結果用不言自明的變量來替代。
void renderBanner() {final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;final boolean isIE = browser.toUpperCase().indexOf("IE") > -1;final boolean wasResized = resize > 0;if (isMacOs && isIE && wasInitialized() && wasResized) {// do something} } 復制代碼提煉函數(Extract Method)
問題
你有一段代碼可以組織在一起。
void printOwing() {printBanner();//print detailsSystem.out.println("name: " + name);System.out.println("amount: " + getOutstanding()); } 復制代碼解決
移動這段代碼到一個新的函數中,使用函數的調用來替代老代碼。
void printOwing() {printBanner();printDetails(getOutstanding()); }void printDetails(double outstanding) {System.out.println("name: " + name);System.out.println("amount: " + outstanding); } 復制代碼函數改名(Rename Method)
問題
函數的名稱未能恰當的揭示函數的用途。
class Person {public String getsnm(); } 復制代碼解決
修改函數名。
class Person {public String getSecondName(); } 復制代碼引入斷言(Introduce Assertion)
問題
某一段代碼需要對程序狀態做出某種假設。
double getExpenseLimit() {// should have either expense limit or a primary projectreturn (expenseLimit != NULL_EXPENSE) ?expenseLimit:primaryProject.getMemberExpenseLimit(); } 復制代碼解決
以斷言明確表現這種假設。
double getExpenseLimit() {Assert.isTrue(expenseLimit != NULL_EXPENSE || primaryProject != null);return (expenseLimit != NULL_EXPENSE) ?expenseLimit:primaryProject.getMemberExpenseLimit(); } 復制代碼注:請不要濫用斷言。不要使用它來檢查”應該為真“的條件,只能使用它來檢查“一定必須為真”的條件。實際上,斷言更多是用于自我檢測代碼的一種手段。在產品真正交付時,往往都會消除所有斷言。
重復代碼
重復代碼(Duplicate Code)
重復代碼堪稱為代碼壞味道之首。消除重復代碼總是有利無害的。
問題原因
重復代碼通常發生在多個程序員同時在同一程序的不同部分上工作時。由于他們正在處理不同的任務,他們可能不知道他們的同事已經寫了類似的代碼。
還有一種更隱晦的重復,特定部分的代碼看上去不同但實際在做同一件事。這種重復代碼往往難以找到和消除。
有時重復是有目的性的。當急于滿足 deadline,并且現有代碼對于要交付的任務是“幾乎正確的”時,新手程序員可能無法抵抗復制和粘貼相關代碼的誘惑。在某些情況下,程序員只是太懶惰。
解決方法
- 同一個類的兩個函數含有相同的表達式,這時可以采用 提煉函數(Extract Method) 提煉出重復的代碼,然后讓這兩個地點都調用被提煉出來的那段代碼。
- 如果兩個互為兄弟的子類含有重復代碼:
- 首先對兩個類都運用 提煉函數(Extract Method) ,然后對被提煉出來的函數運用 函數上移(Pull Up Method) ,將它推入超類。
- 如果重復代碼在構造函數中,運用 構造函數本體上移(Pull Up Constructor Body) 。
- 如果重復代碼只是相似但不是完全相同,運用 塑造模板函數(Form Template Method) 獲得一個 模板方法模式(Template Method) 。
- 如果有些函數以不同的算法做相同的事,你可以選擇其中較清晰地一個,并運用 替換算法(Substitute Algorithm) 將其他函數的算法替換掉。
- 如果兩個毫不相關的類中有重復代碼:
- 請嘗試運用 提煉超類(Extract Superclass) ,以便為維護所有先前功能的這些類創建一個超類。
- 如果創建超類十分困難,可以在一個類中運用 提煉類(Extract Class) ,并在另一個類中使用這個新的組件。
- 如果存在大量的條件表達式,并且它們執行完全相同的代碼(僅僅是它們的條件不同),可以運用 合并條件表達式(Consolidate Conditional Expression) 將這些操作合并為單個條件,并運用 提煉函數(Extract Method) 將該條件放入一個名字容易理解的獨立函數中。
- 如果條件表達式的所有分支都有部分相同的代碼片段:可以運用 合并重復的條件片段(Consolidate Duplicate Conditional Fragments) 將它們都存在的代碼片段置于條件表達式外部。
收益
- 合并重復代碼會簡化代碼的結構,并減少代碼量。
- 代碼更簡化、更易維護。
重構方法說明
提煉函數(Extract Method)
問題
你有一段代碼可以組織在一起。
void printOwing() {printBanner();//print detailsSystem.out.println("name: " + name);System.out.println("amount: " + getOutstanding()); } 復制代碼解決
移動這段代碼到一個新的函數中,使用函數的調用來替代老代碼。
void printOwing() {printBanner();printDetails(getOutstanding()); }void printDetails(double outstanding) {System.out.println("name: " + name);System.out.println("amount: " + outstanding); } 復制代碼函數上移(Pull Up Method)
問題
有些函數,在各個子類中產生完全相同的結果。
解決
將該函數移至超類。
構造函數本體上移(Pull Up Constructor Body)
問題
你在各個子類中擁有一些構造函數,它們的本體幾乎完全一致。
class Manager extends Employee {public Manager(String name, String id, int grade) {this.name = name;this.id = id;this.grade = grade;}//... } 復制代碼解決
在超類中新建一個構造函數,并在子類構造函數中調用它。
class Manager extends Employee {public Manager(String name, String id, int grade) {super(name, id);this.grade = grade;}//... } 復制代碼塑造模板函數(Form Template Method)
問題
你有一些子類,其中相應的某些函數以相同的順序執行類似的操作,但各個操作的細節上有所不同。
解決
將這些操作分別放進獨立函數中,并保持它們都有相同的簽名,于是原函數也就變得相同了。然后將原函數上移至超類。
注:這里只提到具體做法,建議了解一下模板方法設計模式。
替換算法(Substitute Algorithm)
問題
你想要把某個算法替換為另一個更清晰的算法。
String foundPerson(String[] people){for (int i = 0; i < people.length; i++) {if (people[i].equals("Don")){return "Don";}if (people[i].equals("John")){return "John";}if (people[i].equals("Kent")){return "Kent";}}return ""; } 復制代碼解決
將函數本體替換為另一個算法。
String foundPerson(String[] people){List candidates =Arrays.asList(new String[] {"Don", "John", "Kent"});for (int i=0; i < people.length; i++) {if (candidates.contains(people[i])) {return people[i];}}return ""; } 復制代碼提煉超類(Extract Superclass)
問題
兩個類有相似特性。
解決
為這兩個類建立一個超類,將相同特性移至超類。
提煉類(Extract Class)
問題
某個類做了不止一件事。
解決
建立一個新類,將相關的字段和函數從舊類搬移到新類。
合并條件表達式(Consolidate Conditional Expression)
問題
你有一系列條件分支,都得到相同結果。
double disabilityAmount() {if (seniority < 2) {return 0;}if (monthsDisabled > 12) {return 0;}if (isPartTime) {return 0;}// compute the disability amount//... } 復制代碼解決
將這些條件分支合并為一個條件,并將這個條件提煉為一個獨立函數。
double disabilityAmount() {if (isNotEligableForDisability()) {return 0;}// compute the disability amount//... } 復制代碼合并重復的條件片段(Consolidate Duplicate Conditional Fragments)
問題
在條件表達式的每個分支上有著相同的一段代碼。
if (isSpecialDeal()) {total = price * 0.95;send(); } else {total = price * 0.98;send(); } 復制代碼解決
將這段重復代碼搬移到條件表達式之外。
if (isSpecialDeal()) {total = price * 0.95; } else {total = price * 0.98; } send(); 復制代碼擴展閱讀
- 代碼的壞味道和重構
- 代碼壞味道之代碼臃腫
- 代碼壞味道之濫用面向對象
- 代碼壞味道之變革的障礙
- 代碼壞味道之非必要的
- 代碼壞味道之耦合
參考資料
- 重構——改善既有代碼的設計 - by Martin Fowler
- https://sourcemaking.com/refactoring
轉載于:https://juejin.im/post/5c812403f265da2d971120a6
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的代码坏味道之非必要的的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一文了解Kubernetes的前世今生
- 下一篇: S/4HANA生产订单增强WORKORD