有些設計不是為了提升效能而出現的,而是為了提升開發者體驗,更容易開發與降低錯誤率而誕生的,以下就來以經典的immutable來介紹。

mutable與immutable

從字面上的意思可以得知mutable是可變動的,而immutable是不可變動的。
因此操作的行為我們可以用mutable與immutable來表示。
這個設計原則是從OOP開始起來的,尤其是在Java上的object操作最為經典。
如果你是第一次聽到這個名詞,我強烈推薦你花一些時間去研究,React官方也強調這個的重要性

OOP的使用

以Java為例,可以大致分為:

  1. Mutable: Date, StringBuilder, StringBuffer…
  2. Immutable: String class, Wrapper classes, int, float…

Mutable可以在初始化之後,透過methods來改變物件,例如:getter與setter,因為可以透過method來改變,因此是unstable或是不可預期的資料。
Immutable建立實體之後是無法改變的,可以透過getter來取得裡面的資訊,由於資料無法被改變,因此內容是stable且可預期的。

為什麼String在Java是Immutable

在Java,String是一個特別的class,由於String是很常被使用的class,常使用的頻率與Promitive Data Type是差不多的,如果今天一個初學者剛學Java,就要學習Object reference的概念,相信這是一個很容易混淆且不易被接受的問題。因此基於某些理由的前提下,String在Java是屬於Immutable的一種。(相信這也解答了以前學Java造成混淆的一些疑惑了)

為什麼要Immutable

簡單幾個字來詮釋:單純,可預期
我們在開發程式的時候,很常因為對相同的資料進行操作而可能造成非預期的行為。
例如開發這個頁面會用到userData:

1
2
3
4
5
6
7
8
9
10
11
12
const userData = {
name: 'Cool',
intro: '...',
avatar: '',
}

handleData(userData);

function handleData(data) {
console.log('data: ', data);
return data;
}

然後今天輪到另一位接手開發程式,他做了這樣的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
const userData = {
name: 'Cool',
intro: '...',
avatar: '',
};

handleData(userData);

function handleData(data) {
console.log('data: ', data);
userData.name='Happy';
return data;
}

之後換到其他人開發的時候發現,奇怪?我畫面的呈現怎麼怪怪的,後端的資料明明是正常的呀?
仔細一看發現:
啊!Magic number!
啊!unpure function!
啊!mutable!!!

這訊息量實在太大了,不好承受呀!
事實上這不是一個很好的操作,因為在中間更改資料這個動作,其實很難在第一時間debug出問題來,特別是這個magic number搭配mutable的操作是很大的傷害。

要改善上面的專案,可以參考以下的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const userData = {
name: 'Cool',
intro: '...',
avatar: '',
};

// updateUserName回傳新物件,而不是更新原本的物件s
const newUserData = updateUserName(userData, name);

function updateUserName(data, name) {
const updatedData = Object.assign({}, {
...data,
name: name,
});
return updatedData;
}

掌握每次更新資料時,都是產生新的物件。
你可能會問:為什麼每次都要產生新的資料?這樣不是很耗資源?空間複雜度是不是更高?
不,這是一個很好的設計,用不到的丟掉,用到的留下,程式語言背後就會幫你處理好背後的事情了。
比起花時間在效能議題上,開發者更需要專注在程式邏輯上的開發,因此可以更容易提升開發體驗快速產出。

Immutable應用在前端上

Immutable的原則很單純,資料被建立的時候即不可改變,那麼我們如何用在JavaScript上呢?

迷:我知道我知道,const也是可以做到的嘛!

對…也不對,只講對了一半,const在面對Primitive Data Type時是很有用的,但是面對Object Data Type時,一切都變了樣。

Object的Mutable例子

  1. Object的操作:
1
2
3
4
5
6
7
const userData = {
name: 'Cool',
intro: '...',
avatar: '',
};

userData.name='xxx';
  1. Object的Array的操作:
1
2
3
const array = [];

array.push('1');

所以const並無法做到完整的Immutable

Functional Programming

如果是build-in的FP methods呢?
有些是Immutable,有些是mutable

Immutable的例子有:map, reduce, slice…,這些都會產生新的物件。
但是像是splice就是屬於mutable了,會修改原本的物件。

可以參考這篇文章整理的參考表格:

copyright by hannahpun

就JS的角度來看,好像有點亂?要注意的地方似乎挺多的,那該如何做呢?
可以參考以下的方法:

  1. 制定專案設計原則
  2. 使用Immutablejs套件

下面來簡單介紹一下可以採用的方式

制定專案設計原則

團隊只要有設計上的共識,基本上就可以歸納出什麼狀況需要做什麼操作。
可以多參考官方文件或是其他套件的撰寫風格引入進來。

也可以參考eslint的定義,來嚴格規定團隊的設計風格:eslint-plugin-immutable

不可接受的方式:

1
2
3
4
5
let users = [ { name: '' } ];

list.forEach((data, index) => {
users[index]['name']=data.name;
});

可以接受的方式:

1
2
3
4
let users = [ { name: '' } ];

users = list.map(data => ({ name: data.name }));

Immutablejs套件

Immutablejs提供了常用的方法,並把他們統一為Immutable的寫法。

讓我們來看一下官方的範例:

1
2
3
4
var map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
var map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50

有發現不一樣的地方嗎?他用map2去接set好的map1。
map1並不會改變物件內的東西,則是產生新的map2。