常見代碼重構技巧(非常實用)
(給ImportNew加星標,提高Java技能)
爲什麼要重構
1_代碼重構漫畫. jpeg
項目在不斷演進過程中,代碼不停地在堆砌。如果沒有人爲代碼的質量負責,代碼總是會往越來越混亂的方向演進。當混亂到一定程度之後,量變引起質變,項目的維護成本已經高過重新開發一套新代碼的成本,想要再去重構,已經沒有人能做到了。
造成這樣的原因往往有以下幾點:
-
編碼之前缺乏有效的設計
-
成本上的考慮,在原功能堆砌式編程
-
缺乏有效代碼質量監督機制
對於此類問題,業界已有有很好的解決思路:通過持續不斷的重構將代碼中的 “壞味道” 清除掉。
什麼是重構
重構一書的作者 Martin Fowler 對重構的定義:
重構(名詞):對軟件內部結構的一種調整,目的是在不改變軟件可觀察行爲的前提下,提高其可理解性,降低其修改成本。
重構(動詞):使用一系列重構手法,在不改變軟件可觀察行爲的前提下,調整其結構。
根據重構的規模可以大致分爲大型重構和小型重構:
大型重構:對頂層代碼設計的重構,包括:系統、模塊、代碼結構、類與類之間的關係等的重構,重構的手段有:分層、模塊化、解耦、抽象可複用組件等等。這類重構的工具就是我們學習過的那些設計思想、原則和模式。這類重構涉及的代碼改動會比較多,影響面會比較大,所以難度也較大,耗時會比較長,引入 bug 的風險也會相對比較大。
小型重構:對代碼細節的重構,主要是針對類、函數、變量等代碼級別的重構,比如規範命名和註釋、消除超大類或函數、提取重複代碼等等。小型重構更多的是使用統一的編碼規範。這類重構要修改的地方比較集中,比較簡單,可操作性較強,耗時會比較短,引入 bug 的風險相對來說也會比較小。什麼時候重構 新功能開發、修 bug 或者代碼 review 中出現 “代碼壞味道”,我們就應該及時進行重構。持續在日常開發中進行小重構,能夠降低重構和測試的成本。
代碼的壞味道
2_代碼常見問題. png
代碼重複
- 實現邏輯相同、執行流程相同
方法過長
-
方法中的語句不在同一個抽象層級
-
邏輯難以理解,需要大量的註釋
-
面向過程編程而非面向對象
過大的類
-
類做了太多的事情
-
包含過多的實例變量和方法
-
類的命名不足以描述所做的事情
邏輯分散
-
發散式變化:某個類經常因爲不同的原因在不同的方向上發生變化
-
散彈式修改:發生某種變化時,需要在多個類中做修改
嚴重的情結依戀
- 某個類的方法過多的使用其他類的成員
數據泥團 / 基本類型偏執
-
兩個類、方法簽名中包含相同的字段或參數
-
應該使用類但使用基本類型,比如表示數值與幣種的 Money 類、起始值與結束值的 Range 類
不合理的繼承體系
-
繼承打破了封裝性,子類依賴其父類中特定功能的實現細節
-
子類必須跟着其父類的更新而演變,除非父類是專門爲了擴展而設計,並且有很好的文檔說明
過多的條件判斷
過長的參數列
臨時變量過多
令人迷惑的暫時字段
-
某個實例變量僅爲某種特定情況而設置
-
將實例變量與相應的方法提取到新的類中
純數據類
-
僅包含字段和訪問(讀寫)這些字段的方法
-
此類被稱爲數據容器,應保持最小可變性
不恰當的命名
-
命名無法準確描述做的事情
-
命名不符合約定俗稱的慣例
過多的註釋
壞代碼的問題
-
難以複用
-
系統關聯性過多,導致很難分離可重用部分
-
難於變化
-
一處變化導致其他很多部分的修改,不利於系統穩定
-
難於理解
-
命名雜亂,結構混亂,難於閱讀和理解
-
難以測試
-
分支、依賴較多,難以覆蓋全面
什麼是好代碼
3_代碼質量如何衡量. jpg
代碼質量的評價有很強的主觀性,描述代碼質量的詞彙也有很多,比如可讀性、可維護性、靈活、優雅、簡潔。這些詞彙是從不同的維度去評價代碼質量的。其中,可維護性、可讀性、可擴展性又是提到最多的、最重要的三個評價標準。
要寫出高質量代碼,我們就需要掌握一些更加細化、更加能落地的編程方法論,這就包含面向對象設計思想、設計原則、設計模式、編碼規範、重構技巧等。
如何重構
SOLID 原則
4_SOLID 原則. png
單一職責原則
一個類只負責完成一個職責或者功能,不要存在多於一種導致類變更的原因。
單一職責原則通過避免設計大而全的類,避免將不相關的功能耦合在一起,來提高類的內聚性。同時,類職責單一,類依賴的和被依賴的其他類也會變少,減少了代碼的耦合性,以此來實現代碼的高內聚、松耦合。但是,如果拆分得過細,實際上會適得其反,反倒會降低內聚性,也會影響代碼的可維護性。
開放 - 關閉原則
添加一個新的功能,應該是通過在已有代碼基礎上擴展代碼(新增模塊、類、方法、屬性等),而非修改已有代碼(修改模塊、類、方法、屬性等)的方式來完成。
開閉原則並不是說完全杜絕修改,而是以最小的修改代碼的代價來完成新功能的開發。
很多設計原則、設計思想、設計模式,都是以提高代碼的擴展性爲最終目的的。特別是 23 種經典設計模式,大部分都是爲了解決代碼的擴展性問題而總結出來的,都是以開閉原則爲指導原則的。最常用來提高代碼擴展性的方法有:多態、依賴注入、基於接口而非實現編程,以及大部分的設計模式(比如,裝飾、策略、模板、職責鏈、狀態)。
里氏替換原則
子類對象(object of subtype/derived class)能夠替換程序(program)中父類對象(object of base/parent class)出現的任何地方,並且保證原來程序的邏輯行爲(behavior)不變及正確性不被破壞。
子類可以擴展父類的功能,但不能改變父類原有的功能
父類中凡是已經實現好的方法(相對於抽象方法而言),實際上是在設定一系列的規範和契約,雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。
接口隔離原則
調用方不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。接口隔離原則提供了一種判斷接口的職責是否單一的標準:通過調用者如何使用接口來間接地判定。如果調用者只使用部分接口或接口的部分功能,那接口的設計就不夠職責單一。
依賴反轉原則
高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。
迪米特法則
一個對象應該對其他對象保持最少的瞭解
合成複用原則
儘量使用合成 / 聚合的方式,而不是使用繼承。
單一職責原則告訴我們實現類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向接口編程;接口隔離原則告訴我們在設計接口的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,告訴我們要對擴展開放,對修改關閉。
設計模式
設計模式:軟件開發人員在軟件開發過程中面臨的一般問題的解決方案。這些解決方案是衆多軟件開發人員經過相當長的一段時間的試驗和錯誤總結出來的。每種模式都描述了一個在我們周圍不斷重複發生的問題,以及該問題的核心解決方案。
-
創建型:主要解決對象的創建問題,封裝複雜的創建過程,解耦對象的創建代碼和使用代碼
-
結構型:主要通過類或對象的不同組合,解耦不同功能的耦合
-
行爲型:主要解決的是類或對象之間的交互行爲的耦合
代碼分層
image.png
模塊結構說明
-
server_main:配置層,負責整個項目的 module 管理,maven 配置管理、資源管理等;
-
server_application:應用接入層,承接外部流量入口,例如:RPC 接口實現、消息處理、定時任務等;不要在此包含業務邏輯;
-
server_biz:核心業務層,用例服務、領域實體、領域事件等
-
server_irepository:資源接口層,負責資源接口的暴露
-
server_repository:資源層,負責資源的 proxy 訪問,統一外部資源訪問,隔離變化。注意:這裏強調的是弱業務性,強數據性;
-
server_common:公共層,vo、工具等
代碼開發要遵守各層的規範,並注意層級之間的依賴關係。
命名規範
一個好的命名應該要滿足以下兩個約束:
準確描述所做得事情
格式符合通用的慣例
如果你覺得一個類或方法難以命名的時候,可能是其承載的功能太多了,需要進一步拆分。
約定俗稱的慣例
類命名
類名使用大駝峯命名形式,類命通常使用名詞或名詞短語。接口名除了用名詞和名詞短語以外,還可以使用形容詞或形容詞短語,如 Cloneable,Callable 等,表示實現該接口的類有某種功能或能力。
方法命名
方法命名採用小駝峯的形式,首字小寫,往後的每個單詞首字母都要大寫。和類名不同的是,方法命名一般爲動詞或動詞短語,與參數或參數名共同組成動賓短語,即動詞 + 名詞。一個好的函數名一般能通過名字直接獲知該函數實現什麼樣的功能。
重構技巧
提煉方法
多個方法代碼重複、方法中代碼過長或者方法中的語句不在一個抽象層級。
方法是代碼複用的最小粒度,方法過長不利於複用,可讀性低,提煉方法往往是重構工作的第一步。
意圖導向編程:把處理某件事的流程和具體做事的實現方式分開。
-
把一個問題分解爲一系列功能性步驟,並假定這些功能步驟已經實現
-
我們只需把把各個函數組織在一起即可解決這一問題
-
在組織好整個功能後,我們在分別實現各個方法函數
/**
* 1、交易信息開始於一串標準ASCII字符串。
* 2、這個信息字符串必須轉換成一個字符串的數組,數組存放的此次交易的領域語言中所包含的詞彙元素(token)。
* 3、每一個詞彙必須標準化。
* 4、包含超過150個詞彙元素的交易,應該採用不同於小型交易的方式(不同的算法)來提交,以提高效率。
* 5、如果提交成功,API返回”true”;失敗,則返回”false”。
*/
public class Transaction {
public Boolean commit(String command) {
Boolean result = true;
String[] tokens = tokenize(command);
normalizeTokens(tokens);
if (isALargeTransaction(tokens)) {
result = processLargeTransaction(tokens);
} else {
result = processSmallTransaction(tokens);
}
return result;
}
}
以函數對象取代函數
將函數放進一個單獨對象中,如此一來局部變量就變成了對象內的字段。然後你可以在同一個對象中將這個大型函數分解爲多個小型函數。
引入參數對象
方法參數比較多時,將參數封裝爲參數對象
移除對參數的賦值
public int discount(int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;
if (quantity > 100) inputVal -= 1;
if (yearToDate > 10000) inputVal -= 4;
return inputVal;
}
public int discount(int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;
if (quantity > 100) result -= 1;
if (yearToDate > 10000) result -= 4;
return result;
}
將查詢與修改分離
任何有返回值的方法,都不應該有副作用
-
不要在 convert 中調用寫操作,避免副作用
-
常見的例外:將查詢結果緩存到本地
移除不必要臨時變量
臨時變量僅使用一次或者取值邏輯成本很低的情況下
引入解釋性變量
將複雜表達式(或其中一部分)的結果放進一個臨時變量,以此變量名稱來解釋表達式用途
if ((platform.toUpperCase().indexOf("MAC") > -1)
&& (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize > 0) {
// do something
}
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
// do something
}
使用衛語句替代嵌套條件判斷
把複雜的條件表達式拆分成多個條件表達式,減少嵌套。嵌套了好幾層的 if - then-else 語句,轉換爲多個 if 語句
//未使用衛語句
public void getHello(int type) {
if (type == 1) {
return;
} else {
if (type == 2) {
return;
} else {
if (type == 3) {
return;
} else {
setHello();
}
}
}
}
//使用衛語句
public void getHello(int type) {
if (type == 1) {
return;
}
if (type == 2) {
return;
}
if (type == 3) {
return;
}
setHello();
}
使用多態替代條件判斷斷
當存在這樣一類條件表達式,它根據對象類型的不同選擇不同的行爲。可以將這種表達式的每個分支放進一個子類內的複寫函數中,然後將原始函數聲明爲抽象函數。
public int calculate(int a, int b, String operator) {
int result = Integer.MIN_VALUE;
if ("add".equals(operator)) {
result = a + b;
} else if ("multiply".equals(operator)) {
result = a * b;
} else if ("divide".equals(operator)) {
result = a / b;
} else if ("subtract".equals(operator)) {
result = a - b;
}
return result;
}
當出現大量類型檢查和判斷時,if else(或 switch)語句的體積會比較臃腫,這無疑降低了代碼的可讀性。 另外,if else(或 switch)本身就是一個 “變化點”,當需要擴展新的類型時,我們不得不追加 if else(或 switch)語句塊,以及相應的邏輯,這無疑降低了程序的可擴展性,也違反了面向對象的開閉原則。
基於這種場景,我們可以考慮使用 “多態” 來代替冗長的條件判斷,將 if else(或 switch)中的 “變化點” 封裝到子類中。這樣,就不需要使用 if else(或 switch)語句了,取而代之的是子類多態的實例,從而使得提高代碼的可讀性和可擴展性。很多設計模式使用都是這種套路,比如策略模式、狀態模式。
public interface Operation {
int apply(int a, int b);
}
public class Addition implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}
public class OperatorFactory {
private final static Map<String, Operation> operationMap = new HashMap<>();
static {
operationMap.put("add", new Addition());
operationMap.put("divide", new Division());
// more operators
}
public static Operation getOperation(String operator) {
return operationMap.get(operator);
}
}
public int calculate(int a, int b, String operator) {
if (OperatorFactory .getOperation == null) {
throw new IllegalArgumentException("Invalid Operator");
}
return OperatorFactory .getOperation(operator).apply(a, b);
}
使用異常替代返回錯誤碼
非正常業務狀態的處理,使用拋出異常的方式代替返回錯誤碼
-
不要使用異常處理用於正常的業務流程控制
-
異常處理的性能成本非常高
-
儘量使用標準異常
-
避免在 finally 語句塊中拋出異常
-
如果同時拋出兩個異常,則第一個異常的調用棧會丟失
-
finally 塊中應只做關閉資源這類的事情
//使用錯誤碼
public boolean withdraw(int amount) {
if (balance < amount) {
return false;
} else {
balance -= amount;
return true;
}
}
//使用異常
public void withdraw(int amount) {
if (amount > balance) {
throw new IllegalArgumentException("amount too large");
}
balance -= amount;
}
引入斷言
某一段代碼需要對程序狀態做出某種假設,以斷言明確表現這種假設。
-
不要濫用斷言,不要使用它來檢查 “應該爲真” 的條件,只使用它來檢查 “一定必須爲真” 的條件
-
如果斷言所指示的約束條件不能滿足,代碼是否仍能正常運行?如果可以就去掉斷言
引入 Null 對象或特殊對象
當使用一個方法返回的對象時,而這個對象可能爲空,這個時候需要對這個對象進行操作前,需要進行判空,否則就會報空指針。當這種判斷頻繁的出現在各處代碼之中,就會影響代碼的美觀程度和可讀性,甚至增加 Bug 的幾率。
空引用的問題在 Java 中無法避免,但可以通過代碼編程技巧(引入空對象)來改善這一問題。
//空對象的例子
public class OperatorFactory {
static Map<String, Operation> operationMap = new HashMap<>();
static {
operationMap.put("add", new Addition());
operationMap.put("divide", new Division());
// more operators
}
public static Optional<Operation> getOperation(String operator) {
return Optional.ofNullable(operationMap.get(operator));
}
}
public int calculate(int a, int b, String operator) {
Operation targetOperation = OperatorFactory.getOperation(operator)
.orElseThrow(() -> new IllegalArgumentException("Invalid Operator"));
return targetOperation.apply(a, b);
}
//特殊對象的例子
public class InvalidOp implements Operation {
@Override
public int apply(int a, int b) {
throw new IllegalArgumentException("Invalid Operator");
}
}
提煉類
根據單一職責原則,一個類應該有明確的責任邊界。但在實際工作中,類會不斷的擴展。當給某個類添加一項新責任時,你會覺得不值得分離出一個單獨的類。於是,隨着責任不斷增加,這個類包含了大量的數據和函數,邏輯複雜不易理解。
此時你需要考慮將哪些部分分離到一個單獨的類中,可以依據高內聚低耦合的原則。如果某些數據和方法總是一起出現,或者某些數據經常同時變化,這就表明它們應該放到一個類中。另一種信號是類的子類化方式:如果你發現子類化隻影響類的部分特性,或者類的特性需要以不同方式來子類化,這就意味着你需要分解原來的類。
//原始類
public class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String getName() {
return name;
}
public String getTelephoneNumber() {
return ("(" + officeAreaCode + ")" + officeNumber);
}
public String getOfficeAreaCode() {
return officeAreaCode;
}
public void setOfficeAreaCode(String arg) {
officeAreaCode = arg;
}
public String getOfficeNumber() {
return officeNumber;
}
public void setOfficeNumber(String arg) {
officeNumber = arg;
}
}
//新提煉的類(以對象替換數據值)
public class TelephoneNumber {
private String areaCode;
private String number;
public String getTelephnoeNumber() {
return ("(" + getAreaCode() + ")" + number);
}
String getAreaCode() {
return areaCode;
}
void setAreaCode(String arg) {
areaCode = arg;
}
String getNumber() {
return number;
}
void setNumber(String arg) {
number = arg;
}
}
組合優先於繼承
繼承使實現代碼重用的有力手段,但這並非總是完成這項工作的最佳工具,使用不當會導致軟件變得很脆弱。與方法調用不同的是,繼承打破了封裝性。子類依賴於其父類中特定功能的實現細節,如果父類的實現隨着發行版本的不同而變化,子類可能會遭到破壞,即使他的代碼完全沒有改變。
舉例說明,假設有一個程序使用 HashSet,爲了調優該程序的性能,需要統計 HashSet 自從它創建以來添加了多少個元素。爲了提供該功能,我們編寫一個 HashSet 的變體。
// Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedHashSet() { }
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
通過在新的類中增加一個私有域,它引用現有類的一個實例,這種設計被稱爲組合,因爲現有的類變成了新類的一個組件。這樣得到的類將會非常穩固,它不依賴現有類的實現細節。即使現有的類添加了新的方法,也不會影響新的類。許多設計模式使用就是這種套路,比如代理模式、裝飾者模式
// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
@Override
public int size() { return s.size(); }
@Override
public boolean isEmpty() { return s.isEmpty(); }
@Override
public boolean contains(Object o) { return s.contains(o); }
@Override
public Iterator<E> iterator() { return s.iterator(); }
@Override
public Object[] toArray() { return s.toArray(); }
@Override
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override
public boolean add(E e) { return s.add(e); }
@Override
public boolean remove(Object o) { return s.remove(o); }
@Override
public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
@Override
public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
@Override
public boolean retainAll(Collection<?> c) { return s.retainAll(c); }
@Override
public boolean removeAll(Collection<?> c) { return s.removeAll(c); }
@Override
public void clear() { s.clear(); }
}
// Wrappter class - uses composition in place of inheritance
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedHashSet1(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
繼承與組合如何取捨
-
只有當子類真正是父類的子類型時,才適合繼承。對於兩個類 A 和 B,只有兩者之間確實存在 “is-a” 關係的時候,類 B 才應該繼承 A;
-
在包的內部使用繼承是非常安全的,子類和父類的實現都處在同一個程序員的控制之下;
-
對於專門爲了繼承而設計並且具有很好的文檔說明的類來說,使用繼承也是非常安全的;
-
其他情況就應該優先考慮組合的方式來實現
接口優於抽象類
Java 提供了兩種機制,可以用來定義允許多個實現的類型:接口和抽象類。自從 Java8 爲接口增加缺省方法(default method),這兩種機制都允許爲實例方法提供實現。主要區別在於,爲了實現由抽象類定義的類型,類必須稱爲抽象類的一個子類。因爲 Java 只允許單繼承,所以用抽象類作爲類型定義受到了限制。
接口相比於抽象類的優勢:
-
現有的類可以很容易被更新,以實現新的接口。
-
接口是定義混合類型(比如 Comparable)的理想選擇。
-
接口允許構造非層次結構的類型框架。
接口雖然提供了缺省方法,但接口仍有有以下侷限性:
-
接口的變量修飾符只能是 public static final 的
-
接口的方法修飾符只能是 public 的
-
接口不存在構造函數,也不存在 this
-
可以給現有接口增加缺省方法,但不能確保這些方法在之前存在的實現中都能良好運行。
-
因爲這些默認方法是被注入到現有實現中的,它們的實現者並不知道,也沒有許可
接口缺省方法的設計目的和優勢在於:
爲了接口的演化
- Java 8 之前我們知道,一個接口的所有方法其子類必須實現(當然,這個子類不是一個抽象類),但是 java 8 之後接口的默認方法可以選擇不實現,如上的操作是可以通過編譯期編譯的。這樣就避免了由 Java 7 升級到 Java 8 時項目編譯報錯了。Java8 在覈心集合接口中增加了許多新的缺省方法,主要是爲了便於使用 lambda。
可以減少第三方工具類的創建
- 例如在 List 等集合接口中都有一些默認方法,List 接口中默認提供 replaceAll(UnaryOperator)、sort(Comparator)、、spliterator() 等默認方法,這些方法在接口內部創建,避免了爲了這些方法而專門去創建相應的工具類。
可以避免創建基類
- 在 Java 8 之前我們可能需要創建一個基類來實現代碼複用,而默認方法的出現,可以不必要去創建基類。
由於接口的侷限性和設計目的的不同,接口並不能完全替換抽象類。但是通過對接口提供一個抽象的骨架實現類,可以把接口和抽象類的優點結合起來。 接口負責定義類型,或許還提供一些缺省方法,而骨架實現類則負責實現除基本類型接口方法之外,剩下的非基本類型接口方法。擴展骨架實現佔了實現接口之外的大部分工作。這就是模板方法(Template Method)設計模式。
Image [5].png
接口 Protocol:定義了 RPC 協議層兩個主要的方法,export 暴露服務和 refer 引用服務
抽象類 AbstractProtocol:封裝了暴露服務之後的 Exporter 和引用服務之後的 Invoker 實例,並實現了服務銷燬的邏輯
具體實現類 XxxProtocol:實現 export 暴露服務和 refer 引用服務具體邏輯
優先考慮泛型
聲明中具有一個或者多個類型參數(type parameter)的類或者接口,就是泛型(generic)類或者接口。泛型類和接口統稱爲泛型(generic type)。泛型從 Java 5 引入,提供了編譯時類型安全檢測機制。泛型的本質是參數化類型,通過一個參數來表示所操作的數據類型,並且可以限制這個參數的類型範圍。泛型的好處就是編譯期類型檢測,避免類型轉換。
// 比較三個值並返回最大值
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x;
// 假設x是初始最大值
if ( y.compareTo( max ) > 0 ) {
max = y; //y 更大
} if ( z.compareTo( max ) > 0 ) {
max = z; // 現在 z 更大
} return max; // 返回最大對象
}
public static void main( String args[] ) {
System.out.printf( "%d, %d 和 %d 中最大的數爲 %d\n\n", 3, 4, 5, maximum( 3, 4, 5 ));
System.out.printf( "%.1f, %.1f 和 %.1f 中最大的數爲 %.1f\n\n", 6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ));
System.out.printf( "%s, %s 和 %s 中最大的數爲 %s\n","pear", "apple", "orange", maximum( "pear", "apple", "orange" ) );
}
不要使用原生態類型
由於爲了保持 Java 代碼的兼容性,支持和原生態類型轉換,並使用擦除機制實現的泛型。但是使用原生態類型就會失去泛型的優勢,會受到編譯器警告。
要儘可能地消除每一個非受檢警告
每一條警告都表示可能在運行時拋出 ClassCastException 異常。要盡最大的努力去消除這些警告。如果無法消除但是可以證明引起警告的代碼是安全的,就可以在儘可能小的範圍中,使用@SuppressWarnings("unchecked") 註解來禁止警告,但是要把禁止的原因記錄下來。
利用有限制通配符來提升 API 的靈活性
參數化類型不支持協變的,即對於任何兩個不同的類型 Type1 和 Type2 而言,List 既不是 List 的子類型,也不是它的超類。爲了解決這個問題,提高靈活性,Java 提供了一種特殊的參數化類型,稱作有限制的通配符類型,即 List<? extends E> 和 List<? super E>。使用原則是 producer-extends,consumer-super(PECS)。如果即是生產者,又是消費者,就沒有必要使用通配符了。
還有一種特殊的無限制通配符 List<?>,表示某種類型但不確定。常用作泛型的引用,不可向其添加除 Null 以外的任何對象。
//List<? extends E>
// Number 可以認爲 是Number 的 "子類"
List<? extends Number> numberArray = new ArrayList<Number>();
// Integer 是 Number 的子類
List<? extends Number> numberArray = new ArrayList<Integer>();
// Double 是 Number 的子類
List<? extends Number> numberArray = new ArrayList<Double>();
//List<? super E>
// Integer 可以認爲是 Integer 的 "父類"
List<? super Integer> array = new ArrayList<Integer>();、
// Number 是 Integer 的 父類
List<? super Integer> array = new ArrayList<Number>();
// Object 是 Integer 的 父類
List<? super Integer> array = new ArrayList<Object>();
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
靜態成員類優於非靜態成員類
嵌套類(nested class)是指定義在另一個類的內部的類。嵌套類存在的目的只是爲了它的外部類提供服務,如果其他的環境也會用到的話,應該成爲一個頂層類(top-level class)。 嵌套類有四種:靜態成員類(static member class)、非靜態成員類(nonstatic member class)、匿名類(anonymous class)和 局部類(local class)。除了第一種之外,其他三種都稱爲內部類(inner class)。
匿名類(anonymous class)
沒有名字,聲明的同時進行實例化,只能使用一次。當出現在非靜態的環境中,會持有外部類實例的引用。通常用於創建函數對象和過程對象,不過現在會優先考慮 lambda。
局部類(local class)
任何可以聲明局部變量的地方都可以聲明局部類,同時遵循同樣的作用域規則。跟匿名類不同的是,有名字可以重複使用。不過實際很少使用局部類。
靜態成員類(static member class)
最簡單的一種嵌套類,聲明在另一個類的內部,是這個類的靜態成員,遵循同樣的可訪問性規則。常見的用法是作爲公有的輔助類,只有與它的外部類一起使用纔有意義。
非靜態成員類(nonstatic member class)
儘管語法上,跟靜態成員類的唯一區別就是類的聲明不包含 static,但兩者有很大的不同。非靜態成員類的每個實例都隱含地與外部類的實例相關聯,可以訪問外部類的成員屬性和方法。另外必須先創建外部類的實例之後才能創建非靜態成員類的實例。
總而言之,這四種嵌套類都有自己的用途。假設這個嵌套類屬於一個方法的內部,如果只需要在一個地方創建實例,並且已經有了一個預置的類型可以說明這個類的特徵,就要把它做成匿名類。如果一個嵌套類需要在單個方法之外仍然可見,或者它太長了,不適合放在方法內部,就應該使用成員類。如果成員類的每個實例都需要一個指向其外圍實例的引用,就要把成員類做成非靜態的,否則就做成靜態的。
優先使用模板 / 工具類
通過對常見場景的代碼邏輯進行抽象封裝,形成相應的模板工具類,可以大大減少重複代碼,專注於業務邏輯,提高代碼質量。
分離對象的創建與使用
面向對象編程相對於面向過程,多了實例化這一步,而對象的創建必須要指定具體類型。我們常見的做法是 “哪裏用到,就在哪裏創建”,使用實例和創建實例的是同一段代碼。這似乎使代碼更具有可讀性,但是某些情況下造成了不必要的耦合。
public class BusinessObject {
public void actionMethond {
//Other things
Service myServiceObj = new Service();
myServiceObj.doService();
//Other things
}
}
public class BusinessObject {
public void actionMethond {
//Other things
Service myServiceObj = new ServiceImpl();
myServiceObj.doService();
//Other things
}
}
public class BusinessObject {
private Service myServiceObj;
public BusinessObject(Service aService) {
myServiceObj = aService;
}
public void actionMethond {
//Other things
myServiceObj.doService();
//Other things
}
}
public class BusinessObject {
private Service myServiceObj;
public BusinessObject() {
myServiceObj = ServiceFactory;
}
public void actionMethond {
//Other things
myServiceObj.doService();
//Other things
}
}
對象的創建者耦合的是對象的具體類型,而對象的使用者耦合的是對象的接口。也就是說,創建者關心的是這個對象是什麼,而使用者關心的是它能幹什麼。這兩者應該視爲獨立的考量,它們往往會因爲不同的原因而改變。
當對象的類型涉及多態、對象創建複雜(依賴較多)可以考慮將對象的創建過程分離出來,使得使用者不用關注對象的創建細節。設計模式中創建型模式的出發點就是如此,實際項目中可以使用工廠模式、構建器、依賴注入的方式。
可訪問性最小化
區分一個組件設計得好不好,一個很重要的因素在於,它對於外部組件而言,是否隱藏了其內部數據和實現細節。Java 提供了訪問控制機制來決定類、接口和成員的可訪問性。實體的可訪問性由該實體聲明所在的位置,以及該實體聲明中所出現的訪問修飾符(private、protected、public)共同決定的。
對於頂層的(非嵌套的)類和接口,只有兩種的訪問級別:包級私有的(沒有 public 修飾)和公有的(public 修飾)。
對於成員(實例 / 域、方法、嵌套類和嵌套接口)由四種的訪問級別,可訪問性如下遞增:
-
私有的(private 修飾)-- 只有在聲明該成員的頂層類內部纔可以訪問這個成員;
-
包級私有的(默認)-- 聲明該成員的包內部的任何類都可以訪問這個成員;
-
受保護的(protected 修飾)-- 聲明該成員的類的子類可以訪問這個成員,並且聲明該成員的包內部的任何類也可以訪問這個成員;
-
公有的(public 修飾)-- 在任何地方都可以訪問該成員;
正確地使用這些修飾符對於實現信息隱藏是非常關鍵的,原則就是:儘可能地使每個類和成員不被外界訪問(私有或包級私有)。這樣好處就是在以後的發行版本中,可以對它進行修改、替換或者刪除,而無須擔心會影響現有的客戶端程序。
-
如果類或接口能夠做成包級私有的,它就應該被做成包級私有的;
-
如果一個包級私有的頂層類或接口只是在某一個類的內部被用到,就應該考慮使它成爲那個類的私有嵌套類;
-
公有類不應直接暴露實例域,應該提供相應的方法以保留將來改變該類的內部表示法的靈活性;
-
當確定了類的公有 API 之後,應該把其他的成員都變成私有的;
-
如果同一個包下的類之間存在比較多的訪問時,就要考慮重新設計以減少這種耦合;
可變性最小化
不可變類是指其實例不能被修改的類。每個實例中包含的所有信息都必須在創建該實例時提供,並在對象的整個生命週期內固定不變。不可變類好處就是簡單易用、線程安全、可自由共享而不容易出錯。Java 平臺類庫中包含許多不可變的類,比如 String、基本類型包裝類、BigDecimal 等。
爲了使類成爲不可變,要遵循下面五條規則:
-
聲明所有的域都是私有的
-
聲明所有的域都是 final 的
-
如果一個指向新創建實例的引用在缺乏同步機制的情況下,從一個線程被傳遞到另一個線程,就必須確保正確的行爲
-
不提供任何會修改對象狀態的方法
-
保證類不會被擴展(防止子類化,類聲明爲 final)
-
防止粗心或者惡意的子類假裝對象的狀態已經改變,從而破壞該類的不可變行爲
-
確保對任何可變組件的互斥訪問
-
如果類具有指向可變對象的域,則必須確保該類的客戶端無法獲得指向這些對象的引用。並且,永遠不要用客戶端提供的對象引用來初始化這樣的域,也不要從任何訪問方法中返回該對象引用。在構造器、訪問方法和 readObject 方法中使用保護性拷貝技術
可變性最小化的一些建議:
-
除非有很好的理由要讓類成爲可變的類,否則它就應該是不可變的;
-
如果類不能被做成不可變的,仍然應該儘可能地限制它的可變性;
-
除非有令人信服的理由要使域變成非 final 的,否則要使每個域都是 private final 的;
-
構造器應該創建完全初始化的對象,並建立起所有的約束關係;
質量如何保證
測試驅動開發
測試驅動開發(TDD)要求以測試作爲開發過程的中心,要求在編寫任何代碼之前,首先編寫用於產碼行爲的測試,而編寫的代碼又要以使測試通過爲目標。TDD 要求測試可以完全自動化地運行,並在對代碼重構前後必須運行測試。
TDD 的最終目標是整潔可用的代碼(clean code that works)。大多數的開發者大部分時間無法得到整潔可用的代碼。辦法是分而治之。首先解決目標中的 “可用” 問題,然後再解決 “代碼的整潔” 問題。這與體系結構驅動(architecture-driven)的開發相反。
採用 TDD 另一個好處就是讓我們擁有一套伴隨代碼產生的詳盡的自動化測試集。將來無論出於任何原因(需求、重構、性能改進)需要對代碼進行維護時,在這套測試集的驅動下工作,我們代碼將會一直是健壯的。
TDD 的開發週期
Image [6].png
添加一個測試 -> 運行所有測試並檢查測試結果 -> 編寫代碼以通過測試 -> 運行所有測試且全部通過 -> 重構代碼,以消除重複設計,優化設計結構
兩個基本的原則
-
僅在測試失敗時才編寫代碼並且只編寫剛好使測試通過的代碼
-
編寫下一個測試之前消除現有的重複設計,優化設計結構
關注點分離是這兩條規則隱含的另一個非常重要的原則。其表達的含義指在編碼階段先達到代碼 “可用” 的目標,在重構階段再追求 “整潔” 目標,每次只關注一件事!
分層測試點
參考資料
-
重構 - 改善既有代碼的設計
-
設計模式
-
Effective Java
-
敏捷軟件開發與設計的最佳實踐
-
實現模式
-
測試驅動開發
轉自:Vector
鏈接:Jinjuejin.cn/post/6954378167947624484
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/krchMcXeHyzzfOCQg3y68g