我們如何知道軟件設計的優劣呢?以下是一些拙劣設計的症狀,當軟件出現下面任何一種氣味時,就表明軟件正在腐化。
- 僵化性(Rigidity):很難對系統進行改動,因為每個改動都會迫使許多對系統其他部分的其他改動。
- 脆弱性(Fragility):對系統的改動會導致系統中和改動的地方在概念上無關的許多地方出現問題。
- 牢固性(Immobility):很難解開系統的糾結,使之成為一些可在其他系統中重用的組件。
- 粘滯性(Viscosity):做正確的事情比做錯誤的事情要困難。
- 不必要的復雜性(Needless Complexity):設計中包含有不具任何直接好處的基礎結構。
- 不必要的重復(Needless Repetition):設計中包含有重復的結構,而該重復的結構本可以使用單一的抽象進行統一。
- 晦澀性(Opacity):很難閱讀、理解。沒有很好地表現出意圖。
僵化性
僵化是指難以對軟件進行改動,即使是簡單的改動。如果單一的改動會導致有依賴關系的模塊中的連鎖改動,那么設計就是僵化的。必須要改動的模塊越多,設計就越僵化。
大部分開發人員都遇到這樣的情況:他們對被要求進行一個看似簡單的改動,當他實際進行改動時,才發現有許多改動帶來的影響自己並沒有預測到。最后,改動所花費的時間要遠比初始估算長。他會重復軟件開發人員慣用的悲嘆:“它比我想象的要復雜得多!”
脆弱性
脆弱性是指,在進行一個改動時,程序的許多地方就可能出現問題。常常是,出現新問題的地方與改動的地方並沒有概念上的關聯。要修正這些問題就又會引出新的問題,從而使軟件開發團隊就像一只不停追逐自己尾巴的狗一樣。
牢固性
牢固性是指,設計中包含了對其他系統有用的部分,但是要把這些部分從系統中分離出來需要的努力和風險是巨大的。這是一件令人遺憾的事,但卻是非常常見。
粘滯性
當面臨一個改動時,開發人員常常會發現會有多種改動的方法。其中,一些會保持設計;而另外一些會破壞設計(也就是生硬的手法)。當那些可以保持系統設計 的方法比那些生硬手法更難應用時,就表明設計具有高的粘滯性。做錯誤的事情是容易的,但是做正確的事情卻很難。這樣就很難保持項目中的軟件設計。
不必要的復雜性
如果設計中包含當前沒有用的組成部分,它就含有不必要的復雜性。當開發人員預測需求的變化,並在軟件中放置了處理潛在變化的代碼時,常常會出現這種情況。起初,這樣看起來是一件好事。畢竟,為將來的變化做准備會保持代碼的靈活性,而且可以避免以后再進行痛苦的改動。
糟糕的是,結果常常正好相反。為過多的可能性作准備,致使設計中含有絕不會用到的結構,從而變得混亂。一些准備也許會帶來回報,但是更多的不會。期間,設計背負着這些不會用到的部分,使軟件變得復雜,而且難以理解。
不必要的重復
復制(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)不夠關注的結果。
開發團隊應該運用相應的設計原則來去除腐化。但當軟件還沒出現腐化時不應該應用這些原則。僅僅因為是一個原則就無條件的去遵循它的做法是錯誤的。這些原 則不是可以隨意在系統中到處噴灑的香水。過分遵循這些原則會導致不必要的復雜性(Needless Complexity)的設計臭味,變成另一種腐化。
下一章:面向對象軟件設計原則(三) —— 軟件實體的設計原則