在學習如何編程時,您會花費大量時間編寫代碼。大多數新手開發人員認為這就是他們將來要做的事情。這部分是正確的,但程序員的工作還包括維護和重構代碼。今天我們要談談重構。 重構在 Java 中是如何工作的 - 1

在 CodeGym 上重構

CodeGym 課程中兩次涉及重構: 大任務提供了通過實踐熟悉真正重構的機會,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 設計時,就會出現這些“氣味”。當他或她不完全理解 OOP 功能並且未能完全或正確地使用它們時,就會發生這種情況。

未能使用繼承

如果一個子類只使用了父類函數的一小部分,那麼它就會有層次結構錯誤的味道。發生這種情況時,通常多餘的方法不會被覆蓋,或者它們會拋出異常。一個類繼承另一個類意味著子類幾乎使用父類的所有功能。正確層次結構示例: 重構在 Java 中是如何工作的 - 2不正確層次結構示例: 重構在 Java 中是如何工作的 - 3

開關語句

聲明可能有什麼問題switch?當它變得非常複雜時就很糟糕了。一個相關的問題是大量的嵌套if語句。

具有不同接口的替代類

多個類做同樣的事情,但它們的方法有不同的名稱。

臨時字段

如果一個類有一個對像只有在設置它的值時才偶爾需要的臨時字段,並且它是空的,或者,上帝保佑,null其餘時間,那麼代碼就有味道了。這是一個有問題的設計決定。

使修改變得困難的氣味

這些氣味更嚴重。其他氣味主要使代碼更難理解,但這些會阻止您修改代碼。當您嘗試引入任何新功能時,一半的開發人員會退出,一半會發瘋。

並行繼承層次結構

當對一個類進行子類化需要您為另一個類創建另一個子類時,這個問題就會顯現出來。

均勻分佈的依賴

任何修改都需要您查找類的所有用途(依賴項)並進行大量小的更改。一個變化——在許多課程中進行編輯。

複雜的修改樹

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

“垃圾味”

一種相當令人不愉快的氣味,會引起頭痛。無用的、不必要的、舊的代碼。幸運的是,現代 IDE 和 linters 已經學會了警告這種氣味。

一個方法中的大量註釋

一個方法幾乎每一行都有很多解釋性註釋。這通常是由於復雜的算法,所以最好將代碼拆分成幾個較小的方法並給它們起解釋性名稱。

重複代碼

不同的類或方法使用相同的代碼塊。

懶人班

一個類只承擔很少的功能,儘管它計劃很大。

未使用的代碼

代碼中沒有使用類、方法或變量,它們是自重的。

過度連接

這類氣味的特點是代碼中存在大量不合理的關係。

外部方法

一個方法使用來自另一個對象的數據比它自己的數據更頻繁。

不適當的親密

一個類依賴於另一個類的實現細節。

長課電話

一個類調用另一個類,後者從第三個類請求數據,第三個類從第四個類獲取數據,依此類推。這麼長的調用鏈意味著對當前類結構的高度依賴。

任務經銷商類

只有將任務發送到另一個類時才需要一個類。也許它應該被刪除?

重構技術

下面我們將討論有助於消除所描述的代碼異味的基本重構技術。

提取一個類

一個類執行太多功能。其中一些必須轉移到另一個班級。例如,假設我們有一個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) {
     // Some actions
     double yearlySalary = employee.getYearlySalary();
     double awards = employee.getAwards();
     double monthlySalary = getMonthlySalary(yearlySalary, awards);
     // Continue processing
 }
 
 public double getMonthlySalary(double yearlySalary, double awards) {
      return (yearlySalary + awards)/12;
 }
employeeMethod2 整行專門用於接收值並將它們存儲在原始變量中。有時這樣的構造可能需要多達 10 行。傳遞對象本身並使用它來提取必要的數據要容易得多:

 public void employeeMethod(Employee employee) {
     // Some actions
     double monthlySalary = getMonthlySalary(employee);
     // Continue processing
 }
 
 public double getMonthlySalary(Employee employee) {
     return (employee.getYearlySalary() + employee.getAwards())/12;
 }

簡單,簡短,簡潔。

邏輯上將字段分組並移動到一個單獨的classDespite事實上面的例子非常簡單,當你看到它們時,很多人可能會問,“這是誰做的?”,許多開發人員確實因為粗心而犯了這樣的結構錯誤,不願意重構代碼,或者只是一種“已經足夠好了”的態度。

為什麼重構是有效的

良好重構的結果是,程序的代碼易於閱讀,改變其邏輯的前景並不可怕,引入新功能也不會成為代碼分析的地獄,而是幾天的愉快體驗. 如果從頭開始編寫程序會更容易,則不應進行重構。例如,假設您的團隊估計理解、分析和重構代碼所需的勞動量將大於從頭開始實現相同功能所需的勞動量。或者如果要重構的代碼有很多難以調試的問題。知道如何改進代碼的結構是程序員工作中必不可少的。學習 Java 編程最好在 CodeGym 上完成,這是一個強調實踐的在線課程。1200 多個即時驗證任務,約 20 個小項目,遊戲任務——所有這些都會幫助您對編碼充滿信心。最好的開始時間是現在:)

進一步沉浸在重構中的資源

最著名的關於重構的書是 Martin Fowler 的《重構。改進現有代碼的設計》。還有一本關於重構的有趣出版物,它基於之前的一本書:Joshua Kerievsky 的“Refactoring Using Patterns”。說到模式……重構時,了解基本的設計模式總是很有用的。這些優秀的書籍將對此有所幫助: 說到模式……重構時,了解基本的設計模式總是非常有用的。這些優秀的書籍將對此有所幫助:
  1. “設計模式”,作者 Eric Freeman、Elizabeth Robson、Kathy Sierra 和 Bert Bates,來自 Head First 系列
  2. Dustin Boswell 和 Trevor Foucher 的“可讀代碼的藝術”
  3. 史蒂夫·麥康奈爾 (Steve McConnell) 撰寫的“代碼大全”,闡述了編寫優美優雅代碼的原則。