首頁 > 軟體

Java使用connectTo方法提高程式碼可續性詳解

2022-08-24 14:02:27

原始碼有兩種不同的使用者:程式設計師和計算機。一方面,計算機既能處理乾淨、結構良好的程式碼,也能處理混亂的程式碼。另一方面,程式設計師對程式碼的可讀性很敏感。甚至是程式碼中的空白、正確使用縮排(這與計算機完全無關)也決定了程式碼容易理解或難以理解。

此外,程式碼的可讀性也提高了可靠性,因為通常不容易隱藏一些bug。並且提高了可維護性,因為它更容易修改。

關於可讀性的一些想法

編寫可讀的程式碼是一門被低估的技術,學校很少教授這種技術,但它卻與軟體的可靠性、維護和發展密切相關。程式設計師通常學習用機器容易理解的東西來實現所需功能的程式碼。這個編碼過程需要新增一層又一層的抽象來將功能分解為更小的單元。

Java語言中,這些抽象是包、類和方法。如果整個系統足夠大,就沒有程式設計師可以單獨控制整個程式碼庫。有些開發者對某個特定的業務有一個縱深的認識。其他開發人員可能只負責一個抽象層並維護它的API。他們都需要經常閱讀和理解別人編寫的程式碼。提高可讀性意味著將程式設計師理解一段程式碼所需的時間最小化。

如何編寫可讀的程式?用一句關於表現力的格言來總結就是,簡單而直接地說出你的意思。事實上,可讀性意味著清楚地表達程式碼意圖。統一建模語言(UML)設計師之一格雷迪·布克(Grady Booch)給出了一個自然的類比:乾淨的程式碼讀起來就像優美的散文。

寫好散文不是簡單地遵循一套固定的規則,而是需要練習和閱讀著名作家的偉大文章,這一過程可能需要數年時間。幸運的是,與自然語言相比,計算機程式碼的表達能力非常有限,所以編寫出乾淨的程式碼比寫優美的散文更容易,或者至少更有條理。業界對重構和編寫乾淨的程式碼越來越感興趣。可讀性已經成為敏捷開發中最重要的關注點之一。

試圖使用一組簡單的數位指標(如標記)來評估可讀性。識別符號的長度、表示式中出現的括號的數量,等等。這項工作仍在進行中,要達成一個穩定的共識還有很長的路要走,我們接下來將通過一個例子來說明和解釋程式碼可讀性的一些改進點。

整理connectTo方法

現在我們將注意力轉向一個名叫connectTo的方法,該方法會將兩個組裡面的容器進行合併,並且容器裡面的水會被均分。對其進行重構以提高可讀性。首先檢視初始版本的實現:

public void connectTo(Container other) {
   // 如果兩個容器已經連線,則不做任何事情
   if (group==other.group) return;
   int size1 = group.size(),
   size2 = other.group.size();
   double tot1 = amount * size1,
   tot2 = other.amount * size2,
   newAmount = (tot1 + tot2) / (size1 + size2);
   // 合併兩個組
   group.addAll(other.group);
   // 更新要連線的所有容器的組
   for (Container c: other.group) { c.group = group; }
   // 更新所有新連線的容器
   for (Container c: group) { c.amount = newAmount; }
}

這裡有一個缺陷:它包含了大量的註釋,試圖解釋每一行程式碼的含義。有些程式設計師關心他們的同事,想要他們更好的理解程式碼,自然會新增這樣的註釋。然而,這並不是實現容易理解這一目標的最有效的方法。更好的選擇是使用提取方法的方式來進行重構。

可讀性提示:“提取方法”重構規則——提取可以實現某一個小功能的程式碼塊轉到一個新方法並使用描述性名稱。

我們可以在connectTo方法中應用這種技術。事實上,我們可以拆分5個新的方法,以及獲得一個新的、可讀性更強的connectTo方法:

/** Connects this container with another.
 *
 * @param other The container that will be connected to this one
 */
public void connectTo(Container other) {
   if (this.isConnectedTo(other)) return;
   double newAmount = (groupAmount() + other.groupAmount()) /      
   (groupSize() + other.groupSize());
   mergeGroupWith(other.group);
   setAllAmountsTo(newAmount);
}

這個方法更短,可讀性更強。如果你試著把這個方法大聲讀出來,你會發現它幾乎可以變成可以被理解的一個短文。為此,我們引入了五種適當的支援方法。事實上,很多業內的大佬都認為長方法是一種不好的程式碼味道,提取方法來消除這種壞味道是普遍被採納的一種重構技術。

新增註釋只能解釋部分程式碼,而提取方法既解釋程式碼又隱藏生成過程程式碼——將程式碼提取到單個方法中。在這個例子中,它會使原來的方法抽象級別保持在更高、更統一的高度,避免了舊版本程式碼中的高層API解釋和底層實現錯綜複雜地交織在一起。

用查詢替換區域性變數是另一種可用於connectTo方法的重構技術。

可讀性提示:“用查詢替換區域性變數”重構規則——更改區域性變數,通過呼叫一個計算其值的新方法來替換該量。你可以將此技術應用於區域性變數newAmount,該變數只分配一次,然後用作setAllAmountsTo方法的引數。應用該技術可以直接刪除變數newAmount,並將connectTo方法的最後兩行替換為以下內容。

mergeGroupWith(other.group);
setAllAmountsTo(amountAfterMerge(other));

amountAfterMerge是一個計算合併後的每個容器水量的新方法。但是,稍加思考就會發現,amountAfterMerge方法需要克服很多困難才能完成任務,因為在呼叫方法時,兩個group已經完成了合併。group已經包含了other的group。一個很好的折衷方案是將計算新水量的表示式封裝到一個新方法中,同時保留區域性變數,以便在合併組之前計算出新的量。

final double newAmount = amountAfterMerge(other);
mergeGroupWith(other.group);
setAllAmountsTo(newAmount);

總而言之,我不建議進行這種重構,如抽出5個方法版本中的程式碼所示newAmount表示式是可讀的,不需要隱藏在單獨的方法中。當它替換的表示式很複雜或在類中多次出現時,“用查詢替換區域性變數”規則通常更有用。

現在看看可讀版本中connectTo方法的五個新支援方法。在這五個方法中,有兩個最好宣告為私有的,因為它們可能導致容器物件處於不一致的狀態,不應該從類外部呼叫。他們是mergeGroupWith方法和setAllAmountsTo方法。

mergeGroupWith方法合併兩組容器而不更新它們的水量。如果有人單獨從外部呼叫它,很可能使一些或所有容器的水量發生錯誤。這個方法只有在使用它的上下文中才有意義:在connectTo方法的末尾,然後呼叫setAllAmountsTo方法。事實上,它是否真的應該獨立成一個方法是有爭議的。

一方面,讓它獨立可以通過給予它一個好名字來解釋它的用途,而不是像開始的版本那樣使用註釋解釋。另一方面,獨立出來的方法可能在錯誤的上下文中被呼叫。因為我們是為了可讀性而優化的,所以建立獨立的方法會更好一點。類似的權衡setAllAmountsTo方法也適用。

private void mergeGroupWith(Set<Container> otherGroup) {
   group.addAll(otherGroup);
   for (Container x: otherGroup) {
     x.group = group;
   }
}
private void setAllAmountsTo(double amount) {
   for (Container x: group) {
     x.amount = amount;
   }
}

私有方法不值得用Javadoc註釋。它們只在類內部使用,所以很少有人覺得有必要了解他們的細節。因此,新增註釋不是太有必要的。註釋的成本並不限於編寫它們所需的時間。就像其他原始碼一樣,它需要維護,否則可能會過時。也就是說,隨著版本的迭代,註釋和它所描述的程式碼不同步了。

記住:過時的評論比沒有評論更糟糕! 用描述性名稱代替註釋並不能避免這種風險。如果編寫的程式碼功能和名稱不符了,然後最終仍然可能產生一些過時的名稱,這和過時的註釋同樣糟糕。

其他三種新的支援方法都是唯讀特性,不會帶來任何不良影響。我們不應該輕易做出讓他們公有化的決定。新增到類中任何公共成員的後續維護成本都要比新增相同的私有成員的成本大得多。公共方法的額外成本包括:

  • 描述其功能的適當註釋;
  • 條件檢查,以處理可能不正確的輸入內容;
  • 一套完整的測試,以確保其正確性。

connectTo方法的三個新的公有支援方法:

/** Checks whether this container is connected to another one.
 *
 * @param other the container whose connection with this will be
checked
 * @return <code>true</code> if this container is connected
 * to <code>other</code>
 */
public boolean isConnectedTo(Container other) {
 return group == other.group;
}
/** Returns the number of containers in the group of this
container.
 *
 * @return the size of the group
 */
public int groupSize() {
 return group.size();
}
/** Returns the total amount of water in the group of this
container.
 *
 * @return the amount of water in the group
 */
public double groupAmount() {
 return amount * group.size();
}

順便說一下,isConnectedTo方法還改進了類的可測試性,因為它使以前在實現中需要推測的內容都變成了直接可測試的。實現connectTo的六個方法都非常短,其中connectTo是最長的方法本身只有6行。簡潔是乾淨程式碼的主要原則之一。

到此這篇關於Java使用connectTo方法提高程式碼可續性詳解的文章就介紹到這了,更多相關Java connectTo內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com