程式碼的壞味道:可變的資料
一、背景
以Java語言為例,說到可變的資料,就要提到函數語言程式設計,函數語言程式設計主要有以下概念: * 純函式(Pure Function) * 頭等函式和高階函式(First-Class and High-Order functions) * 不可變性(Immutability) * 引用透明性(Referential Transparency)
Java作為程式語言的老大哥之一,是在JDK8的時候引入了函數語言程式設計,java是一門物件導向的程式語言,在以前呼叫函式的時候總是需要依賴於一個物件,經常會寫出匿名類這樣的程式碼:
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
};
JDK8中引入了函數語言程式設計介面和Lambda來簡化程式碼:
Runnable runnable = () -> System.out.println("Hello World");
不可變性是函數語言程式設計推崇的一個重要概念,保證資料的不可變性,從而可以讓我們:開發更加簡單、可回溯、測試友好,以及減少了任何可能的副作用,從而減少了Bug的出現。 但是JDK8對函數語言程式設計的支援還不夠完善,比如Collector的toXXX缺少生成不可變的集合,各種集合想要初始化一個不可變的物件也比較繁瑣。當然這些問題在JDK11版本中得到了大幅度改進,不僅支援了型別推斷,而且還支援了各種不可變物件的初始化,極大的簡化了程式碼,比如: ``` // JDK7的常規初始化 ---------- 可變集合 List list = new ArrayList<>(); list.add("test01"); list.add("test02"); list.add("test03");
// JDK7的匿名內部類初始化 --- 可變集合 List list = new ArrayList<>() {{ add("test01"); add("test02"); add("test03"); }};
// JDK8的Stream初始化 ------ 可變集合 List list = Stream.of("test01", "test02", "test03").collect(Collectors.toList());
// JDK11的.of初始化 -------- 不可變集合 var list = List.of("test01", "test02", "test03");
// JDK11的stream初始化 ----- 不可變集合 var list = Stream.of("test01", "test02", "test03").collect(Colleactors.toUnmodifiableList());
// 藉助工具類:Arrays ------- 不可變集合 List list = new ArrayList<>(Arrays.asList("test01", "test02", "test03"));
// 藉助工具類:Collections List readList = Collections.unmodifiableList(list);
// 藉助工具類:Guava ImmutableList list = ImmutableList.of("test01", "test02", "test03"); ``` 通過上面各種集合初始化的對比,相信你也能發現,JDK11對不可變性的支援也日益完善,函數語言程式設計的很多優秀的特性在java語言得到了實現,所以還在用jdk8的小夥伴還是儘早升級,要不然jdk17都要出來了。
二、不可控的可變資料
在第一版java語言的《重構》書中,還沒有發現可變資料的影子,也難怪,這本書是在2010年出版的,jdk8是在2012年第一次釋出,但是隨著函數語言程式設計在這些高階語言中的應用,在2019年第二版js語言的《重構》書中,在程式碼的壞味道中會發現多了可變資料
這一條。
下面介紹兩種好用的重構手法,來避免不可控的可變資料為我們帶來的麻煩。
1. 移除設定函式(Remove Setting Method)
和讀資料相比,修改資料是一項危險的操作,這也就是為什麼在併發程式設計中會有各種複雜的鎖機制來保證資料的一致性。對於專案中的Model來說,setter方法就是其對外暴露不可控因素的源頭,其實我們完全可以避免使用setter,通過不可變的方式來替代。詳情可見3.1的程式碼樣例部分。
2. 編寫不可變類
Java中最典型的不可變類就是String類,裡面的各種方法,只要涉及到字串的變化,不會再原字串上進行修改,而是生成一個新的字串返回。 想要編寫不可變類,也不難,只要做到以下三點: * 所有欄位只在建構函式中初始化 * 若發生改變,就返回一個新物件 * 程式設計純函式
三、程式碼案例,如何避免程式碼中的可變性
接下來的程式碼都以jdk11版本的語法為例,部分程式碼為虛擬碼只為說明邏輯:
1. 基本資料型別、包裝類和String
基本資料型別 :int、long、float、double、byte、short、boolean、char 包裝類 :Integer、Long、Float、Double、Byte、Short、Boolean、Character String 本身是不可變類
// 在使用以上資料型別申請變數時, 應該儘量避免對同一個變數反覆賦值
int i = toResult();
....
i = toAnotherResult();
// toAnotherResult()應該重新申請一個變數,不應該對以前的變數進行覆蓋,並且不必要的變數應該進行Inline操作
還有一種情況在開發的時候會經常遇到:
```
// 第一種情況,if中只有一行賦值程式碼
String s1;
if(isRight(xxx)) {
s1 = "test_01";
} else {
s1 = "test_02";
}
use String s1 do something ...
// 第二種情況, if中內嵌了多行程式碼
String s2;
if(isRight(xxx)) {
do something ...
s1 = "test_01";
} else {
do something ...
s1 = "test_02";
}
use String s2 do something ...
上面這種情況應該在初始化的時候就給變數賦值。
// 第一種情況可以使用三目運算子來解決:
String s1 = isRight(xxx) ? "test_01" : "test_02";
// 第二種情況可以使用Extract Method(提煉函式)來解決: String s2 = toStr(xxx);
private String toStr(xxx) { //使用衛語句簡化if-else結構 if(isRight(xxx)) { do something ... return "test_01"; } do something ... return "test_02"; }
```
2. 構建不可變的集合
集合型別 :List、Map、Set, 下面List為例來說明
下列情況應當避免: ``` // 避免: 初始化可變列表 List list = new ArrayList<>() {{ add("test01"); add("test02"); add("test03"); }};
// 避免: 在一個方法中改變引數列表的長度 public void change(List list) { list.add("test04"); }
// 避免: 在一個方法中改變引數列表的內部值
public void fill(List models) {
models.foreach(model -> model.setType("new_model"));
}
構建不可變的列表:
// 初始化
var list_01 = List.of("test01", "test02", "test03", "");
var list_02 = List.of("test04", "test05", "test06", "");
// 通過stream來實現列表的合併和過濾,建立一個新的不可變集合 // 兩個集合合併 var list_03 = Stream.concat(list_01.stream(), list_02.stream()).collect(Collectors.toUnmodifiableList()); // 多個集合合併 var list_04 = Stream.of(list1.stream(), list2.stream(), list3.stream()).flatMap(Function.identity()).collect(Collectors.toUnmodifiableList()); // 集合過濾 var list_05 = list_04.stream().filter(StringUtils::isNotEmpty).collect(Collectors.toUnmodifiableList());
// 集合改變內部的值生成一個新的集合,這裡的model代表一個虛擬的物件 var models = List.of(model1, model2, model3); var newModels = models.stream().map(model -> model.withType("new_model")).collect(Collectors.toUnmodifiableList()); ``` 總之: 集合搭配Stream可以進行任何變化生成新的不可變集合,沒有副作用,非常的nice。
3. 構建不可變的model
@Setter是導致model可變的罪魁禍首,其實我們完全可以不使用setter來構建我們的model,可以在lombok中把setter相關的禁用掉。
lombok.setter.flagUsage = error
lombok.data.flagUsage = error
我們可以用以下注釋來構建不可見的model,並且通過staticName = "of"讓model的構建更加函式式化。
```
// 宣告
@With
@Getter
@Builder(toBuilder = true)
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor(staticName = "of")
public class Model() {
private String id;
private String name;
private String type;
}
// 構建Model var model_01 = Model.of("101", "model_01", "model");
// 構建空Model var model_02 = Model.of();
// 構建指定引數的Model var model_03 = Moder.toBuilder().id("301").name("model_03").build();
// 修改Model的一個值,通過@With來生成一個全新的model var model_04 = model_01.withName("model_04");
// 修改多個值,通過@Builder來生成一個全新的model var model_05 = model_01.toBuilder.name("model_05").type("new_model").build(); ```
四、總結
編寫程式碼時,時刻提醒自己:控制資料的可變性 ??????????????
相關文章
- 程式碼的壞味道「上」
- 22種程式碼的壞味道
- 消滅 Java 程式碼的“壞味道”Java
- 優化程式碼中的“壞味道”優化
- 程式碼壞味道之非必要的
- 程式碼的壞味道和重構
- 21種程式碼的“壞味道” (轉)
- 幹掉你程式碼中的壞味道
- 程式碼壞味道之程式碼臃腫
- 重構:幹掉有壞味道的程式碼
- 怎麼消除JavaScript中的程式碼壞味道JavaScript
- 消除程式碼中的壞味道,編寫高質量程式碼
- 找出那些程式碼裡的壞味道吧——《重構》筆記筆記
- 程式碼壞味道之濫用物件導向物件
- 程式碼的味道 (轉)
- 軟體管理中的壞味道薦
- [譯] 再談 CSS 中的程式碼味道CSS
- 七牛李道兵談“架構壞味道”架構
- javascript判斷變數的資料型別程式碼例項JavaScript變數資料型別
- MyBatis資料持久化 SQL複用(可重用的 SQL 程式碼段)MyBatis持久化SQL
- 修復損壞的資料塊
- 想要寫出好味道的程式碼,你需要養成這些好習慣!
- 好程式碼、壞程式碼之二
- 常用的資料庫程式碼資料庫
- var+不可變資料結構 vs val+可變資料結構資料結構
- 可變資料型別(mutable)與不可變資料型別(immutable)總結資料型別
- Oracle資料庫塊的物理損壞與邏輯損壞Oracle資料庫
- 命名&可閱讀的程式碼
- 編寫可讀的程式碼
- ORACLE資料庫壞塊的處理 (處理無物件壞快的方法)Oracle資料庫物件
- Decorator模式有代理的味道模式
- 系統慢慢變壞的邏輯
- 記下改變資料庫密碼的命令資料庫密碼
- 如何提高程式碼的可讀性? - 讀《編寫可讀程式碼的藝術》
- PHP的可變變數名PHP變數
- 程式碼質量第 2 層 - 可重用的程式碼
- 程式碼質量第 3 層 - 可讀的程式碼
- 關於C99可變引數巨集的例項程式碼講解