一、背景
以Java語(yǔ)言為例诵肛,說(shuō)到可變的數(shù)據(jù),就要提到函數(shù)式編程默穴,函數(shù)式編程主要有以下概念:
- 純函數(shù)(Pure Function)
- 頭等函數(shù)和高階函數(shù)(First-Class and High-Order functions)
- 不可變性(Immutability)
- 引用透明性(Referential Transparency)
Java作為編程語(yǔ)言的老大哥之一怔檩,是在JDK8的時(shí)候引入了函數(shù)式編程,java是一門(mén)面向?qū)ο蟮木幊陶Z(yǔ)言蓄诽,在以前調(diào)用函數(shù)的時(shí)候總是需要依賴于一個(gè)對(duì)象薛训,經(jīng)常會(huì)寫(xiě)出匿名類這樣的代碼:
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
};
JDK8中引入了函數(shù)式編程接口和Lambda來(lái)簡(jiǎn)化代碼:
Runnable runnable = () -> System.out.println("Hello World");
不可變性是函數(shù)式編程推崇的一個(gè)重要概念,保證數(shù)據(jù)的不可變性若专,從而可以讓我們:開(kāi)發(fā)更加簡(jiǎn)單许蓖、可回溯、測(cè)試友好调衰,以及減少了任何可能的副作用膊爪,從而減少了Bug的出現(xiàn)。
但是JDK8對(duì)函數(shù)式編程的支持還不夠完善嚎莉,比如Collector的toXXX缺少生成不可變的集合米酬,各種集合想要初始化一個(gè)不可變的對(duì)象也比較繁瑣。當(dāng)然這些問(wèn)題在JDK11版本中得到了大幅度改進(jìn)趋箩,不僅支持了類型推斷赃额,而且還支持了各種不可變對(duì)象的初始化,極大的簡(jiǎn)化了代碼叫确,比如:
// JDK7的常規(guī)初始化 ---------- 可變集合
List<String> list = new ArrayList<>();
list.add("test01");
list.add("test02");
list.add("test03");
// JDK7的匿名內(nèi)部類初始化 --- 可變集合
List<String> list = new ArrayList<>() {{
add("test01");
add("test02");
add("test03");
}};
// JDK8的Stream初始化 ------ 可變集合
List<String> 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<String> list = new ArrayList<>(Arrays.asList("test01", "test02", "test03"));
// 借助工具類:Collections
List<String> readList = Collections.unmodifiableList(list);
// 借助工具類:Guava
ImmutableList<String> list = ImmutableList.of("test01", "test02", "test03");
通過(guò)上面各種集合初始化的對(duì)比跳芳,相信你也能發(fā)現(xiàn),JDK11對(duì)不可變性的支持也日益完善竹勉,函數(shù)式編程的很多優(yōu)秀的特性在java語(yǔ)言得到了實(shí)現(xiàn)飞盆,所以還在用jdk8的小伙伴還是盡早升級(jí),要不然jdk17都要出來(lái)了次乓。
二吓歇、不可控的可變數(shù)據(jù)
在第一版java語(yǔ)言的《重構(gòu)》書(shū)中,還沒(méi)有發(fā)現(xiàn)可變數(shù)據(jù)的影子票腰,也難怪城看,這本書(shū)是在2010年出版的,jdk8是在2012年第一次發(fā)布杏慰,但是隨著函數(shù)式編程在這些高級(jí)語(yǔ)言中的應(yīng)用测柠,在2019年第二版js語(yǔ)言的《重構(gòu)》書(shū)中炼鞠,在代碼的壞味道中會(huì)發(fā)現(xiàn)多了可變數(shù)據(jù)
這一條。
下面介紹兩種好用的重構(gòu)手法鹃愤,來(lái)避免不可控的可變數(shù)據(jù)為我們帶來(lái)的麻煩簇搅。
1. 移除設(shè)置函數(shù)(Remove Setting Method)
和讀數(shù)據(jù)相比,修改數(shù)據(jù)是一項(xiàng)危險(xiǎn)的操作软吐,這也就是為什么在并發(fā)編程中會(huì)有各種復(fù)雜的鎖機(jī)制來(lái)保證數(shù)據(jù)的一致性瘩将。對(duì)于項(xiàng)目中的Model來(lái)說(shuō),setter方法就是其對(duì)外暴露不可控因素的源頭凹耙,其實(shí)我們完全可以避免使用setter姿现,通過(guò)不可變的方式來(lái)替代。詳情可見(jiàn)3.1的代碼樣例部分肖抱。
2. 編寫(xiě)不可變類
Java中最典型的不可變類就是String類备典,里面的各種方法,只要涉及到字符串的變化意述,不會(huì)再原字符串上進(jìn)行修改提佣,而是生成一個(gè)新的字符串返回。
想要編寫(xiě)不可變類荤崇,也不難拌屏,只要做到以下三點(diǎn):
- 所有字段只在構(gòu)造函數(shù)中初始化
- 若發(fā)生改變,就返回一個(gè)新對(duì)象
- 編程純函數(shù)
三术荤、代碼案例倚喂,如何避免代碼中的可變性
接下來(lái)的代碼都以jdk11版本的語(yǔ)法為例,部分代碼為偽代碼只為說(shuō)明邏輯:
1. 基本數(shù)據(jù)類型瓣戚、包裝類和String
基本數(shù)據(jù)類型 :int端圈、long、float子库、double舱权、byte、short仑嗅、boolean刑巧、char
包裝類 :Integer、Long无畔、Float、Double吠冤、Byte浑彰、Short、Boolean拯辙、Character
String 本身是不可變類
// 在使用以上數(shù)據(jù)類型申請(qǐng)變量時(shí), 應(yīng)該盡量避免對(duì)同一個(gè)變量反復(fù)賦值
int i = toResult();
....
i = toAnotherResult();
// toAnotherResult()應(yīng)該重新申請(qǐng)一個(gè)變量郭变,不應(yīng)該對(duì)以前的變量進(jìn)行覆蓋颜价,并且不必要的變量應(yīng)該進(jìn)行Inline操作
還有一種情況在開(kāi)發(fā)的時(shí)候會(huì)經(jīng)常遇到:
// 第一種情況,if中只有一行賦值代碼
String s1;
if(isRight(xxx)) {
s1 = "test_01";
} else {
s1 = "test_02";
}
use String s1 do something ...
// 第二種情況, if中內(nèi)嵌了多行代碼
String s2;
if(isRight(xxx)) {
do something ...
s1 = "test_01";
} else {
do something ...
s1 = "test_02";
}
use String s2 do something ...
上面這種情況應(yīng)該在初始化的時(shí)候就給變量賦值诉濒。
// 第一種情況可以使用三目運(yùn)算符來(lái)解決:
String s1 = isRight(xxx) ? "test_01" : "test_02";
// 第二種情況可以使用Extract Method(提煉函數(shù))來(lái)解決:
String s2 = toStr(xxx);
private String toStr(xxx) {
//使用衛(wèi)語(yǔ)句簡(jiǎn)化if-else結(jié)構(gòu)
if(isRight(xxx)) {
do something ...
return "test_01";
}
do something ...
return "test_02";
}
2. 構(gòu)建不可變的集合
集合類型 :List周伦、Map、Set未荒, 下面List為例來(lái)說(shuō)明
下列情況應(yīng)當(dāng)避免:
// 避免: 初始化可變列表
List<String> list = new ArrayList<>() {{
add("test01");
add("test02");
add("test03");
}};
// 避免: 在一個(gè)方法中改變參數(shù)列表的長(zhǎng)度
public void change(List<String> list) {
list.add("test04");
}
// 避免: 在一個(gè)方法中改變參數(shù)列表的內(nèi)部值
public void fill(List<Model> models) {
models.foreach(model -> model.setType("new_model"));
}
構(gòu)建不可變的列表:
// 初始化
var list_01 = List.of("test01", "test02", "test03", "");
var list_02 = List.of("test04", "test05", "test06", "");
// 通過(guò)stream來(lái)實(shí)現(xiàn)列表的合并和過(guò)濾专挪,創(chuàng)建一個(gè)新的不可變集合
// 兩個(gè)集合合并
var list_03 = Stream.concat(list_01.stream(), list_02.stream()).collect(Collectors.toUnmodifiableList());
// 多個(gè)集合合并
var list_04 = Stream.of(list1.stream(), list2.stream(), list3.stream()).flatMap(Function.identity()).collect(Collectors.toUnmodifiableList());
// 集合過(guò)濾
var list_05 = list_04.stream().filter(StringUtils::isNotEmpty).collect(Collectors.toUnmodifiableList());
// 集合改變內(nèi)部的值生成一個(gè)新的集合,這里的model代表一個(gè)虛擬的對(duì)象
var models = List.of(model1片排, model2寨腔, model3);
var newModels = models.stream().map(model -> model.withType("new_model")).collect(Collectors.toUnmodifiableList());
總之: 集合搭配Stream可以進(jìn)行任何變化生成新的不可變集合,沒(méi)有副作用率寡,非常的nice迫卢。
3. 構(gòu)建不可變的model
@Setter是導(dǎo)致model可變的罪魁禍?zhǔn)祝鋵?shí)我們完全可以不使用setter來(lái)構(gòu)建我們的model冶共,可以在lombok中把setter相關(guān)的禁用掉乾蛤。
lombok.setter.flagUsage = error
lombok.data.flagUsage = error
我們可以用以下注釋來(lái)構(gòu)建不可見(jiàn)的model,并且通過(guò)staticName = "of"讓model的構(gòu)建更加函數(shù)式化捅僵。
// 聲明
@With
@Getter
@Builder(toBuilder = true)
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor(staticName = "of")
public class Model() {
private String id;
private String name;
private String type;
}
// 構(gòu)建Model
var model_01 = Model.of("101", "model_01", "model");
// 構(gòu)建空Model
var model_02 = Model.of();
// 構(gòu)建指定參數(shù)的Model
var model_03 = Moder.toBuilder().id("301").name("model_03").build();
// 修改Model的一個(gè)值家卖,通過(guò)@With來(lái)生成一個(gè)全新的model
var model_04 = model_01.withName("model_04");
// 修改多個(gè)值,通過(guò)@Builder來(lái)生成一個(gè)全新的model
var model_05 = model_01.toBuilder.name("model_05").type("new_model").build();
四命咐、總結(jié)
編寫(xiě)代碼時(shí)篡九,時(shí)刻提醒自己:控制數(shù)據(jù)的可變性 ????????????????????????????
本文內(nèi)容參考來(lái)源于:極客時(shí)間專欄《軟件設(shè)計(jì)之美》《代碼之丑》 | 書(shū)籍《重構(gòu)》