CodeGym /Java Blog /Toto sisi /編碼規則:從創建系統到使用對象
John Squirrels
等級 41
San Francisco

編碼規則:從創建系統到使用對象

在 Toto sisi 群組發布
大家好!今天我們想和你談談寫出好的代碼。當然,並不是每個人都想立即閱讀像 Clean Code 這樣的書,因為它們包含大量信息,但一開始並不清楚。當你讀完時,你可能會扼殺所有編碼的慾望。考慮到所有這些,今天我想為您提供一個小指南(一小組建議)以幫助您編寫更好的代碼。在本文中,讓我們回顧一下與創建系統和使用接口、類和對象相關的基本規則和概念。閱讀本文不會花費太多時間,我希望不會讓您感到厭煩。我將從上到下按照我的方式進行,即從應用程序的一般結構到更具體的細節。 編碼規則:從創建系統到使用對象 - 1

系統

以下是系統通常需要的特徵:
  • 最小的複雜性。必須避免過於復雜的項目。最重要的是簡單明了(更簡單 = 更好)。
  • 易於維護。創建應用程序時,您必須記住它需要維護(即使您個人不負責維護它)。這意味著代碼必須清晰明了。
  • 松耦合。這意味著我們最大限度地減少了程序不同部分之間的依賴性(最大限度地遵守 OOP 原則)。
  • 可重用性。我們設計的系統具有在其他應用程序中重用組件的能力。
  • 可移植性。使一個系統適應另一個環境應該很容易。
  • 統一風格。我們在其各個組件中使用統一的樣式來設計我們的系統。
  • 可擴展性(scalability)。我們可以在不違反其基本結構的情況下增強系統(添加或更改組件不應影響所有其他組件)。
構建不需要修改或新功能的應用程序幾乎是不可能的。我們將不斷需要添加新部件,以幫助我們的創意與時俱進。這就是可擴展性發揮作用的地方。可擴展性本質上是擴展應用程序、添加新功能以及使用更多資源(或者換句話說,使用更大的負載)。換句話說,為了更容易添加新的邏輯,我們堅持一些規則,比如通過增加模塊化來降低系統的耦合度。編碼規則:從創建系統到使用對象 - 2

圖片來源

系統設計階段

  1. 軟件系統。整體設計應用程序。
  2. 劃分為子系統/包。定義邏輯上不同的部分並定義它們之間交互的規則。
  3. 將子系統劃分為類。將系統的各個部分劃分為具體的類和接口,並定義它們之間的交互。
  4. 將類劃分為方法。根據分配的職責,為類創建必要方法的完整定義。
  5. 方法設計。創建各個方法功能的詳細定義。
通常普通開發人員處理這種設計,而應用程序的架構師處理上述幾點。

系統設計的一般原則和概念

延遲初始化。在這個編程習慣中,應用程序不會浪費時間創建一個對象,直到它被實際使用。這加快了初始化過程並減少了垃圾收集器的負載。也就是說,您不應該做得太過火,因為那樣會違反模塊化原則。也許值得將所有構造實例移動到某個特定部分,例如,main 方法或工廠類。好的代碼的一個特徵是沒有重複的樣板代碼。通常,此類代碼放在單獨的類中,以便在需要時調用。

面向對象編程

我還要注意麵向方面的編程。這種編程範式就是要引入透明邏輯。即,將重複的代碼放入類(方面)中,並在滿足某些條件時調用。例如,調用具有特定名稱的方法或訪問特定類型的變量時。有時方面可能會令人困惑,因為無法立即清楚代碼從何處調用,但這仍然是非常有用的功能。特別是在緩存或日誌記錄時。我們在不向普通類添加額外邏輯的情況下添加此功能。 肯特·貝克 (Kent Beck) 的簡單架構四項規則:
  1. 表現力——一堂課的意圖應該清楚地表達出來。這是通過適當的命名、小尺寸和遵守單一職責原則(我們將在下面更詳細地考慮)來實現的。
  2. 最少數量的類和方法——如果你希望讓類盡可能小且範圍窄,你可能會走得太遠(導致獵槍手術反模式)。這個原則要求保持系統緊湊,不要走得太遠,為每個可能的動作創建一個單獨的類。
  3. 無重複——重複的代碼會造成混亂,並且表明系統設計欠佳,將被提取並移動到一個單獨的位置。
  4. 運行所有測試——通過所有測試的系統是可管理的。任何更改都可能導致測試失敗,向我們揭示我們對方法內部邏輯的更改也以意想不到的方式改變了系統的行為。

堅硬的

在設計系統時,著名的 SOLID 原則值得考慮:

S(單一職責)、 O(開閉)、 L(里氏代換)、 I(接口隔離)、 D(依賴倒置)。

我們不會詳述每個單獨的原則。這會超出本文的範圍,但您可以在此處閱讀更多內容。

界面

也許創建一個設計良好的類的最重要步驟之一是創建一個設計良好的接口來表示一個良好的抽象,隱藏類的實現細節並同時呈現一組明顯彼此一致的方法。讓我們仔細看看 SOLID 原則之一——接口隔離:客戶端(類)不應該實現他們不會使用的不必要的方法。換句話說,如果我們談論的是創建一個具有最少數量方法的接口,旨在執行接口的唯一工作(我認為這與單一責任原則非常相似),那麼最好創建幾個較小的方法來代替一個臃腫的界面。幸運的是,一個類可以實現多個接口。請記住正確命名您的接口:名稱應盡可能準確地反映分配的任務。當然,它越短,引起的混亂就越少。文檔註釋通常寫在界面級別。這些註釋提供了關於每個方法應該做什麼、它需要什麼參數以及它將返回什麼的詳細信息。

班級

編碼規則:從創建系統到使用對象 - 3

圖片來源

我們來看看類在內部是如何安排的。或者更確切地說,編寫類時應該遵循的一些觀點和規則。通常,一個類應該以特定順序的變量列表開始:
  1. 公共靜態常量;
  2. 私有靜態常量;
  3. 私有實例變量。
接下來是各種構造函數,從參數最少的到參數最多的順序排列。它們之後是從最公共到最私有的方法。一般來說,隱藏一些我們想要限制的功能實現的私有方法在最底層。

班級規模

現在我想談談班級規模。讓我們回顧一下 SOLID 原則之一——單一職責原則。它聲明每個對像只有一個目的(職責),其所有方法的邏輯旨在完成它。這告訴我們要避免大的、臃腫的類(這實際上是上帝對象的反模式),如果我們有很多方法,各種不同的邏輯塞進一個類,我們需要考慮把它拆成一個幾個邏輯部分(類)。這反過來會增加代碼的可讀性,因為如果我們知道任何給定類的大致目的,就不會花很長時間來理解每個方法的目的。另外,請注意類名,它應該反映它包含的邏輯。例如,如果我們有一個名稱中有 20 多個單詞的類,我們需要考慮重構。任何自重的類都不應該有那麼多內部變量。事實上,每個方法都與其中的一個或幾個一起工作,從而在類中產生了很多內聚力(這正是它應該的,因為類應該是一個統一的整體)。結果,增加班級的凝聚力會導致班級規模的減少,當然,班級的數量也會增加。這對某些人來說很煩人,因為您需要更多地仔細閱讀類文件才能了解特定的大型任務是如何工作的。最重要的是,每個類都是一個小模塊,應該與其他模塊的相關性最低。這種隔離減少了我們在向類中添加額外邏輯時需要進行的更改數量。每個方法都與其中的一個或幾個一起工作,從而在類中產生很多凝聚力(這正是它應該的,因為類應該是一個統一的整體)。結果,增加班級的凝聚力會導致班級規模的減少,當然,班級的數量也會增加。這對某些人來說很煩人,因為您需要更多地仔細閱讀類文件才能了解特定的大型任務是如何工作的。最重要的是,每個類都是一個小模塊,應該與其他模塊的相關性最低。這種隔離減少了我們在向類中添加額外邏輯時需要進行的更改數量。每個方法都與其中的一個或幾個一起工作,從而在類中產生很多凝聚力(這正是它應該的,因為類應該是一個統一的整體)。結果,增加班級的凝聚力會導致班級規模的減少,當然,班級的數量也會增加。這對某些人來說很煩人,因為您需要更多地仔細閱讀類文件才能了解特定的大型任務是如何工作的。最重要的是,每個類都是一個小模塊,應該與其他模塊的相關性最低。這種隔離減少了我們在向類中添加額外邏輯時需要進行的更改數量。的凝聚力導致班級規模的減少,當然,班級的數量也會增加。這對某些人來說很煩人,因為您需要更多地仔細閱讀類文件才能了解特定的大型任務是如何工作的。最重要的是,每個類都是一個小模塊,應該與其他模塊的相關性最低。這種隔離減少了我們在向類中添加額外邏輯時需要進行的更改數量。的凝聚力導致班級規模的減少,當然,班級的數量也會增加。這對某些人來說很煩人,因為您需要更多地仔細閱讀類文件才能了解特定的大型任務是如何工作的。最重要的是,每個類都是一個小模塊,應該與其他模塊的相關性最低。這種隔離減少了我們在向類中添加額外邏輯時需要進行的更改數量。

對象

封裝

這裡先說一個OOP的原則:封裝。隱藏實現並不等於創建一個方法來隔離變量(通過單獨的方法、getter 和 setter 不加考慮地限制訪問,這並不好,因為整個封裝點都丟失了)。隱藏訪問旨在形成抽象,即該類提供我們用來處理數據的共享具體方法。用戶不需要確切地知道我們是如何處理這些數據的——它有效,這就足夠了。

得墨忒耳法則

我們還可以考慮 Demeter 法則:它是一小組規則,有助於在類和方法級別管理複雜性。假設我們有一個Car對象,它有一個move(Object arg1, Object arg2)方法。根據 Demeter 法則,此方法僅限於調用:
  • Car對象本身的方法(換句話說,“this”對象);
  • 在move方法中創建的對象的方法;
  • 作為參數傳遞的對象的方法(arg1arg2);
  • 內部Car對象的方法(同樣是“this”)。
換句話說,得墨忒耳法則就像父母對孩子說的:“你可以和你的朋友交談,但不能和陌生人交談”。

數據結構

數據結構是相關元素的集合。將對象視為數據結構時,有一組數據元素可供方法操作。這些方法的存在是隱含的假設。也就是說,數據結構是一個對象,其目的是存儲和處理(處理)存儲的數據。它與常規對象的主要區別在於,普通對像是對隱式假定存在的數據元素進行操作的方法集合。你明白嗎?普通對象的主要方面是方法。內部變量有助於它們的正確操作。但是在數據結構中,方法是用來支持你使用存儲的數據元素的,這在這裡是最重要的。一種類型的數據結構是數據傳輸對象 (DTO)。這是一個具有公共變量且沒有方法(或只有讀/寫方法)的類,用於在處理數據庫、解析來自套接字的消息等時傳輸數據。數據通常不會長期存儲在此類對像中。它幾乎立即轉換為我們的應用程序工作的實體類型。反過來,實體也是一種數據結構,但其目的是參與應用程序各個級別的業務邏輯。DTO 的目的是將數據傳輸到應用程序或從應用程序傳輸數據。DTO 示例:也是一種數據結構,但其目的是參與應用程序各個級別的業務邏輯。DTO 的目的是將數據傳輸到應用程序或從應用程序傳輸數據。DTO 示例:也是一種數據結構,但其目的是參與應用程序各個級別的業務邏輯。DTO 的目的是將數據傳輸到應用程序或從應用程序傳輸數據。DTO 示例:

@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
一切似乎都很清楚,但在這裡我們了解了混合動力車的存在。混合對像是具有處理重要邏輯、存儲內部元素以及訪問器(獲取/設置)方法的方法的對象。這樣的對像很雜亂,很難添加新方法。您應該避免使用它們,因為不清楚它們的用途是什麼——存儲元素還是執行邏輯?

創建變量的原則

讓我們思考一下變量。更具體地說,讓我們考慮一下創建它們時適用的原則:
  1. 理想情況下,您應該在使用變量之前聲明並初始化它(不要創建一個變量然後忘記它)。
  2. 盡可能將變量聲明為 final 以防止其值在初始化後更改。
  3. 不要忘記我們通常在某種for循環中使用的計數器變量。也就是說,不要忘記將它們歸零。否則,我們所有的邏輯都可能崩潰。
  4. 您應該嘗試在構造函數中初始化變量。
  5. 如果可以選擇使用帶引用或不帶引用的對象(new SomeObject()),請選擇不帶引用,因為在使用該對像後,它將在下一個垃圾收集週期中被刪除,並且不會浪費其資源。
  6. 保持變量的生命週期(變量的創建和最後一次引用之間的距離)盡可能短。
  7. 在循環之前初始化循環中使用的變量,而不是在包含循環的方法的開頭。
  8. 始終從最有限的範圍開始,僅在必要時擴展(您應該嘗試使變量盡可能本地化)。
  9. 每個變量僅用於一個目的。
  10. 避免具有隱藏目的的變量,例如在兩個任務之間拆分的變量——這意味著它的類型不適合解決其中一個任務。

方法

編碼規則:從創建系統到使用對象 - 4

來自電影“星球大戰前傳 III - 西斯的複仇”(2005 年)

讓我們直接進行邏輯​​的實現,即方法。
  1. 規則#1——緊湊。理想情況下,一個方法不應超過 20 行。這意味著,如果公共方法顯著“膨脹”,您需要考慮拆分邏輯並將其移動到單獨的私有方法中。

  2. 規則 #2 — ifelsewhile和其他語句不應包含大量嵌套塊:大量嵌套會顯著降低代碼的可讀性。理想情況下,嵌套的{}塊不應超過兩個。

    並且還希望保持這些塊中的代碼緊湊和簡單。

  3. 規則#3——一個方法應該只執行一個操作。也就是說,如果一個方法執行各種複雜的邏輯,我們將其分解為子方法。因此,該方法本身將是一個外觀,其目的是以正確的順序調用所有其他操作。

    但是,如果該操作看起來太簡單而無法放入單獨的方法中怎麼辦?誠然,有時感覺就像向麻雀開砲,但小方法有很多好處:

    • 更好的代碼理解;
    • 隨著開發的進行,方法往往會變得更加複雜。如果一個方法開始時很簡單,那麼將其功能複雜化會容易一些;
    • 實現細節被隱藏;
    • 更容易的代碼重用;
    • 更可靠的代碼。

  4. 遞減規則——代碼應該從上到下閱讀:你閱讀的越低,你對邏輯的研究就越深入。反之亦然,你走得越高,方法就越抽象。例如,switch 語句相當不緊湊且不受歡迎,但如果您無法避免使用 switch,則應嘗試將其盡可能低地移動到最低級別的方法。

  5. 方法參數——理想的數字是多少?理想情況下,完全沒有 :) 但這真的發生了嗎?也就是說,您應該嘗試使用盡可能少的參數,因為參數越少,方法越容易使用,也越容易測試。如有疑問,請嘗試預測使用具有大量輸入參數的方法的所有場景。

  6. 此外,最好將具有布爾標誌的方法分開作為輸入參數,因為這一切本身就意味著該方法執行多個操作(如果為真,則做一件事;如果為假,則做另一件事)。正如我在上面所寫的,這不好,應該盡可能避免。

  7. 如果一個方法有大量的輸入參數(一個極端是 7 個,但你真的應該在 2-3 之後開始思考),一些參數應該被分組到一個單獨的對像中。

  8. 如果有幾個相似的(重載的)方法,那麼相似的參數必須以相同的順序傳遞:這提高了可讀性和可用性。

  9. 當你給一個方法傳遞參數時,你必須確保它們都被使用了,否則你為什麼需要它們?從界面中刪除任何未使用的參數並完成它。

  10. try/catch本質上看起來不太好,所以將它移到一個單獨的中間方法(一種處理異常的方法)中是個好主意:

    
    public void exceptionHandling(SomeObject obj) {
        try {  
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

我在上面談到了重複的代碼,但讓我再重複一遍:如果我們有幾個方法有重複的代碼,我們需要將它們移到一個單獨的方法中。這將使方法和類都更加緊湊。不要忘記管理名稱的規則:有關如何正確命名類、接口、方法和變量的詳細信息將在本文的下一部分討論。但這就是我今天要給你的全部內容。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION