人們常用「牽一髮動全身」來描述電腦軟體的複雜度本質。電腦軟體在開發上的困難之一,便是來自於其本質性的高複雜度,而這高複雜度的本質,成了電腦軟體程式不容易更動的原因之一。

為什麼難以更動呢?因為,電腦程式中的各個組成之間,難免存在一定的相依性,而當兩個程式模組之間存在相依性時,若A模組相依於B模組,則B模組發生了若干的變動,則A模組便有可能受到波及影響,可能需要因應這個變動而變動。若是,B模組沒有適度的因應這個變動而做相對應的改變,那麼便有可能因此而引發不在預期內的結果,甚至是錯誤。這種因為對程式碼進行修改,而引發非預期的結果或錯誤,就被稱為是修改的副作用(side effect)。

如果程式中的相依關係少且單純,那麼,造成副作用的可能性自然就會降低,因為相依關係還容易理解及記憶。但若相依關係不僅數量繁多而且複雜,那麼可能就超出人腦所能夠管理的範圍,一旦需要修改程式時,這個變動經由龐大而且錯綜複雜的相依性網絡,所產生的效應,往往就沒有辦法為人所控制,這正是軟體開發之所以複雜的原因。

管理程式之間相依性的重要性
經過這麼多年,大家也明白到,「變動」是軟體開發過程中很難避免的,管理變動或是因應變動,就成了軟體專案開發是否能夠成功的關鍵之一。既然變動是難以避免的,而程式中各個模組間的相依性,又會深深影響到變動所造成的衝擊,那麼,管理程式中的相依關係,就成了從事設計時的頭號議題之一,很多的設計手法或技巧,都是在引導我們如何妥善管理程式中的相依關係。

舉例來說,著名的設計模式Facade,其作用即在做為一個子系統對外的唯一介面,同時隱藏住子系統內各個組成間的關係,以及實作的方式。

當你運用Facade模式於設計之中,所有對子系統的相依關係,僅會發生在做為Facade的類別之上。無論子系統中還有多少其他的類別,都不會和外部的類別建立任何相依關係。這使得相依關係都集中在單一類別上,即使日後子系統內部的類別有任何的變動,只要子系統對外的介面不受影響,那麼,對於相依於此子系統之Facade類別的外部類別而言,也都不會受到影響。

如此一來,便可以將變動所波及的範圍,限制在子系統的局部範圍內,不致於讓影響擴散到整個系統中的各個部份,因而更容易管理及控制。這正是一個透過設計來安排程式碼中的相依關係,進而降低變動發生時,對程式可能造成的衝擊的例子。

正如你所想像的,相依關係的數量會影響到程式中的複雜度。但是,不論你在設計中再怎麼處理、管理、約束相依關係的發生,藉以試著降低相依關係的數量,程式碼間一定會存在一定的相依關係,這是免不了的,因為程式模組間總是不會獨立存在,系統的整體運行終需要各個模組間的協同合作,才能夠達成。

事實上,除了相依關係的個數之外,相依關係發生的方式以及相依的方向,都會影響到程式複雜度的高低與否。例如前例中的Facade,便是控制相依關係發生的方式,使其集中在特定的單一類別上,進而降低程式的複雜程度。那麼,相依關係發生的方向,又是如何影響到程式的複雜度,又能夠如何降低變動對程式所產生的影響呢?

從設計原則層面來看相依性的處理
在設計中安排相依性關係的手法當中,有一個十分重要的原則,被稱為「相依倒置原則(Dependency Inversion Principle,DIP」。這個原則是由Robert C. Martin所提出的物件導向設計原則之一。這個設計原則的核心精神,就是在於告訴我們如何安排相依關係的方向。

在軟體設計中,如果某個模組A呼叫或使用了另一個模組B,我們便說A相依於B。A與B之間存在相依關係,而且相依關係的方向是由A至B。而軟體設計時常運用分層的設計方式,也就是說,將程式模組的作用區分為由上而下的不同層次,位於不同層次的模組,扮演不同的作用。

通常,在愈下層的模組,其抽象程度愈低,通常處理一些更接近實作層次、更基礎、更低階的動作。相反的,位在愈高層次中的模組,便負責更接近原則層次、更高階的邏輯。

你可以試著回想在你過去所做的設計中,不同層次中的模組,它們的相依關係究竟是如何呢?或許你會注意到,我們時常讓上層的模組倚賴下層的模組,因為上層的模組會運用下層的模組提供一些基礎的操作,以便滿足上層模組的需要。

但是,問題來了。任何事物多半如此,愈抽象的概念愈不容易發生變化,而愈具象的事物則愈容易有所變動,所以設計大師們才會教導我們要「依據介面而寫,而不要依據實作(Program to an interface, not an implementation),這便是因為抽象的介面不容易改變,但實作是會改變的。

若是上層的模組相依於下層的模組,下層的模組卻容易改變,這意謂著被相依的模組容易變動,自然也就連帶影響了上層的模組。還記得我們曾約略提過的「穩定相依原則(Stable Dependencies Principle,SDP)」嗎?這個設計的原則是希望模組間的相依關係,是朝向比較穩定的方向去進行。但若上層的模組相依於下層的模組,那麼相依關係便朝向比較不穩定的方向去進行,效果便適得其反了。

所以,相依倒置原則便告訴我們,應該要將這種上層依賴下層的相依關係給倒轉過來,讓下層的模組倒過來倚賴上層的模組,這麼一來,就可以讓相依關係朝比較穩定的方向進行了。

要怎麼倒轉這種相依的關係呢?Robert C. Martin的解決方法便是引入一個抽象的介面層,讓上層的模組相依於這個介面層,也讓下層的模組相依同一個介面層,這麼一來,上層的模組就不再相依於下層的模組了。

Robert C. Martin所舉的例子,是一個叫做Copy的類別,它會從KeyboardReader這個類別去讀取來自於鍵盤的輸入內容,然後寫到一個叫做PrinterWriter的類別去。

事實上,Copy這個高階的上層類別執行的是很抽象的複製演算法,即使輸入的來源不是KeyboardReader、寫入的對象不是PrinterWriter,對Copy來說,都不會有什麼影響。但是,因為必須運用低階的類別進行讀取或寫入的動作,就使得它對低階的產生相依關係。

當我們對複製動作的需求,改成不從鍵盤讀取、或不寫入印表機時,除了撰寫對應的供讀取、寫入的類別之外,還得修改Copy類別。複製演算法本身不太會有變化,讀取、寫入的對象卻可能持續增加、改變,而每次改變都會導致Copy類別須修改,這就是朝不穩定的方向建立相依關係所產生的問題。

照Robert C. Martin所提出的解法,可以分別引入Reader、Writer這兩個抽象類別,用來描述可供讀取、可供寫入類別的抽象介面,接著在Copy中運用這兩個抽象類別,而不是直接使用具象的實作類別。這麼一來,即使有了不同的Reader或Writer的實作品,也不會影響到Copy的寫法。

透過在原先依賴下層的上層與下層之間引入一個抽象的層次,便可以倒轉上下層之間原有的相依關係,使得上層中的模組不再倚賴下層中的模組,進而讓容易發生變化的下層不致於在改變時,影響到上層的模組,這便是相依倒置原則建議我們的一個解決方法。

 

作者簡介


Advertisement

更多 iThome相關內容