這個東西可以共用,我們抽出來吧!
當我們發現程式中,有很多長得很像的部分,我們通常會開始思考,如何把這些東西整理出來。
畢竟著名的DRY原則,告訴我們:不要重複你自己。
於是我們學習使用function或class來解決相同程式重複撰寫的問題。
透過物件導向開發的方式,來減少重複的程式碼,後來更是衍生出各種設計模式(design pattern)。
聽起來很棒,但若是無腦的將所有看起來很像的東西,通通抽出來,那其實會造成一些潛在的問題。
比如說,有些時候看起來重複的程式,其實不適合整合成同一個function或class中的method。為什麼?因為他們可能只是現在看起來一樣,其實在處理的是不同問題,或是在本質上有所差異。
現實世界
在一個購物平台網站,買家跟賣家都可以註冊帳號。因為一開始系統很簡單,所以兩者的行為可能是差不多的,所以工程師決定簡單地新增一個共用的註冊程式。
/**
* 註冊帳號
*
* @param $identity 身份,buyer或seller
* @param $account 帳號
* @param $password 密嗎
*/
function register($identity, $account, $password)
{
// 寫入db
insert_into_member_table($identity, $account, $password);
}
過了不久,PM說因為法規要求,賣家要提供營業地址。於是為了繼續共用程式,工程師將register()
調整成:
/**
* 註冊帳號
*
* @param $identity 身份,buyer或seller
* @param $account 帳號
* @param $password 密嗎
* @param $address 營業地址
*/
function register($identity, $account, $password, $address)
{
// 買家
if ($identity == 'buyer') {
$address = null;
}
// 寫入db
insert_into_member_table($identity, $account, $password, $address);
}
又過了不久,PM說我們要將賣家分級,根據賣家等級,建立不同的賣場(store)。為了程式共用...
/**
* 註冊帳號
*
* @param $identity 身份,buyer或seller
* @param $account 帳號
* @param $password 密嗎
* @param $address 營業地址
* @param $level 等級
*/
function register($identity, $account, $password, $address, $level)
{
// 買家
if ($identity == 'buyer') {
$address = null;
// 賣家
} else {
// 超級賣家
if ($level == 'super') {
create_super_store($account);
// 普通賣家
} else {
create_normal_store($account);
}
}
// 寫入db
insert_into_member_table($identity, $account, $password, $address);
}
好,又過了不久...PM說商家也要分等級了!為了給高等級的商家有更好的服務,高等商家可以填寫地址,公司會不定期寄送贈品到指定地址。那原本賣家用到的$level跟$address看起來可以重複使用,為了不要新增參數,就沿用吧...
/**
* 註冊帳號
*
* @param $identity 身份,buyer或seller
* @param $account 帳號
* @param $password 密嗎
* @param $address 營業地址或居住地址
* @param $level 賣家等級或商家等級
*/
function register($identity, $account, $password, $address, $level)
{
// 買家
if ($identity == 'buyer') {
// 不是VIP會員不用填地址
if ($level != 'vip') {
$address = null;
}
// 賣家
} else {
// 超級賣家
if ($level == 'super') {
create_super_store($account);
// 普通賣家
} else {
create_normal_store($account);
}
}
// 寫入db
insert_into_member_table($identity, $account, $password, $address);
}
ok...又隔了不久,PM說....工程師立刻答應,「好的沒問題,只要在register加上...」
等一下。
你確定繼續擴充這支程式沒問題嗎?
如果我們先檢視一下目前的程式,可以發現已經有一些很明顯的問題存在了:
- 各種買家、賣家獨有的判斷,導致if/else不斷增生,第一時間難以讀懂這支程式,
$address
的value到底是什麼?(可讀性不佳、維護性不佳) - function的參數越來越不精準,有多重涵義(interface不明),無法透過程式介面大概猜測出程式的行為。(可讀性不佳)
- 賣場(store)只有賣家有,卻出現在一支名為register的function裡面(不符合單一職責原則)
- 只加上買家或賣家的行為,都要動到這支程式,必須要很小心或經過測試才知道有沒有影響到另一方的行為(擴充性不佳、維護性不佳、不符合單一職責原則)
- ...
繼續在這樣的架構上擴充程式,絕對會長成一個危樓...。
那...難道不共用/複用程式嗎?
當然不是,而是說我們應該要適時地調整程式架構(refactor),當發現程式出現分歧,或是開始難以維護或理解的時候,就不應該再用錯誤的方式重複使用程式(reuse)。
比如說上面的例子,如果對於商業邏輯的認知足夠的話,一開始就可以把註冊程式區分為registerSeller()
跟registerBuyer()
,並且將賣家的賣場store部分切離出去:
// 賣家註冊
function registerSeller($account, $password, $address)
{
// 寫入db
insert_into_member_table('seller', $account, $password, $address);
}
// 買家註冊
function registerBuyer($account, $password, $level, $address = null)
{
// 不是VIP會員不用填地址
if ($level != 'vip') {
$address = null;
}
// 寫入db
insert_into_member_table('buyer', $account, $password, $address);
}
雖然看起來重複程式了,但其實仔細想想,買家和賣家其實是兩種不同的身份,隨著平台的發展,行為可能會有很大的不同,未來程式有機會擴充成很不同的樣子,一開始如果沒有想清楚,為了共用而共用,並且按照這樣的邏輯發展下去,其實就只是把複雜的東西塞到一支萬能function或class裡面而已,雖然達到了reuse,但卻降低了程式的可讀性、維護性還有擴充性,這並不是真正的reuse。
結論
共用某些東西之前,多思考三秒鐘,確認它們:
- 這個類別、函數、參數名稱真的有相同意義嗎?
- 這段程式的行為在現實中所對應的概念是一致的嗎?
- 未來要擴充的話,可能的方向是?
那說了這麼多,結論大概是:
不要只想著要共用某隻 function 就把所有的東西往裡面塞 XD