我們如何知道軟體設計的優劣呢?以下是一些拙劣設計的癥狀,當軟體出現下面任何一種氣味時,就表明軟體正在腐化。
- 僵化性(Rigidity):很難對系統進行改動,因為每個改動都會迫使許多對系統其他部分的其他改動。
- 脆弱性(Fragility):對系統的改動會導致系統中和改動的地方在概念上無關的許多地方出現問題。
- 牢固性(Immobility):很難解開系統的糾結,使之成為一些可在其他系統中重用的組件。
- 粘滯性(Viscosity):做正確的事情比做錯誤的事情要困難。
- 不必要的複雜性(Needless Complexity):設計中包含有不具任何直接好處的基礎結構。
- 不必要的重複(Needless Repetition):設計中包含有重複的結構,而該重複的結構本可以使用單一的抽象進行統一。
- 晦澀性(Opacity):很難閱讀、理解。沒有很好地表現出意圖。
僵化性
僵化是指難以對軟體進行改動,即使是簡單的改動。如果單一的改動會導致有依賴關係的模組中的連鎖改動,那麼設計就是僵化的。必須要改動的模組越多,設計就越僵化。
大部分開發人員都遇到這樣的情況:他們對被要求進行一個看似簡單的改動,當他實際進行改動時,才發現有許多改動帶來的影響自己並沒有預測到。最後,改動所花費的時間要遠比初始估算長。他會重複軟體開發人員慣用的悲歎:“它比我想象的要複雜得多!”
脆弱性
脆弱性是指,在進行一個改動時,程式的許多地方就可能出現問題。常常是,出現新問題的地方與改動的地方並沒有概念上的關聯。要修正這些問題就又會引出新的問題,從而使軟體Team Dev就像一隻不停追逐自己尾巴的狗一樣。
牢固性
牢固性是指,設計中包含了對其他系統有用的部分,但是要把這些部分從系統中分離出來需要的努力和風險是巨大的。這是一件令人遺憾的事,但卻是非常常見。
粘滯性
當面臨一個改動時,開發人員常常會發現會有多種改動的方法。其中,一些會保持設計;而另外一些會破壞設計(也就是生硬的手法)。當那些可以保持系統設計 的方法比那些生硬手法更難應用時,就表明設計具有高的粘滯性。做錯誤的事情是容易的,但是做正確的事情卻很難。這樣就很難保持項目中的軟體設計。
不必要的複雜性
如果設計中包含當前沒有用的組成部分,它就含有不必要的複雜性。當開發人員預測需求的變化,並在軟體中放置了處理潛在變化的代碼時,常常會出現這種情況。起初,這樣看起來是一件好事。畢竟,為將來的變化做準備會保持代碼的靈活性,而且可以避免以後再進行痛苦的改動。
糟糕的是,結果常常正好相反。為過多的可能性作準備,致使設計中含有絕不會用到的結構,從而變得混亂。一些準備也許會帶來回報,但是更多的不會。期間,設計背負著這些不會用到的部分,使軟體變得複雜,而且難以理解。
不必要的重複
複製(Copy)和粘貼(paste)也許是有用的文本編輯(text-editing)操作,但是它們卻是災難性的代碼編輯(code-editing)操作。時常,軟體系統都是構建於眾多的重複代碼片斷之上。
當系統中有重複代碼時,對系統進行改動會變得困難。在一個重複的代碼體中發現的錯誤必須要在每個重複體中一一修正。不過,由於每個重複體之間都有細微的差別,所以修正的方式也不總是相同的。
晦澀性
晦澀性是指,代碼模組難以理解。當開發人員最初編寫一個模組時,代碼對於他們來說看起來也許是清晰的。這是由於他們使自己專註於代碼的編寫,並且他們對 於代碼非常熟識。在熟識減退以後,他們或許會回過頭來再去看那個模組,並想知道他們為什麼會編寫出如此糟糕的代碼。為了防止這種情況發生,開發人員必須要 站在代碼閱讀者的位置,共同努力對他們的代碼進行重構。
1 什麼激發了軟體的腐化
什麼激發了軟體的腐化?答案是需求的變化。由於需求沒有按照初始設計預見的方式進行變化,從而導致了設計的退化。通常,改動都很急迫,並且進行改動的開發人員對原始的設計思路並不熟識。因而,雖然對設計的改動可以工作,但是它卻以某種方式違反了原始的設計。隨著改動的不斷進行,這些違反不斷地積累,設計開始出現臭味。
然而,我們不能因為設計的退化而責怪需求的變化。作為開發人員,我們對需求變化有非常好的瞭解。事實上,我們中的大多數人都認識到需求是項目中最不穩定的因 素。如果我們的設計由於持續、大量的需求變化而失敗,那就表明我們的設計和實踐本身是有缺陷的。我們必須要設法找到一種方法,使得設計對於變化具有彈性, 並且應用一些實踐來防止設計腐化。
2 設計腐化的例子
老闆給你的任務。。。。。。
老闆一大早就來找你,要你務必在三個星期內完成這樣一個程式:從鍵盤讀入字元,並輸出到印表機。
你是一個很有效率的開發人員,僅僅用了兩個星期就把程式完成了(Copy V1):
void Copy()
{
int c;
While ((c = RdKbd()) !=EOF)
WrtPrt(c);
}
你把程式編譯好後,安裝在公司裡的234個工作站。你的程式運行良好,3個月內一點問題都沒有,於是同事都齊聲讚揚你,老闆也開始賞識你。你自己也開始飄飄然了。
需求在變化。。。。。。
三個月後的某天的某個上午,老闆又來找你,說有時希望能從紙帶讀入機讀入資訊。你咬牙切齒,翻著白眼。你想知道為何人們總是改變需求。你的程式不是為紙 帶讀入機設計的!你警告老闆,這樣的改變會破壞程式的優雅。不過老闆怒視了你一下,你又立刻低下了頭,開始想解決方案了。
因為程式已經 安裝到數百個工作站,你不能改變Copy程式的介面。改變介面會導致長時間的重新編譯和重新測試。單單系統測試工程師就會痛恨你,更別提配置控制組的那7 個傢伙了。並且過程式控制制部門會用專門的一天時間來對所有調用了Copy的模組進行各種各樣的程式碼檢閱。但是這也難不到你,你巧妙地完成了任務(Copy V2):
// remember to reset this flag
bool ptFlag = false;
void void Copy()
{
int c;
While ((c = (ptFlag ? Rdpt() : RdKbd())) !=EOF)
WrtPrt(c);
}
想讓Copy程式從紙帶讀入機讀入資訊的調用者必須把ptFlag設定為true,然後再調用Copy時,它就能正確地從紙帶讀入機讀入資訊。一旦 Copy調用返回,調用者必須重新設定ptFlag,否則接下來的調用者就會錯誤地從紙帶讀入機而不是鍵盤讀入資訊。為了提醒程式員重設這個標誌,你增加 了一個適當的注釋。
同樣,你的程式一發布,就獲得了好評。甚至比以前更成功,一大群渴望的程式員正在等待機會去使用它。生活是美好的。
得寸進尺。。。。。。
美好的日子過得總是太快,幾個禮拜後的那天早上老闆又來光顧你,他說:客戶有時希望Copy程式可以輸出到紙帶穿孔機上。
客戶!他們總是毀壞你的設計。如果沒有客戶,編寫軟體會變得容易得多。
你再次警告老闆,如果繼續以這樣可怕的速度變更需求,那麼在年底前軟體就會變得難以維護了。老闆心照不宣地點點頭,接著告訴你無論如何都要進行這次改動。
這次的改動和上次相似,只不過需要另外一個全域變數,下面的程式展示了你努力後的卓越成果(Copy V3):
// remember to reset these flags
bool ptFlag = false;
bool punchFlag = false;
void Copy()
{
int c;
While ((c = (ptFlag ? Rdpt() : RdKbd())) != EOF))
punchFlag ? WrtPunch(c) : WrtPrc(c);
}
尤其讓你感到驕傲的是,你還記得去修改注釋。雖然,你對程式的結構開始變得搖搖欲墜感到擔心。任何對於輸入或者輸出裝置的再次變更肯定會迫使你對 while迴圈的條件判斷進行徹底的重新組織。但是畢竟你的程式還能正常工作。不過現在已經到達你承受的底線了,如果可惡的客戶再次通過改變需求來破壞你 的設計你就立刻走人。你下定了這個決心。
你的崩潰。。。。。。
很不幸,沒過兩個星期。那天早上你剛到辦公室還沒坐下,老闆又跑了進來,看他焦急的神態你猜得出他已經等了你3個小時了。老闆開門見山地說:客戶有時希望Copy程式可以從檔案中輸入……
沒等他把話說完,你已經衝出了辦公室,消失在茫茫的晨曦當中。
2.1 運用物件導向設計原則設計Copy程式
讓我們換個情境來處理上面的情況如何?~^_^~
1、 當老闆第一次給你任務時,你還沒預計到任何需求的變化,所以一開始編寫的代碼和“Copy V1”完全一樣。
2、 在老闆要求你使程式可以從紙帶讀入機中讀入資訊時,你作出了下列的反應:
class Reader
{
public:
virtual int read() = 0;
};
class KeyBordreader : public Reader
{
public:
virtual int read() { return RdKbd();}
}
KeyBordReader GdefaultReader;
void Copy(Reader& reader = GdefaultReader)
{
int c;
While((c = reader.read()) != EOF)
WrtPrt(c);
}
3、 在老闆要求你使程式可以輸出到紙帶穿孔機時,你作出了下列的反應:
class Reader
{
public:
virtual int read() = 0;
};
class KeyBordreader : public Reader
{
public:
virtual int read() { return RdKbd();}
}
class Writer
{
public:
virtual void writ(int c) = 0;
};
class PrinterWriter : public Writer
{
public:
virtual void write(int c) { WrtPrc(c);}
}
KeyBordReader GdefaultReader;
PrinterWriter GdefaultWriter;
void Copy(Reader& reader = GdefaultReader, Writer& writer)
{
int c;
While((c = reader.read()) != EOF)
writer.write(c);
}
在要實現新需求時,你抓住這次機會去改進設計,以便設計對於將來的同類變化具有彈性,而不是設法去給設計打補丁。從第一次改進開始,無論何時老闆要求一種 新的輸入裝置,你都能以不導致Copy程式退化的方式作出響應;從第二次改進開始,無論何時老闆要求一種新的輸入或輸出裝置,你也能以不導致Copy程式 退化的方式作出響應。
但請注意,你不是一開始設計該模組時就試圖預測程式將如何變化。相反,你是以最簡單的方式編寫的。直到需求最終確實變化時,你才修改模組的設計,使之對該種變化保持彈性。
註:你的程式遵守了物件導向程式設計中的開放-封閉原則(OCP)和依賴倒置原則(DIP)。[見以下章節]
3 設計的腐化和設計原則
設計的腐化是一種癥狀,是可以主觀(如果不能客觀的話)進行量度的。腐化常常是由於違法了設計原則中的一個或多個所導致的。例如,僵化性常常是由於對開放-封閉原則(OCP)不夠關注的結果。
Team Dev應該運用相應的設計原則來去除腐化。但當軟體還沒出現腐化時不應該應用這些原則。僅僅因為是一個原則就無條件的去遵循它的做法是錯誤的。這些原 則不是可以隨意在系統中到處噴洒的香水。過分遵循這些原則會導致不必要的複雜性(Needless Complexity)的設計臭味,變成另一種腐化。
下一章:物件導向軟體設計原則(三) —— 軟體實體的設計原則
CodeProject