這樣要來介紹的是開關原則,其實以字面上來說會有點讓人誤解其中的意思,個人認為有點像是積木或是拼圖疊加的概念,但是概念上也是很簡單,只要熟悉觀念即可變成自己的設計心法,讓我們來看看吧。

開關原則

在OOP的設計中,開關原則需要保持開放程式的擴充性質,並且關閉程式的修改。
聽起來好像很奇怪?那麼我們來舉例吧!
如果今天你是設計汽車的工程師,當有人拿車子要給你修理輪胎的時候,你一定只會把輪胎更換而已,並不會動到其他東西,例如:引擎, 座椅……。只需要把指定的元件更換,而不需要動到過多的東西。

換句話說

以OOP的設計原則來說,開關原則可以讓程式保持DRY(Don’t Repeat Yourself)

實例展示

讓我們來看以下的例子,我有一個訂單,裡面包含了飲品:

1
2
3
4
5
6
7
8
9
10
11
App = () => {
const drink = {
name: 'milk',
size: 1000,
}
return (
<div className="App">
<Order drink={drink}/>
</div>
);
}

訂單:

1
2
3
4
5
6
7
8
9
Order = ({ drink }) => {
const { name, size } = drink;
return (
<div>
<h2>{name}</h2>
<p>limit: {size}</p>
</div>
);
}

問題來了

然而現在問題來了,如果今天突然出現其他需求,需要飲品的規格不太一樣:

1
2
3
4
5
6
7
8
9
10
11
const drink2 = {
name: 'coke',
size: 350,
isScalable: false,
};

const drink3 = {
name: 'water',
size: 600,
isPure: true,
};

怎麼做

這個時候,一般的開發者會怎麼做呢?沒錯,就是你想的那樣

1
2
3
4
5
6
7
return (
<div className="App">
<Order drink={drink}/>
<Order drink={drink2}/>
<Order drink={drink3}/>
</div>
);

當然,因為規格不一樣,因此我們必須把我們的Component改成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
Order = ({ drink }) => {
const { name, size, isScalable, isPure } = drink;
const [hasUpgraded, setHasUpgraded] = React.useState(false);
return (
<div>
<h2>{name}</h2>
<p>limit: {size}</p>
{ isPure ? <p>Clean</p> : <p>Not clean</p> }
{ isScalable && <button onClick={() => setHasUpgraded(!hasUpgraded)}>Upgrade</button> }
</div>
);
}

你會發現程式碼開始變得越來越髒了,如果isPure和isScalable裡面所做的事情是大量的JSX代碼,可想而知,別人拿到這個程式碼會需要耗費較大的時間閱讀你的程式碼。那…該怎麼辦呢?那就使用HOC來改善吧!

結合HOC改善

有學過OOP相關的語言的人,應該知道上面的例子應用在物件導向上,是可以設計成繼承關係的。不過React團隊鼓勵大家使用function組件大於繼承,而High Order Function就是一個例子,他的設計原則也可以保持程式的DRY。

我們可以透過HOC把water功能從drink中抽離出來:

1
2
3
4
5
6
7
8
9
10
const concatWaterOrder = DrinkComponent => props => {
const { drink } = props;
const { isPure } = drink;
return (
<DrinkComponent drink={drink}>
{ isPure ? <p>Clean</p> : <p>Not clean</p> }
{ props.children }
</DrinkComponent>
);
};

最後,把共用擁有的功能變成drink component,後面接著放置其他component產生出來的DOM:

1
2
3
4
5
6
7
8
9
10
const drinkOrder = props => {
const { name, limit } = props.drink;
return (
<div>
<h2>{name}</h2>
<p>limit: {size}</p>
{ props.children }
</div>
);
};

如何使用

其實這樣就客製化好我們的開關component了,使用方式:

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
26
App = () => {
const drink = {
name: 'milk',
size: 1000,
};
const drink2 = {
name: 'coke',
size: 350,
isScalable: false,
};

const drink3 = {
name: 'water',
size: 600,
isPure: true,
};

const waterOrder = concatWaterOrder(drinkOrder);

return (
<div className="App">
<Order drink={drink}/>
<waterOrder drink={drink3}/>
</div>
);
}

同理製作

同理,繼續做其他component

1
2
3
4
5
6
7
8
9
10
const concatSodaOrder = DrinkComponent => props => {
const { drink } = props;
const { isScalable } = drink;
return (
<DrinkComponent drink={drink}>
{ isScalable && <button onClick={() => setHasUpgraded(!hasUpgraded)}>Upgrade</button> }
{ props.children }
</DrinkComponent>
);
};

結果:

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
26
27
28
App = () => {
const drink = {
name: 'milk',
size: 1000,
};
const drink2 = {
name: 'coke',
size: 350,
isScalable: false,
};

const drink3 = {
name: 'water',
size: 600,
isPure: true,
};

const waterOrder = concatWaterOrder(drinkOrder);
const sodaOrder = concatSodaOrder(sodaOrder);

return (
<div className="App">
<Order drink={drink}/>
<waterOrder drink={drink3}/>
<sodaOrder drink={drink2}/>
</div>
);
}

彈性的使用方式

而且HOC是可以很彈性的使用的,你也可以這樣做:

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
26
27
28
29
30
31
32
33
34
35
36
37
App = () => {
const drink = {
name: 'milk',
size: 1000,
};
const drink2 = {
name: 'coke',
size: 350,
isScalable: false,
};

const drink3 = {
name: 'water',
size: 600,
isPure: true,
};

const drink4 = {
name: 'sparklingWater',
size: 580,
isPure: true,
isScalable: false,
};

const waterOrder = concatWaterOrder(drinkOrder);
const sodaOrder = concatSodaOrder(sodaOrder);
const sparklingWaterOrder = concatWaterOrder(concatSodaOrder(drinkOrder));

return (
<div className="App">
<Order drink={drink}/>
<waterOrder drink={drink3}/>
<sodaOrder drink={drink2}/>
<sparklingWaterOrder drink={drink4}/>
</div>
);
}

如此一來,重複的特性就不需要再另外設計一個component了,很符合開關原則的重複利用與擴充性