程式碼的壞味道:可變的資料

LeaveStyle發表於2021-04-23

一、背景

以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(); ```

四、總結

編寫程式碼時,時刻提醒自己:控制資料的可變性 ??????????????

本文內容參考來源於:極客時間專欄《軟體設計之美》《程式碼之醜》 | 書籍《重構

相關文章