1. 程式人生 > >軟體構建中的設計(一)

軟體構建中的設計(一)

設計中的挑戰

“軟體設計”意味著去構思。創造或發明一套方案,把一份軟體的規格說明書變成功能可執行的軟體。設計就是把需求分析和編碼除錯連起來的活動。好的高層次設計能提供一個穩妥容納多個較低層次設計的結構。好的設計對於小型專案非常有用,對於大型專案就更是不可或缺。

設計是一個險惡的問題

設計是一個險惡的問題。“險惡”問題就是那種只能通過解決或部分解決才能被明確的問題。說通俗一點,就是你必須先把這個問題解決一遍以便能夠明確地定義它,然後再次解決問題,從而形成一個可行的方案。

舉個栗子,位於美國華盛頓州塔科馬的Tacoma Narrows大橋曾經坍塌過一次,只因設計者在設計時只關注它是否足夠結實以承受設計負荷,卻忽略了大風給它帶來的 橫向諧波。在1940年狂風大作的某一天,這種諧波越來越大且不可控制,從而讓大橋坍塌。

這就是一個險惡問題的好例子,因為直到這座橋坍塌,工程師們才知道應該充分地考慮空氣動力學的因素。只有通過建造這座大橋(即解決這個問題),他們才能學會從這一問題中應該額外考慮的環節,從而才能建造出到現在依然屹立不倒的另一座橋樑。

設計是個了無章法的過程

說設計了無章法,是因為在此過程中你我會採取很多錯誤的步驟,多次誤入歧途,當然,我們也會改正。事實上,犯錯正是設計的關鍵所在,在設計階段犯錯並加以改正,其代價要比在編碼後才發現同樣的錯誤並徹底修改低得多。說設計了無章法,還因為優、劣設計之間的差異往往非常微妙。

另外,說設計了無章法,還因為你很難判斷設計何時算是“足夠好”?設計到什麼細節才算夠?有多少設計需要用形式化的設計符號完成,又有多少設計可以留到編碼時再做?什麼時候才算完成?因為設計永無止境,因此對上述問題最常見的回答是“到你沒時間再做了為止”。

設計就是確定取捨和調整順序的過程

理想的世界中,每一套系統都能即刻完成執行,不消耗任何儲存空間,不佔用任何網路頻寬,沒有任何錯誤,也無需任何成本即可生成。而在現實世界中,設計者工作的一個關鍵內容便是去衡量彼此衝突的各項設計特性,並盡力在區中尋求平衡。如果快速的反應速度比縮減開發時間更重要,那麼設計者會選取一套設計方案。如果縮減開發時間更重要,那麼設計者可能又會形成另一套不同的設計方案。

設計受到諸多限制

設計的要點,一部分是在創造可能發生的事情,而另一部分又是在限制可能發生的事情。假如人們有無限的時間、無限的木材,不考慮外在因素的情況下,人們建造的房屋可以無限蔓延,房屋可以無限高無限大。如果毫無約束,軟體最後也會是這樣的結果。正是由於這些限制,才會促使產生簡單的方案,並最終改善這一解決方案。軟體設計的目標也是如此。 

設計是不確定的

如果你讓三個人去設計一套同樣的程式,他們很可能會做出三套截然不同的設計,而每套設計可能都很不錯。

設計是一個啟發式過程

正因為設計過程充滿不確定性,因此設計技術也就趨於具有探索性——“經驗法則”或者“試試沒準能行的辦法”——而不是保證能產生預期結果的可重複的過程。設計過程中總會有試驗和犯錯誤。在一件工作或一件工作的某個方面十分湊效的設計或技術,不一定在下一個專案中適用。。沒有任何工具和方法是用之四海皆靈的。

設計是自然而然形成的

把設計的這些特性綜合歸納起來,我們可以說設計是“自然而然形成的”。設計不是在誰的頭腦中直接跳出來的。它是在不斷的設計評估、非正式討論、寫試驗程式碼以及修改試驗程式碼中演化和完善的。

關鍵的設計概念

好的設計源於對一小批關鍵設計概念的理解。這一節將會討論“複雜度”所扮演的角色、設計應具有的特徵,以及設計的層次。

偶然的難題和本質的難題

Brooks認為,兩類不同的問題導致軟體開發變得困難——本質的問題和偶然的問題。在哲學界,本質的屬性是一個事物必須具備、如果不具備就不再是該事物的屬性。舉個栗子,碳烤活魚必須要有魚,如果碳烤活魚沒有魚的話,就失去了它必須具備的本質。偶然的屬性則是指一件事物碰巧具有的屬性,有沒有這些屬性都不影響事物的本身。比方碳烤活魚可以加一些佐料,黃瓜或者豆芽,但是如果沒有這些也無傷大雅,這些細節都是次要的偶然屬性,可以把偶然屬性想成是附屬的、任意的、非必要的或偶然出現的性質。

軟體開發中大部分的偶然性難題在很久以前就已得到解決了。比如說,與笨拙的語法相關的那些偶然性難題大多已經從組合語言到第三代程式語言的演進過程中被解決了,而且這類問題的重要性也漸漸下降了。與非互動式計算機相關的偶然性難題也隨著分時作業系統取代批模式系統而被解決。整合程式設計環境更是進一步解決了由於開發工具之間無法很好地協作而帶來的效率問題。

Brooks論述說,在軟體開發剩下的那些本質性困難上的進展將會變得相對緩慢。究其原因,是因為從本質上說軟體開發就是不斷地去挖掘錯綜複雜、相互連線的整套概念的所有細節。其本質性的困難來自很多方面:必須去面對複雜、無序的現實世界;精確而完整的識別出各種依賴關係與例外情況;設計出完全正確而不是大概正確的解決方案;諸如此類。即使我們能發明出一種與現實中亟待解決的問題有著相同術語的程式語言,但是人們想要弄清現實世界到底如何運作仍有很多挑戰,因此程式設計仍會十分困難。當軟體要解決更大規模的現實問題時,現實的實體之間的互動行為就會變得更為複雜,這些問題又增加了軟體解決方案的本質性困難。

管理複雜度的重要性

在對軟體專案失敗的原因進行調查時,人們很少吧技術原因歸為專案失敗的首要因素。專案的失敗大多數都是不盡如人意的需求、規劃和管理所導致的。但是,當專案確由技術因素導致失敗時,其原因通常就是失控的複雜度。有關的軟體變得極端複雜,讓人無法知道它究竟是做什麼的。當沒人知道對一處程式碼的改動會對其他程式碼帶來什麼影響時,專案也就快停止進展了。

作為軟體開發人員,我們不應該試著在同一時間把整個程式都塞進自己的大腦,而應該試著以某種方式去組織程式,以便能夠在一個時刻可以專注於一個特定的部分。這麼做的目的是儘量減少在任一時間所要考慮的程式量。

從軟體架構的層次上,可以通過吧整個系統分解為多個子系統來降低問題的複雜度。人類更易於理解許多項簡單的資訊,而不是一項複雜的資訊。所有軟體設計技術的目標都是把複雜問題分解成簡單的部分。子系統間的相互依賴越少,你就越容易在同一時間裡專注問題的一小部分。精心設計的物件關係使關注點相互分離,從而使你能在每時每刻專注於一件事情。在更高匯聚的層次上,包提供了相同的好處。

保持子程式的短小精悍也能幫助你減少思考的負擔。從問題的領域著手,而不是從底層實現細節入手去編寫程式,在最抽象的層次上工作,也能減少人的腦力負擔。

如何應對複雜度

高代價、低效率的設計源於下面的三種根源:

  • 用複雜的方法解決簡單的問題;
  • 用簡單但錯誤的方法解決複雜的問題;
  • 用不恰當的複雜方法解決複雜的問題

正如Dijkstra所指出的,現代的軟體本身就很複雜,無論你多努力,最終都會與存於現實世界問題本身的某種程度的複雜性不期而遇。這就意味著要用下面這兩種方法來管理複雜度:

  • 把任何人在同一時間需要處理的本質複雜度的量減少最少;
  • 不要讓偶然性的複雜度無謂地快速增長。

一旦你能理解軟體開發中任何其他技術目標都不如管理複雜度重要時,眾多設計上的考慮就都會變得直截了當。

理想的設計特徵

高質量的設計具有很多常見的特徵。如果你能實現所有這些目標,你的設計就真的非常好了。這些目標之間有時會相互抵觸,但這也正是設計中的挑戰所在——在一系統相互競爭的目標之中,做出一套最好的折中方案。有些高質量設計的特徵也同樣是高質量程式的特徵,如可靠性和效能等。而有些則只是設計的範疇內的特徵。

下面列出一些設計範疇內的特徵:

  • 最小的複雜度(Minimal complexity) :正如剛剛說過的,設計的首要目標就是要讓複雜度最小。要詞句做出“聰明的”設計,因為“聰明的”設計常常都是難以理解的。應該做出簡單且易於理解的設計。如果你的設計方案不能讓你在專注於程式的一部分時安心地忽視其他部分的話,這一設計就沒有什麼作用了。
  • 易於維護(Easy of maintenance) :易於維護意味著在設計時為做維護工作的程式設計師著想。請時刻想著這些維護程式設計師可能會就你寫的程式碼而提出的問題。把這些程式設計師當成你的聽眾。進而設計出能自明的系統來。
  • 鬆散耦合(loose coupling) :鬆散耦合意味著在設計時讓程式的各個組成部分之間關聯最小。通過應用類介面中的合理抽象、封裝性及資訊隱藏等原則,設計出相互關聯儘可能最少的類。減少關聯也就是減少了整合、測試與維護時的工作量。
  • 可擴充套件性(extensibility):可擴充套件性就是說你能增強系統的功能而無須破壞其底層結構。你可以改動系統的某一部分而不會影響到其他部分。越是可能發生的改動,越不會給系統造成什麼破壞。
  • 可重用性(reusability) :可重用性意味著所設計的系統的組成部分能在其它系統中重複使用。
  • 高扇入(high fan-in):高扇入就是說讓大量的類使用某個給定的類。這意味著設計出的系統很好的利用了在較低層次上的工具類。
  • 低扇出(low fan-out) :低扇出就是說讓一個類裡少量或適中的使用其他的類。高扇出(超過約7個類)說明一個類使用了大量的其它的類。因此可能變得過於複雜。研究發現,無論考慮某個子程式呼叫其他子程式的量,還是考慮某個類使用其他類的量,低扇出的原則都是有益的。
  • 可移植性(portability) :可移植性是說應該這樣設計系統,使它能很方便地移植到其他環境中。
  • 精簡性(leanness):精簡性意味著設計出的系統沒有多餘的部分。伏爾泰說,一本書的完成,不在它不能再加入任何內容的時候,而在不能再刪去任何內容的時候。在軟體領域中,這一觀點就更正確,因為任何多餘的程式碼也需要 開發、複審和測試,並且當修改了其他程式碼之後還要重新考慮它們。軟體的後續版本也需要和這些多餘程式碼保持向後相容。要問這個關鍵的問題:“這雖然簡單,但把它加進來之後會損害什麼呢?”
  • 層次性(stratification) :層次性意味著儘量保持系統各個分解層的層次性,使你能在任意的層面上觀察系統,並得到某種具有一致性的看法。設計出來的系統應該在任意層次上觀察而不需要進入其他層次。舉例來說,假設你正在編寫一個新系統,其中用到很多設計不佳的舊程式碼,這時你應該為新系統編寫一個負責同舊程式碼互動的層。在設計這一層時,要讓它能隱藏舊程式碼的低劣質量,同時為新的層次提供一組一致的服務。這樣,你的系統的其他部分就只需與這一層進行互動,而無須直接同舊程式碼打交道了。在這個例子中,層次化設計的益處有:(1)它把低劣程式碼的爛泥潭禁閉起來;(2)如果你最終能拋棄或者重構舊程式碼,那時就不必修改除互動層之外的任何新程式碼。
  • 標準技術(standard techniques) :一個系統所依賴的外來的、古怪的東西越多,別人在第一次想要理解它的時候就越是頭疼。要儘量用標準化的、常用 的方法,讓整個系統給人一種熟悉的感覺。

設計的層次

需要在一個軟體系統中的若干不同細節層次上進行設計。有些設計技術適用於所有的層次,而有些只適用於某些層次上。圖1-1展示了這些層次。

圖1-1   一個程式最後那個的層次設計。系統①首先被組織為系統②。子系統被進一步分解為類③,然後類又被分解為子程式和資料④。每個子程式的內部也需要進行設計⑤

第一層:軟體系統

第一層次就是整個系統。有的程式設計師直接從系統層次就開始設計類,但是往往先從子系統或者包這些類的更高組織層次來思考會更有益處。

第2層:分解為子系統或包

在這一層次上設計的主要成果是識別出所有的主要子系統。這些子系統可能會很大,比如說資料庫、使用者介面、業務規則、命令直譯器、報表引擎等。這一層的主要設計活動就是確定如何把程式分為主要的子系統,並定義清楚允許各子系統如何使用其他子系統。對於任何至少需要幾周時間才能完成的專案,在這一層次上進行劃分通常都是必需的。在每個子系統的內部可能要用到不同的設計方法,請對系統中的每一部分選用最恰當的方法。在圖1-1中,這一層次的設計是用②註明的。

在這一層中,定義子系統之間的通訊規則是非常重要的,即不同子系統之間互相通訊的規則。如果所有的子系統都能同其他子系統通訊,你就會完全失去把它們分開來所帶來的好處。應該通過限制子系統之間的通訊來讓每個子系統更有存在意義。

舉個栗子,在圖1-2中,你把一個系統劃分成六個子系統。在沒有定義任何規則時,熱力學第二定律就會發生作用,整個系統的熵將會增加。熵之所以增加的一種原因是,如果不對子系統的通訊加任何限制,那麼它們之間的通訊就會肆意地發生,如圖1-3所示。

圖1-2   一個有六個子系統的系統示例

圖1-3   當子系統之間的通訊沒有任何限制時就會像這個樣子

正如你所看到的,這裡的每個子系統最終都會直接與其他子系統進行通訊,從而為我們提出一些重要的問題:

  • 一個開發人員需要理解系統中多少個不同的部分?哪怕只理解一丁點兒,才能在圖形子系統中改動某些東西?
  • 當你想在另一個系統中試圖使用業務規則時會發生什麼?
  • 當你想在系統中加入一套新的使用者介面時,比如說為了測試而開發的命令列介面會發生什麼?
  • 當你想把資料儲存放到一臺遠端計算機上,又會發生什麼?

你可以把子系統之間的連線當成水管。當你想去掉某個子系統時,勢必會有不少水管連在上面。你需要斷開再重新連線的水管數量越多,弄出來的水就會越多。你肯定想把系統的架構設計成這樣:如果想把某個子系統取走重用時,不用重新連線太多水管,重新連線起來也不會太難。

有先見之明的話,所有這些問題就不會花太多額外功夫。只有當必要時,才應該允許子系統之間的通訊。如果你還拿不準該如何設計的話,那麼就應該先對子系統之間的通訊加以限制,等日後需要時再放鬆,這要比先不限制,等子系統之間已經有上百個呼叫時再加以限制要容易得多。圖1-4展示了施加少量通訊規則後可以把1-3中的系統變成的樣子。

圖1-4   施加若干通訊規則後,子系統之間的互動得以顯著地簡化

為了讓子系統之間的連線簡單易容且易於維護,就要儘量減少子系統之間的互動關係。最簡單的互動關係就是讓一個子系統去呼叫另一個子系統中的程式;稍微複雜一點的互動關係是在一個子系統中包含另一個子系統中的類;而最複雜的互動關係是讓一個子系統中的類繼承另一個子系統中的類。

有一條很好的基本原則,像如圖1-4這樣的系統層設計圖應該是無環圖。換句話說,程式中不應該有任何環形關係,比如說A類使用了B類、B類使用了C類,而C類又使用了A類這種情況。對於大型程式或一系列程式而言,在子系統這一層次上進行設計是至關重要的。如果你覺得自己要寫的程式小到可以跳過在子系統層次上進行設計這一步驟,那麼只要確保跳過這層設計的決定是經過深思熟慮的。

常用的子系統

有些種類的子系統會在不同的系統中反覆出現。下面幾種就較為常見。

業務規則:業務規則是指那些在計算機系統中編入的法律、規則、政策以及過程。比方你在開發一套薪資系統,你可能要把國家國稅局關於允許扣提的金額和估算的稅率編到你的系統中。

使用者介面:應建立一個子系統,把使用者介面元件同其他部分分隔開來,以便使使用者介面的演化不會破壞程式的其餘部分。在大多數情況下,使用者介面子系統會使用多個附屬的子系統或類來處理使用者介面、命令列介面、選單操作、窗體管理、幫助系統,等等。

資料庫訪問:可以把對資料庫進行訪問的實現細節進行隱藏起來,讓程式的絕大部分可以不必關心處理底層結構的繁瑣細節,並能像在業務層次一樣處理資料。隱藏實現細節的子系統可以為系統提供有價值的抽象層,從而減少程式的複雜度。它把和資料庫相關的操作集中起來,減少了在對資料進行操作時發生錯誤的機率。同時,它還能讓資料庫的設計結構更易於變化,做這種修改時無須修改程式的主要部分。

對系統的依賴性:把對作業系統的依賴因素歸到一個子系統裡,就如同把對硬體的依賴因素風中起來一樣。比如說,你在開發一個運行於Windows作業系統上的程式,可為什麼一定要把自己侷限在Windows環境中呢?把所有與Windows相關的系統呼叫都隔離起來,放到一個Windows介面子系統中,這樣日後你想把程式移植到Linux或Mac OS作業系統時,只要新增或修改介面子系統就可以了。

第三層:分解為類

在這一層次上的設計包括識別出系統中所有的類。例如,資料庫介面子系統可能會被進一步劃分成資料訪問類、持久化框架類以及資料庫元資料。圖1-1中的第三層就展示了第二層中一個子系統是如何被分解為類的,當然這一暗示著第二層的其他三個子系統也被分解為類了。

當定義子系統中的類時,也就同時定義了這些類與系統的其餘部分打交道的細節。尤其是要確定好類的介面。總的來說,這一層的主要設計任務是把所有子系統進行適當的分解,並確保分解出的細節都恰到好處,能夠用單個的類實現。

類和物件的比較:面向物件設計的一個核心概念就是物件(object)與類(class)的區分。物件是指執行期間在程式中實際存在的具體實體,而類是指在程式原始碼中存在的靜態事物。物件是動態的,它擁有在程式執行期間所能得到的具體的值和屬性。例如,你可以定義一個名為Person的類,它具有姓名、年齡、性別等屬性。程式執行期間,你可以有nancy、hank等Person物件,它們都是類的例項。

第四層:分解子程式

這一層的設計包括把每個類細分為子程式。在第三層中定義出類的介面已經定義了其中一些子程式,而第四層的設計將細化出類的私用(private)子程式。當你檢視類裡面子程式的細節時,就會發現很多子程式都很簡單,但也有些子程式是由更多層次化組織的子程式所組成的,這就需要更多的設計工作了。

完整地定義出類內部的的子程式,常常會有助於更好地理解類的介面,反過來,這又有助於對類的介面進行進一步修改,也就是說再次返回第三層的設計。

這一層次的分解和設計通常是留給程式設計師個人來完成的,它對於用時超過幾小時的專案而言就是必須做的了。雖然不用非常正式地完整這一步驟,但至少也要在腦中完成。

第五層:子程式內部的設計

在子程式層次上進行設計就是為每個子程式佈置詳細的功能。子程式內部的設計工作通常是由負責該子程式的開發人員來完成的。這裡的設計工作包括編寫虛擬碼、選擇演算法、組織子程式內部的程式碼塊,以及用程式語言編寫程式碼。這一步的工作是不可跳過的,能夠達到的質量也因人而異有好有壞。在圖1-1中的第五步就是這一層設計的工作。