以下將會以前端開發作為例子,讓大家更了解耦合與內聚的概念是什麼。

耦合(Coupling)


簡單來說就是相依性,如果討論的對象,彼此的相依性越強烈,那麼我們就可以說彼此的耦合性越大。

代表人物

如果要拿來擬人的話,最具代表性的人,大概就是國文課本的賣油翁了。
大家最熟悉的課文:
康肅問曰:「汝亦知射乎?吾射不亦精乎?」
翁曰:「無他,但手熟爾。」
康肅忿然曰:「爾安敢輕吾射!」
翁曰:「以我酌油知之。」
乃取一葫蘆置於地,以錢覆其口,徐以杓酌油瀝之,自錢孔入而錢不濕。
因曰:「我亦無他,惟手熟爾。」

賣油翁雖然其他的事並不會,但是他只負責賣油這個任務,而且很專精。

共用元件

耦合性可以讓同樣頁面擁有的Component, function…,重複被使用,因為我們在開發程式的時候,一定會時常遇到類似的情境需要用到類似的功能,因此會把這些東西便成為共用元件。
例如以下的例子:

  1. config.js
    1
    2
    3
    4
    5
    export const categories = [
    {label: '全部', value: 0},
    {label: '未滿一年', value: 1},
    {label: '一年', value: 2},
    ];

共用config可以讓全站皆可使用,但這也意味著被用越多次的話,耦合性就會越強大,也很容易發生改A壞B的狀況。

假設情境A - 共用config

今天有一個工程師收到一個需求,除了原本應在Client的config之外
要新增一個新的功能for business,文案一樣是“全部, 未滿一年, 一年…”
然而後端給予前端的資料型態為[全部: 1, 未滿一年: 2, 一年: 3…]
前端想當然不以為意,把原本的categoriesForClient改成:

1
2
3
4
5
export const categories = [
{label: '全部', value: 1},
{label: '未滿一年', value: 2},
{label: '一年', value: 3},
];

最後發現,後端的Client和Business的schema並沒有對齊,一個value從0開始,一個從1開始,導致原本Client的顯示都不對,而位移了一格。
這種狀況就是高耦合性的config所造成的Side effects。
因此,前端應該要改成:

1
2
3
4
5
6
7
8
9
10
11
export const categoriesForClient = [
{label: '全部', value: 0},
{label: '未滿一年', value: 1},
{label: '一年', value: 2},
];

export const categoriesForBusiness = [
{label: '全部', value: 1},
{label: '未滿一年', value: 2},
{label: '一年', value: 3},
];

相信以上面的例子就能充分了解耦合性帶來的好處與壞處了。
好處就是方便,加速開發時間的成本,提升耦合性也可以讓工程師在維護code更加容易。
壞處就是如果一個不小心就會發生改A壞B的狀況,很常發生在人與人溝通之間訊息不對等造成的誤會。
千萬要記住,要修改耦合性越高的元件,要越小心地去更動。
你可能會覺得設計看起來有點蠢?不,這種事情一定有機會碰到的,相信我!

假設情境B - UI元件

大部分的人認為,使用前端框架當然是要搭配UI component的design guideline開發效率才會提升呀
因此我們時常會在全站使用已經裝好的UI元件統一使用,就一般的情境來說,不太可能會碰到載了別人的套件還掛掉的狀況。
不過我們可能會拿下載好的UI component,然後override UI component變成自己的設計元件。
例如:ant-design, bootstrap……

內聚(Cohesion)


如果對象可以完成的事情越多,也代表這個對象所具備的能力或功能越強大,那麼就可以稱為內聚性越大。

代表人物

要說代表人物的話,應該就屬超人了,他可以做的事情很多,同時也代表他的責任重大,完成職責的時間量也很大。

假設情境

今天企劃開一個需求,要設計一個可以計算加法的計算機:

1
2
3
4
5
6
7
8
9
const Caculator = (a, b) => {
const sum = a + b;
return (
<>
<p>{a} + {b}</p>
<p>result: {sum}</p>
</>
);
}

目前看起來好像很正常對吧?但是現在企劃說,想要再加減法功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Caculator = (a, b, sign) => {
let sum = 0;
if(sign === '+') {
sum = a + b;
} else if(sign === '-') {
sum = a - b;
}
return (
<>
<p>{a} {sign} {b}</p>
<p>result: {sum}</p>
</>
);
}

因應需要判斷加減法,所以多了一個參數判斷符號,裡面也多了判斷。
然而隨著需求的增加,程式也會變得越來越龐大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Caculator = (a, b, sign) => {
let sum = 0;
if(sign === '+') {
sum = a + b;
} else if(sign === '-') {
sum = a - b;
} else if(sign === '*') {
sum = a * b;
} else if(sign === '/') {
sum = (b !== 0) ? (a / b) : 'Not exist';
}
return (
<>
<p>{a} {sign} {b}</p>
<p>result: {sum}</p>
</>
);
}

從這個計算機來看,你會發現他的耦合性是很低的,他只耦合了呼叫他的Component。
而這個Component本身可以計算加減乘除,非常的萬能,具備高內聚的特性。
但是你說這個component這樣設計好嗎?不就是低耦合高內聚的特性嗎?其實這見仁見智。
如果今天這個計算機Component他每一個計算scope內所包的程式很龐大,其實是不容易維護的。
講難聽一點,可能會變成好幾千行的一坨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
27
28
29
const calulateHandler = (a, b, sign) => {
switch(sign) {
case '+': {
return a + b;
}
case '-': {
return a - b;
}
case '*': {
return a * b;
}
case '/': {
return (b !== 0) ? (a / b) : 'Not exist';
}
default: {
return '';
}
}
}

const Caculator = (a, b, sign) => {
const sum = calulateHandler(a, b, sign);
return (
<>
<p>{a} {sign} {b}</p>
<p>result: {sum}</p>
</>
);
}

我們將計算過程包裝成一個pure function,Caculator專注在頁面的UI顯示,而calulateHandler專注在計算,如此就能讓程式的分工變的明確,工程師在維護上也比較知道,如果是計算出問題就找calulateHandler,UI顯示要改變就找Caculator,可維護性大大的增加。

所以你也會發現,低耦合高內聚的程式其實也具備高擴展性的特性,你要調動功能是很容易的,他並不像高耦合性的程式不容易改動,容易出現Side effect的狀況,其實也因為他很容易擴充,所以大家低耦合高聚合才會成為大家時常聽到的一個設計目標。

違背單一職責原則

之前文章有討論到前端所用到的SOLID原則,而高內聚的程式正是違反了單一職責原則,應該要把職責一一切出來,而不應該把過多的成本放在同一個元件上。

哪個比較好

我們常常聽到很多人說開發程式要“低耦合, 高內聚”。
可是真的是這樣嗎?這可能要打一個很大的問號。
其實開發程式沒有絕對的,特別是在設計樣式套用的情境上需要更加小心。
有時候也要因應情境調整耦合和內聚各自佔的比例,過多或過少都會增加出狀況的風險。
千萬不可以為了設計樣式而設計程式,一旦用的不好,很容易讓團隊造成不小的衝擊。