JavaRush /Java Blog /Random-TW /Java 中重構的工作原理

Java 中重構的工作原理

在 Random-TW 群組發布
學習程式設計時,很多時間都花在寫程式碼上。大多數新手開發人員相信這是他們未來的活動。這在某種程度上是正確的,但程式設計師的任務也包括維護和重構程式碼。今天我們來談談重構。 Java 中重構的工作原理 - 1

JavaRush 課程中的重構

JavaRush 課程兩次涵蓋了重構主題: 由於這項艱鉅的任務,我們有機會在實踐中熟悉真正的重構,並且 IDEA 中的重構講座將幫助您了解使生活變得異常輕鬆的自動化工具。

什麼是重構?

這是程式碼結構的變化,但不改變其功能。例如,有一個方法比較 2 個數字,如果第一個數字較大則傳回true ,否則傳回 false
public boolean max(int a, int b) {
    if(a > b) {
        return true;
    } else if(a == b) {
        return false;
    } else {
        return false;
    }
}
結果是代碼非常繁瑣。即使是初學者也很少寫這樣的東西,但是卻有這樣的風險。看起來,if-else如果你能寫一個短 6 行的方法,為什麼這裡會有一個區塊:
public boolean max(int a, int b) {
     return a>b;
}
現在這個方法看起來簡單而優雅,儘管它與上面的範例做了相同的事情。這就是重構的工作原理:它改變程式碼的結構而不影響其本質。重構方法和技術有很多,我們將更詳細地考慮。

為什麼需要重構?

有幾個原因。例如追求程式碼的簡單、簡潔。該理論的支持者認為,程式碼應該盡可能簡潔,即使需要數十行註解才能理解。其他開發人員認為應該重構程式碼,以便用最少的註解就可以理解。每個團隊選擇自己的立場,但我們必須記住,重構不是還原其主要目標是改進程式碼的結構。 這個全球目標可以包含幾個目標:
  1. 重構可以提高對其他開發人員編寫的程式碼的理解;
  2. 幫助發現並修復錯誤;
  3. 允許您提高軟體開發速度;
  4. 整體改進了軟體組成。
如果長期不進行重構,可能會出現開發困難,甚至完全停工。

“代碼有味道”

當程式碼需要重構時,他們會說它「有味道」。當然,不是字面上的意思,但是這樣的程式碼看起來確實不太好看。以下我們將考慮初始階段的主要重構技術。

不必要的大元素

有些繁瑣的類別和方法由於其龐大的尺寸而無法有效地使用。

大班

這樣的類別有大量的程式碼行和許多不同的方法。對於開發人員來說,為現有類別添加功能通常比建立新類別更容易,這就是它成長的原因。通常,此類的功能是重載的。在這種情況下,將部分功能分離到單獨的類別中會有所幫助。我們將在重構技術部分更詳細地討論這一點。

大方法

當開發人員為方法添加新功能時,就會出現這種「氣味」。“既然可以寫在這裡,為什麼要把參數檢查放在一個單獨的方法裡呢?”,“為什麼要把求數組最大元素的方法分開,就放在這裡吧。這樣程式碼就更清晰了」以及其他誤解。 重構大型方法有兩個規則:
  1. 如果在編寫方法時,想要在程式碼中新增註釋,則需要將此功能分離到單獨的方法中;
  2. 如果一個方法需要超過 10-15 行程式碼,您應該識別它執行的任務和子任務,並嘗試將子任務分離到單獨的方法中。
消除大方法的幾種方法:
  • 將一個方法的部分功能分離到一個單獨的方法中;
  • 如果局部變數不允許您提取部分功能,您可以將整個物件傳遞給另一個方法。

使用許多原始資料類型

通常,當類別中用於儲存資料的欄位數量隨著時間的推移而增加時,就會出現此問題。例如,如果您使用基本類型而不是小物件來儲存資料(貨幣、日期、電話號碼等)或常數來編碼任何資訊。在這種情況下,一個好的做法是將欄位進行邏輯分組並將它們放置在單獨的類別中(選擇一個類別)。您也可以在類別中包含處理此資料的方法。

長長的選項列表

這是一個相當常見的錯誤,尤其是與大型方法結合使用時。如果方法的功能重載,或者方法組合了多種演算法,通常會發生這種情況。長長的參數清單非常難以理解,而且這樣的方法使用起來也不方便。因此,最好傳輸整個物件。如果物件沒有足夠的數據,則值得使用更通用的物件或分割方法的功能,以便它處理邏輯相關的資料。

資料組

邏輯相關的資料組經常出現在程式碼中。例如,資料庫的連線參數(URL、使用者名稱、密碼、架構名稱等)。如果無法從元素清單中刪除單一字段,則該清單是必須放置在單獨的類別(類別選擇)中的一組資料。

破壞 OOP 概念的解決方案

當開發人員違反 OOP 設計時,就會出現這種類型的「氣味」。如果他沒有完全理解這個範式的功能,不完全或不正確地使用它們,就會發生這種情況。

拒絕繼承

如果子類別使用了父類別的最小部分功能,那麼它聽起來就像一個不正確的層次結構。通常,在這種情況下,根本不會覆蓋不必要的方法或引發異常。如果一個類別是從另一個類別繼承的,這意味著幾乎完全使用了它的功能。正確層次結構範例: Java 中重構的工作原理 - 2 不正確層次結構範例: Java 中重構的工作原理 - 3

switch語句

操作員可能出了什麼問題switch?當它的設計非常複雜時就很糟糕了。這也包括許多嵌套塊if

具有不同介面的替代類

幾個類別實際上做同樣的事情,但它們的方法命名不同。

臨時場地

如果類別包含物件偶爾需要的臨時字段,當它充滿值時,其餘時間為空,或者上帝保佑,null那麼程式碼就會“聞起來”,這樣的設計是可疑的決定。

導致改裝困難的氣味

這些「氣味」更嚴重。其餘的主要損害對程式碼的理解,而這些並不使得修改程式碼成為可能。當引入任何功能時,一半的開發人員會退出,一半的開發人員會發瘋。

平行繼承層次結構

當你建立一個類別的子類別時,你必須建立另一個類別的另一個子類別。

統一依賴分佈

執行任何修改時,您必須查找此類的所有依賴項(使用)並進行許多小的變更。一項更改 - 在許多課程中進行編輯。

複雜的修改樹

這種氣味與前一種氣味相反:更改會影響同一類的大量方法。通常,此類程式碼中的依賴關係是級聯的:更改了一種方法後,您需要修復另一種方法中的某些內容,然後是第三種方法,依此類推。一堂課 - 許多變化。

“垃圾味”

相當令人不快的氣味,會導致頭痛。無用的、不必要的、舊的程式碼。幸運的是,現代 IDE 和 linter 已經學會警告此類氣味。

方法中有大量註釋

該方法幾乎每一行都有很多解釋性註釋。這通常與複雜的演算法相關,因此最好將程式碼劃分為幾個較小的方法並給它們指定有意義的名稱。

程式碼重複

不同的類別或方法使用相同的程式碼區塊。

懶人班

該類承擔的功能非常少,儘管其中許多功能都是計劃好的。

未使用的程式碼

程式碼中未使用類別、方法或變量,它們是「自重」。

過度耦合

這類異味的特點是程式碼中存在大量不必要的連接。

第三方方法

方法使用另一個物件的資料比使用自己的資料更頻繁。

不恰當的親密行為

一個類別使用另一個類別的服務欄位和方法。

長類通話

一個類別呼叫另一個類,後者從第三個類請求數據,從第四個類請求數據,依此類推。如此長的呼叫鏈意味著對目前類別結構的高度依賴。

類別任務經銷商

一個類別只需要將任務傳遞給另一個類別。也許應該將其刪除?

重構技術

以下我們將討論有助於消除所描述的程式碼異味的初始重構技術。

選課

該類別執行太多功能;其中一些功能需要移至另一個類別。例如,有一個類別Human還包含住宅地址和提供完整地址的方法:
class Human {
   private String name;
   private String age;
   private String country;
   private String city;
   private String street;
   private String house;
   private String quarter;

   public String getFullAddress() {
       StringBuilder result = new StringBuilder();
       return result
                       .append(country)
                       .append(", ")
                       .append(city)
                       .append(", ")
                       .append(street)
                       .append(", ")
                       .append(house)
                       .append(" ")
                       .append(quarter).toString();
   }
}
將地址資訊和方法(資料處理行為)放在一個單獨的類別中是一個好主意:
class Human {
   private String name;
   private String age;
   private Address address;

   private String getFullAddress() {
       return address.getFullAddress();
   }
}
class Address {
   private String country;
   private String city;
   private String street;
   private String house;
   private String quarter;

   public String getFullAddress() {
       StringBuilder result = new StringBuilder();
       return result
                       .append(country)
                       .append(", ")
                       .append(city)
                       .append(", ")
                       .append(street)
                       .append(", ")
                       .append(house)
                       .append(" ")
                       .append(quarter).toString();
   }
}

方法選擇

如果任何功能可以分組在一個方法中,則應將其放置在單獨的方法中。例如,計算二次方程式根的方法:
public void calcQuadraticEq(double a, double b, double c) {
    double D = b * b - 4 * a * c;
    if (D > 0) {
        double x1, x2;
        x1 = (-b - Math.sqrt(D)) / (2 * a);
        x2 = (-b + Math.sqrt(D)) / (2 * a);
        System.out.println("x1 = " + x1 + ", x2 = " + x2);
    }
    else if (D == 0) {
        double x;
        x = -b / (2 * a);
        System.out.println("x = " + x);
    }
    else {
        System.out.println("Equation has no roots");
    }
}
讓我們將所有三個可能選項的計算轉移到單獨的方法:
public void calcQuadraticEq(double a, double b, double c) {
    double D = b * b - 4 * a * c;
    if (D > 0) {
        dGreaterThanZero(a, b, D);
    }
    else if (D == 0) {
        dEqualsZero(a, b);
    }
    else {
        dLessThanZero();
    }
}

public void dGreaterThanZero(double a, double b, double D) {
    double x1, x2;
    x1 = (-b - Math.sqrt(D)) / (2 * a);
    x2 = (-b + Math.sqrt(D)) / (2 * a);
    System.out.println("x1 = " + x1 + ", x2 = " + x2);
}

public void dEqualsZero(double a, double b) {
    double x;
    x = -b / (2 * a);
    System.out.println("x = " + x);
}

public void dLessThanZero() {
    System.out.println("Equation has no roots");
}
每個方法的程式碼變得更短、更清晰。

傳輸整個對象

當呼叫帶參數的方法時,有時會看到這樣的程式碼:
public void employeeMethod(Employee employee) {
    // Некоторые действия
    double yearlySalary = employee.getYearlySalary();
    double awards = employee.getAwards();
    double monthlySalary = getMonthlySalary(yearlySalary, awards);
    // Продолжение обработки
}

public double getMonthlySalary(double yearlySalary, double awards) {
     return (yearlySalary + awards)/12;
}
在該方法中,employeeMethod分配了多達2行用於獲取值並將其儲存在原始變數中。有時,此類設計最多需要 10 行。將物件本身傳遞給方法要容易得多,您可以從中提取必要的資料:
public void employeeMethod(Employee employee) {
    // Некоторые действия
    double monthlySalary = getMonthlySalary(employee);
    // Продолжение обработки
}

public double getMonthlySalary(Employee employee) {
    return (employee.getYearlySalary() + employee.getAwards())/12;
}
簡單、簡短、簡潔。

將欄位進行邏輯分組並將它們放在單獨的類別中

儘管上面的例子非常簡單,很多人看到它們時可能會問“到底是誰做的?”,但許多開發人員由於疏忽、不願意重構程式碼,或者只是“行了”,類似的結構錯誤。

為什麼重構是有效的

良好重構的結果是程式碼易於閱讀,程式邏輯的修改不會成為威脅,新功能的引入不會變成程式碼解析地獄,而是幾天愉快的活動。如果從頭開始重寫程式會更容易,則不應使用重構。例如,團隊估計解析、分析和重構程式碼的勞動成本高於從頭開始實現相同功能的勞動成本。或是需要重構的程式碼有很多錯誤很難調試。知道如何改進程式碼結構是程式設計師工作中必須要做的事情。嗯,最好在 JavaRush 上學習 Java 程式設計——這是一個強調實踐的線上課程。1200 多個帶有即時驗證的任務、大約 20 個迷你項目、遊戲任務 - 所有這些都將幫助您對編碼充滿信心。最好的開始時間就是現在:) Как устроен рефакторинг в Java - 4

進一步深入重構的資源

關於重構最著名的書是《重構》。改進現有程式碼的設計」作者:Martin Fowler。還有一本關於重構的有趣出版物,是根據 Joshua Kiriewski 之前的書《Refactoring with Patterns》編寫的。說到模板。重構時,了解基本的應用程式設計模式總是非常有用的。這些偉大的書籍將對此有所幫助:
  1. 「設計模式」 - 由 Eric Freeman、Elizabeth Freeman、Kathy Sierra、Bert Bates 撰寫,來自 Head First 系列;
  2. 「可讀程式碼,或者說程式設計是一門藝術」——Dustin Boswell、Trevor Faucher。
  3. Steve McConnell 的《完美代碼》,概述了美麗而優雅的程式碼​​的原則。
嗯,有幾篇關於重構的文章:
  1. 任務艱鉅:讓我們開始重構遺留程式碼
  2. 重構
  3. 為大家重構
    留言
    TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
    GO TO FULL VERSION