當你想要新增某個類別至你的系統中時,有時候會發現它和既存類別之間的共通性,而這樣的共通性可能會有兩種類型。

第一種類型是新增類別與既存類別有著介面的共通性,一般來說,也就是這二者有著共通的函式外貌(Signature),卻沒有任何可以共用的函式主體──只有函式的宣告部分是共用的。而第二種類型,除了有共通的函式外貌之外,還有部分可共用的函式。

所以,共通性有兩種層次,一種是介面的共通(一般來說,介面即為一類別所具備的Public函式),另一種則是函式的共通,除了具備共通的介面外,連實作的程式碼也相同。

盡量不對舊有程式碼造成衝擊
如果新增類別與既存類別的共通性屬於第一種類型,那麼加入新類別,在程式碼上所造成的演化方向,便是引入一個新的介面,透過此介面描述共通的函式外貌,並命令它們實作此一介面。例如既存類別A(以下以Java說明):

class A
{
public void m1() { … }
public void m2() { … }
}


接著當我們要加入新的類別B時,知道(或察覺)B和A具備共通性──它們都有m1()這個函式,但新增的B並不具備A的另一個函式m2()。這是十分常見的,兩類別間的介面通常不會完全一致,而它們的一致性是透過另一個介面來描述。所以,對A及B而言,我們加入新的介面I來描述A及B的共通性,並使A及B實作I。

class A implements I
{
public void m1() { … }
public void m2() { … }
}
class B implements I
{
public void m1() { … }
public void m3() { … }
}


對既存的類別A而言,除了改為實作介面I之外,單就類別本身對外的外貌而言(m1()及m2()),並沒有更動。所以,這個改變便不會波及舊有的程式碼,而造成原有程式碼行為有所變化。這是程式碼演化時最重要的事情之一,要盡量不對舊有程式碼造成衝擊。

如果新增類別與既存類別的共通性是第二種類型的話,那麼便會增加一個父類別,並在此父類別加入共通的函式──包括純綷的函式外貌宣告及含實際程式碼的函式,並將既存類別中的部分函式移至父類別中,讓既存的類別與新類別皆自此新加入的父類別加以繼承。

abstract class C
{
public void m1() { … }
protected void m4() { … }
abstract protected void m5();
}
class A extends C
{
public void m2() { … m4(); …}
protected void m5() { …}
}
class B extends C
{
public void m3() { … m4(); …}
protected void m5() { …}
}


在這個例子中,m1()具有實體的程式碼,所以整個函式都提升到父類別C去。這意謂著,當A及B都自C繼承時,便自然而然地具備這段程式碼的能力。原先A有個函式m2(),但它並不是與新增類別B的共通部分;雖然B不具備與m2()完全相同的特性,卻有部分相同,你當然可以選擇在B中重製相同的程式碼,但顯然這不是好的做法。

你可以考慮將部分可於B中重複使用的程式碼獨立移出,例如上例中的m4()即為自m2()中取出的函式,並修改A的m2(),使它呼叫m4()並執行一些和B不相通的動作。同樣的,B也可以在自有的m3()中呼叫m4()。

此外,A與B共通的m1(),有可能完全不需要更動,直接由B抽取而出即可。但也有可能發生多型的情況,也就是說,m1()中有部分的行為,是依子類別的不同而異的。在這種情況下,我們可以另行定義出一個函式,例如本例中的m5,將它宣告為protected(暗示由子類別覆寫,並非類別介面的一部分)以及abstract(強迫子類別實作),並從父類別的函式m1()中,呼叫這個由各子類別實作的函式,以展現出各子類別不同的行為。

演化最好是漸進的,每次的改變越小越好
我們在調整時,你可以觀察到過程中把握住幾個不變的原則:1.保持現有客戶端程式碼不受影響,2.盡量利用現有的程式碼,3.持續整理共通性。

在這個演化過程中,A雖然增加了一個新函式(即m5()),但是所有Public的函式都沒有變化,這也是我們要維持現有客戶端程式碼不受影響的應盡義務。要達成這個目標,不是保持介面(宣告為Public的函式外貌)不變,就是只在既有的介面上做擴充,也就是說,你只能再增加Public函式,但卻不能刪去或改變任何一個原有的函式,因為這麼做勢必立即影響到現有的客戶端程式,光是編譯就無法通過。

此外,除了保持介面不動或是只做擴充外,現有介面中函式的行為,也不能有所改變。這一點我們已強調多次,引起副作用的改變是最危險的。盡量利用原有的程式碼,一方面可以幫助我們降低造成額外風險的機會,另一方面也省去重寫程式碼的成本。

鑑別新類別在階層架構的位置,以降低新增的成本
每當你加入新的類別時,要試著鑑別出它在類別階層架構中的位置,也就是說,它應該位於那個層次上,繼承自那一個類別。

類別繼承階層架構,同時也是類別間共通性描述的架構。我們要這麼做的原因,最大的目的,當然是要找出在所有的類別中,欲增加的類別究竟與何者最為相像。這樣的尋找過程,不是找到合適的爸爸,就是找到適合做兄弟的類別。

找出最相像的類別,真正目的,自然是希望能夠直接沿襲現有的程式碼,使得我們再花點額外的功夫,就能讓新類別產生。每當你加入一個新類別的時候,除了找出它在共通性階層架構的位置之外,也同時必須審視在它加入後,這樣的階層架構是否需要調整(有沒有新的父類別產生、現有的子類別有沒有需要降級至孫類別的……)。

每加入一個類別,就重新審視,能保持程式碼緩慢演進。倘若這樣的動作是很長一段時間才進行一次,那麼當你打算要重新調整類別繼承階層架構時,通常會發現自己需要進行一次很大的變動,於是就陷入了很不利的局面,因為做大規模的調整,對現有程式碼會造成很大的衝擊。

程式碼在變化時,不只是自己的程式碼會改變,同時使用到這些程式碼的客戶端程式碼也會受到影響。當你自身的改變越大時,對客戶端程式造成的影響自然就有可能越大。持續演化,而每次演化的幅度都不大,才是比較正確的方式。

作者簡介:
王建興
清華大學資訊工程系的博士研究生,研究興趣包括電腦網路、點對點網路、分散式網路管理、以及行動式代理人,專長則是Internet應用系統的開發。曾參與過的開發專案性質十分廣泛而且不同,從ERP、PC Game到P2P網路電話都在他的涉獵範圍之內。

相關連結
程式碼的演化之路(1)持續讓程式碼保持進步的能力
程式碼的演化之路(2)擴充新功能,記得善用重構的技巧
程式碼的演化之路(3)要小心副作用,注意類別與繼承的管理

熱門新聞

Advertisement