在設計元件的時候,有時候因應專案的需求要不斷的增加新功能,隨著功能的增加,該如何一邊增加功能一邊增強元件的維護性呢?一起來看看吧!

設計元件

一開始開發專案的時候,我們可能會先設計一個元件:

1
2
3
4
5
6
7
8
9
const Button = ({ children, onClick, disabled }) => {
return (
<button
className="btn"
onClick={onClick}
disabled={disabled}
>{children}</button>
);
}

但是後來又其他需求,所以又要多加一些新的功能在同個元件上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const handleWarning = (onWarning, onClick, type, target) => {
if(type === 1) {
onWarning(target);
} else {
onClick(target);
}
}

const Button = ({ children, onClick, disabled, type, onWarning }) => {
const style = text === 1 ? 'btn-primary' : 'btn';
return (
<button
className={style}
onClick={(target) => handleWarning(onWarning, onClick, type, target)}
disabled={disabled}
>{children}</button>
);
}

經過一次擴充component的功能之後,你會發現程式開始變大了,可能會有以下的狀況需要考量:

  1. 日積月累的legacy code難以維護
  2. 每次載入這個元件的運算成本越來越大

如何解決

React官方鼓勵大家使用composition的方式來設計component,特別是在Functional Component的設計模式下,更容易達成。
如果是以上面的例子來說,我們可以怎麼做呢?

初始元件基本上不動,不過可以調整一下樣式的擴充性:

1
2
3
4
5
6
7
8
9
const Button = ({ children, onClick, disabled }) => {
return (
<button
className={`btn ${type ? `btn-${type}` : ''}`}
onClick={onClick}
disabled={disabled}
>{children}</button>
);
}

擴充元件,可以透過組合的方式來達成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const handleWarning = (onWarning, onClick, type, target) => {
if(type === 1) {
onWarning(target);
} else {
onClick(target);
}
}

const PrimaryButton = ({ text, onClick, disabled, type, onWarning, children }) => {
return (
<Button
type="primary"
onClick={(target) => handleWarning(onWarning, onClick, type, target)}
disabled={disabled}
>
{children}
</Button>
)
}

如此一來,原始Button元件能保持原本乾淨的樣子,你也可以基於原始元件去擴充設計同結構的元件來使用。
如果你還難以想像好處的話,可以再設計一個擴充元件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const handleDelete = async (onValidate, onDelete, onClick, target) => {
const validateResult = await onValidate(target);
if(validateResult && validateResult.payload && validateResult.payload.success) {
const deleteResult = await onDelete(target);
if(deleteResult && deleteResult.payload && deleteResult.payload.success) {
console.log('delete success!');
} else {
console.log('please try again');
}
} else {
console.log('please try again');
}
}

const DeleteButton = ({ onClick, disabled, type, onValidate, onDelete, children }) => {
return (
<Button
type="danger"
onClick={(target) => handleDelete(onValidate, onDelete, onClick, target)}
disabled={disabled}
>
{children}
</Button>
)
}

你會看到像這種delete button的情境,他需要做的工作其實會稍微比其他現有的元件多一些工作。
但是這種程式如果堆積起來會是很可怕的事情,因此搭配Composition設計樣式可以解決這種需求。

Composition與Inheritance的差別

學過物件導向的人可能會覺得,這個概念有點像繼承,把父類別想像成是原始元件,擁有它的特性。
但是其實只是概念相似,做的事情還是不太一樣的。

  1. 繼承會無條件繼承下來
    Composition可以挑選你要用的東西使用,並不會有全部繼承的問題。

  2. Composition的靈活度比Inheritance高
    React官方有一篇文章曾經討論過,到底需不需要用到Inheritance。
    結果是,他們認為Composition就能解決大部分的事情,沒必要特地設計成Inheritance。
    Composition你也可以想像成是lego的組合方式,他可以組合成小功能,當然也能組合成很大的功能。

專業一點解釋Composition與Inheritance的差別

  1. Composition是has-a的概念
    以上面的例子來說,DeleteButton具有Button的特性。
    因此我們可以說,DeleteButton has a Button.

  2. Inheritance是is-a的概念
    以物件導向的觀念來解釋的話,java.util.ArrayList繼承java.util.List。
    那麼你可以說,ArrayList is a List。