程式碼的壞味道:可變的資料
一、背景
以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(); ```
四、總結
編寫程式碼時,時刻提醒自己:控制資料的可變性 ??????????????
相關文章
- 程式碼的壞味道「上」
- 優化程式碼中的“壞味道”優化
- 程式碼的壞味道和重構
- 程式碼壞味道之非必要的
- 消滅 Java 程式碼的“壞味道”Java
- 程式碼壞味道之程式碼臃腫
- 重構:幹掉有壞味道的程式碼
- 消除程式碼中的壞味道,編寫高質量程式碼
- 程式碼壞味道之濫用物件導向物件
- 用 set或map修復if-else的壞味道 - egkatzioura
- 七牛李道兵談“架構壞味道”架構
- 想要寫出好味道的程式碼,你需要養成這些好習慣!
- 命名&可閱讀的程式碼
- 系統慢慢變壞的邏輯
- 可變資料型別(mutable)與不可變資料型別(immutable)總結資料型別
- 程式碼質量第 3 層 - 可讀的程式碼
- 程式碼質量第 2 層 - 可重用的程式碼
- 【小心】快停止這6種讓Python程式變慢的壞習慣!Python
- 關於C99可變引數巨集的例項程式碼講解
- 可變資料型別不能作為python函式的引數資料型別Python函式
- Metacat:讓Netflix的大資料變得可發現且有意義大資料
- 可落地的DDD程式碼實踐
- Python基礎(一)可變與不可變資料型別Python資料型別
- 圖資料庫 Nebula Graph 的程式碼變更測試覆蓋率實踐資料庫
- “服務可達的資料鏈DNA” ,打通從程式碼到使用者的“任督二脈”
- u盤檔案損壞怎麼恢復資料 u盤恢復損壞資料的有效方法
- Redis 的基礎資料結構(一) 可變字串、連結串列、字典Redis資料結構字串
- 【資料庫資料恢復】MongoDB資料庫檔案損壞的資料恢復案例資料庫資料恢復MongoDB
- 將資料、程式碼、棧放入不同的段
- 資料底層損壞的恢復方法—拼碎片恢復資料
- 多變數資料協同可視探索框架變數框架
- 【伺服器資料恢復】IBM儲存伺服器硬碟壞道離線、oracle資料庫損壞的資料恢復伺服器資料恢復IBM硬碟Oracle資料庫
- InterBase資料庫檔案損壞的修復方法資料庫
- 乾淨的程式碼: 編寫可讀的函式函式
- 使用JSDoc提高程式碼的可讀性JS
- 如何提高程式碼的可維護性
- 如何提高程式碼的可測試性
- php 刪除資料夾的實現程式碼PHP