什麼是軟體開發中的過度設計?
對於一個經驗不足的程序員,什麼時候應該意識到這是過度設計呢?
「適度」的設計又有哪些準則?
只要team裡面的人沒有牛逼到來什麼需求都可以吧代碼重構到恰好滿足那個需求的最佳狀態,那麼我們在開發的時候總是要考慮一下未來的需求到底會往哪個方向走。
你蒙中了,就叫正交分解。
你沒蒙中,就叫過度設計。
來個形象點的故事........ -----------請大家相信我,這個真的是來自天才小熊貓的新浪微博。
補充下:樓上高分答案談的是「功能上的過度設計」,可以理解為發生在產品經理身上的問題。
而我談的是「程序上的過度設計」,是發生在程序員身上的問題。這兩者基本上是兩個方面的問題,諸位看官看前務必心理有數===================================================
頂樓童鞋舉的例子很精彩,不過過於極端了,實際上的過度設計往往發生得隱蔽和普遍得多。
簡單來說,過度設計就是進行了過多的面向未來的設計,進行了不必要的抽象封裝,為系統增加了不必要的複雜度。
舉個例子,你要做一個功能模塊,但你考慮到到這個系統里還有幾個未完成的模塊和你要做的東西類似,所以你決定為此額外做一些抽象和封裝,以便將來複用。然而到後來你開發那些相似的模塊時你才發現,可能是由於抽象不足或抽象錯誤,你不得不重新修改之前的封裝才能完成復用,導致最終成本實際上還不如不做;或者你發現復用的部分所降低的成本實際上還不如包裝花費的成本。 這些都是最常見的過度設計的例子。
程序員在掌握了一些基本的設計能力之後,最常見也是最難克服的設計問題往往就是過度設計。上面的錯誤我相信大多數人都一而再,再而三的的犯過。與過度設計相對的就是設計不足。
雖然是兩個相對的概念,但設計不足和過度設計絕大多數時候都是一起出現的。都是最常見的設計問題。設計不足不僅常見於新手,老手也常犯。甚至我還見過有一類老程序員在經歷過多次過度設計的打擊之後,轉向另一個極端,否定抽象封裝的作用,走上「反設計」的道路。過度設計和設計不足的平衡問題沒有很好的解決辦法,只有依靠經驗的積累和不斷的總結思考。如何把握這個度是最能考驗程序員的經驗和價值的問題之一。
我所嘗試過的軟體方法中,有一種方法的思維方式對於解決這個問題幫助最大,就是TDD(測試驅動開發),這裡簡單說下為什麼TDD能解決這個問題:
TDD的一個核心思想是小步增量,不斷重構。具體說來就是TDD有兩個狀態(常見的說法是兩頂帽子):狀態A:用test case描繪需求,並使用最簡單的方式滿足這個test case。注意要用最簡單的方式滿足這個需求,不能為任何test case之外的需求做任何設計。 test case通過之後進入狀態B;狀態B:重構代碼,讓現有的代碼在盡量保持簡單性的同時足夠優雅清晰。注意此時你只能對現有的實現代碼進行重構,不能增加任何新的功能和test case。整個TDD的過程就是在這兩個狀態間不斷轉換的過程。在狀態A增加功能,在狀態B優化設計。TDD的這種思維方式走的稍微極端一點。它直接排斥任何對未來的設計,轉而以優雅簡潔的設計和test case來為未來需求的重構降低成本。 可以說嚴格遵循TDD做出來的設計必然在過度設計和設計不足方面都不會有太大的問題。
我嚴重推薦TDD。不管你最終會不會接受TDD這種開發方式,它獨特的思維方式都必然會給你的設計觀念帶來很大影響。為了設計而設計,過度最求模塊化,可擴展性,設計模式等。
一個經驗不足的程序員如果太過關注設計,極有可能導致設計過度。因為經驗不足,所以很難理解抽象層次,也就無法正確設計,很多時候只是在繞圈子。
一個簡單的例子:XML解析有兩種方法,SAX和DOM,而DOM實際上是基於SAX實現的,不理解本質的程序員有可能為了某些特殊需求將DOM包裹了一層,出來的卻是個SAX解析器。所以新手更容易犯的是錯誤的設計,這和過度設計是有一些差別的。
真正的過度設計容易出現在規模較大的項目,規模大,所以人員多,如果不儘可能預先設計,容易造成返工,浪費資源。設計的適度依賴於架構師的經驗和需求的穩定性,實際上也很難避免過度的設計。
對於一兩個開發的小項目,談論過度設計則有點奇怪,因為在這種情況下導出好設計的唯一手段就是重構。新手想要不經過反覆修改,而一次性解決問題,實際上是在給自己找麻煩。
即使大規模的項目,如果能夠在前期控制參與人員,在小範圍內實現核心功能,並通過重構改良設計,然後再增加人員進行全面開發,也會是一個很好的實踐!過度設計就是鴨子還沒打下來,就在爭論怎麼吃的問題。
軟體的設計,目的是滿足需求,這個需求是多維度的綜合: 業務要求,性能,可靠性,安全……等等。過度設計,就是在某個或者多個維度上超出實際需求的裕度。好比一個網站預計百來人訪問,你給個能支撐500人的設計就夠了,結果設計出一個百萬訪問量的架構來。
實際當中,要避免過度設計,我覺得主要是兩點:
1. 弄清楚需求,不要臆想,在其基礎上適當放寬。
2. 要有成本概念,在工程上,超過需求到一定程度的設計極大概率會有超額成本。
另外不太同意對未來預測進行設計。因為對需求的預測和對股價預測一樣不靠譜,就算你在某個維度猜中了,其它維度上的變化可以讓你的設計面目全非。
前面票數最高的 @柯鼠 的答案雖然很搞笑,但是其實不準確,它混淆了程序設計和產品設計的概念。圖中的軟體雖然最後變得十分詭異,但其實每一個新版本都忠實地實現了新的需求。變得詭異的是需求,而不是程序設計。
程序設計的一切都應該以需求為準。程序的過度設計,並不是你實現了太多的功能,而是為了實現太多你覺得會出現結果沒有出現的需求,導致系統架構過於龐大複雜。所以通常一個程序是否被過度設計,看界面是看不出來的。。。
過度設計這個東西就是想太多,希望你的程序能應對一切需求的變化,希望你的程序擁有一切牛掰的特性,但實際上你水平就在這裡所以你做不到。一旦你想做的超出了你的水平,你很可能把自己絆住,根本實現不出來;或者實現出來太過複雜導致bug多多效率低下;或設計歪了,你發現意料之外的需求根本都加不進去了!那麼對於一個經驗不足的程序員,怎麼意識到這是過度設計呢?沒辦法,設計是否合適,這很大程度上就一個靠主觀判斷的事,沒有經驗就沒法分辨,必須多寫多看多思考。失敗是成功的麻麻,只要想做出合適的設計,不當的設計就是必經之路。你想的經常不會發生,發生的總能刷新你的世界觀。不每天來刷刷經驗,怎麼能更好地認識世界呢_(:3」∠)_大牛們其實就是刷出來的,哪些需要哪些不需要,哪些做完了維護成本可能會比修改成本還要高,怎麼做效率更高,怎麼做可以降低耦合度。。。當然,就算世界觀圓潤的大牛們也會失足的(?)′?`(ヾ)雖然沒法完全避免過度設計,但下面幾個建議,也許能幫上忙:1. 一定記住設計是為了滿足需求,不要為了設計而設計;
2. 不要花費太多精力在你覺得一年之內不會提出來的需求上;3. 如果一個實現能比較容易的改成另一個實現,現在選擇容易的一方;4. 去掉這個中間層會發生什麼?5. 不要花太多時間在設計上,人人都超不出自己的界限;6. 不要指望一下把所有細節都設計出來,邊寫邊重構是個好習慣;其實當你完成一個系統之前都認識不到自己是否有過度設計,而當你認識到自己有過度設計的時候正說明了你在成長。一個設計用一段時間你就知道哪裡有問題了,有條件的話過一段時間就把以前寫的翻出來重構一下,每半年一年你可能都會有新的想法。
資料庫的可移植性
當年可是接受過很多教育的...- 採用Hibernate的好處之一就是,HQL可以幫我們確保將來的不同資料庫之間移植性。
- 為什麼不能寫存儲過程?一個重要的原因就是業務邏輯放到資料庫里會導致資料庫移植成本變大。
- 程序內盡量採用標準SQL語法,因為我們要考慮將來的移植風險。
下面這幾種吧:from 6個變態的C語言Hello World程序。當然了,如果誠如文中所說,為了混淆,這就不算過度設計。否則就不僅僅是過度設計了,而是Drunbility
hello1.c
_____
#define _________ }
#define ________ putchar
#define _______ main
#define _(a) ________(a);
#define ______ _______(){
#define __ ______ _(0x48)_(0x65)_(0x6C)_(0x6C)
#define ___ _(0x6F)_(0x2C)_(0x20)_(0x77)_(0x6F)
#define ____ _(0x72)_(0x6C)_(0x64)_(0x21)
#define _____ __ ___ ____ _________
#include&
hello2.c
#include&
main(){
int x=0,y[14],*z=y;*(z++)=0x48;*(z++)=y[x++]+0x1D;
*(z++)=y[x++]+0x07;*(z++)=y[x++]+0x00;*(z++)=y[x++]+0x03;
*(z++)=y[x++]-0x43;*(z++)=y[x++]-0x0C;*(z++)=y[x++]+0x57;
*(z++)=y[x++]-0x08;*(z++)=y[x++]+0x03;*(z++)=y[x++]-0x06;
*(z++)=y[x++]-0x08;*(z++)=y[x++]-0x43;*(z++)=y[x]-0x21;
x=*(--z);while(y[x]!=NULL)putchar(y[x++]);
}
hello3.c
#include&
#define __(a) goto a;
#define ___(a) putchar(a);
#define _(a,b) ___(a) __(b);
main()
{ _:__(t)a:_(r,g)b:_($,p)
c:_(l,f)d:_( ,s)e:_(a,s)
f:_(o,q)g:_(l,h)h:_(d,n)
i:_(e,w)j:_(e,x)k:_(
,z)
l:_(H,l)m:_(X,i)n:_(!,k)
o:_(z,q)p:_(q,b)q:_(,,d)
r:_(i,l)s:_(w,v)t:_(H,j)
u:_(a,a)v:_(o,a)w:_(),k)
x:_(l,c)y:_( ,g)z:___(0x0)}
hello4.c
int n[]={0x48,
0x65,0x6C,0x6C,
0x6F,0x2C,0x20,
0x77,0x6F,0x72,
0x6C,0x64,0x21,
0x0A,0x00},*m=n;
main(n){putchar
(*m)!= ?main
(m++):exit(n++);}
hello5.c
main(){int i,n[]={(((1&<&<1)&<&<(1&<&<1)&<&<(1&<&< 1)&<&<(1&<&<(1&>&>1)))+((1&<&<1)&<&<(1&<&<1))), (((1 &<&<1)&<&<(1&<&<1)&<&<(1&<&<1)&<&<(1&<&<1))-((1&<&<1)&<&<( 1&<&<1)&<&<(1&<&<1))+((1&<&<1)&<&<(1&<&<(1&>&>1)))+ (1
&<&<(1&>&>1))),(((1&<&<1)&<&<(1&<&<1)&<&<(1&<&<1)&<&< (1 &<&<1))-((1&<&<1)&<&<(1&<&<1)&<&<(1&<&<(1&>&>1)))- ((1
&<&<1)&<&<(1&<&<(1&>&>1)))),(((1&<&<1)&<&<(1&<&<1)&<&<(1 &<&<1)&<&<(1&<&<1))-((1&<&<1)&<&<(1&<&<1)&<&<(1&<&<(1&>&>1
)))-((1&<&<1)&<&<(1&<&<(1&>&>1)))),(((1&<&<1)&<&< (1 &<&<1)&<&<(1&<&<1)&<&<(1&<&<1))-((1&<&<1)&<&<(1&<&<1)&<&<( 1&<&<(1&>&>1)))-(1&<&<(1&>&>1))),(((1&<&<1)&<&<(1&<&<1 )&<&<(1&<&<1))+((1&<&<1)&<&<(1&<&<1)&<&<(1&<&<(1&>&>1)))
-((1&<&<1)&<&<(1&<&<(1&>&>1)))),((1&<&<1)&<&< (1&<&<1) &<&<(1&<&<1)),(((1&<&<1)&<&<(1&<&<1)&<&<(1&<&<1)&<&<(1&<&< 1))-((1&<&<1)&<&<(1&<&<1))-(1&<&<(1&>&>1))),(((1&<&< 1)&<&<(1&<&<1)&<&<(1&<&<1)&<&<(1&<&<1))-((1&<&<1)&<&< (1 &<&<1)&<&<(1&<&<(1&>&>1)))-(1&<&<(1&>&>1))), (((1&<&<1 )&<&<(1&<&<1)&<&<(1&<&<1)&<&<(1&<&<1))- ((1&<&<1)&<&< (1 &<&<1)&<&<(1&<&<(1&>&>1)))+(1&<&<1)), (((1&<&<1)&<&< ( 1&<&<1)&<&<(1&<&<1)&<&< (1&<&<1))-((1&<&<1)&<&< (1&<&<1) &<&<(1&<&<(1&>&>1)))-((1&<&<1) &<&<(1&<&< (1&>&>1)))),
(((1&<&<1)&<&< (1&<&<1)&<&<(1&<&<1)&<&< (1&<&<1))- ((1 &<&<1)&<&<(1&<&<1)&<&<(1&<&<1))+((1&<&<1)&<&< (1&<&<(1&>&>
1)))), (((1&<&<1)&<&<(1&<&<1) &<&<(1&<&<1))+(1&<&<(1 &>&>1))),(((1&<&<1)&<&<(1&<&<1))+((1&<&<1)&<&< (1&<&<( 1&>&>1))) + (1&<&< (1&>&>1)))}; for(i=(1&>&>1);i
&<(((1&<&<1) &<&<(1&<&<1))+((1 &<&<1)&<&< (1&<&<(1&>&>1
))) + (1&<&<1)); i++) printf("%c",n[i]); }hello6.cpp
下面的程序只能由C++的編譯器編譯(比如:g++)
#include &
#define _(_) putchar(_);
int main(void){int i = 0;_(
++++++++++++++++++++++++++++
++++++++++++++++++++++++++++
++++++++++++++++++++++++++++
++++++++++++++++++++++++++++
++++++++++++++++++++++++++++
++++i)_(++++++++++++++++++++
++++++++++++++++++++++++++++
++++++++++i)_(++++++++++++++
i)_(--++i)_(++++++i)_(------
----------------------------
----------------------------
----------------------------
----------------------------
----------------i)_(--------
----------------i)_(++++++++
++++++++++++++++++++++++++++
++++++++++++++++++++++++++++
++++++++++++++++++++++++++++
++++++++++++++++++++++++++++
++++++++++++++++++++++++++++
++++++++++++++++++++++++++i)
_(----------------i)_(++++++
i)_(------------i)_(--------
--------i)_(----------------
----------------------------
----------------------------
----------------------------
----------------------------
------i)_(------------------
----------------------------
i)return i;}
想多了
多想三步的可能性,但只多做一步的設計,基本就可以了
多想三步,就要多做三步,就是過度設計,因為越面向未來的設計,越沒有依據,你還沒做完,產品改需求了感覺前面一些關於 「界面功能過於複雜甚至於混亂」 的例子並不是題主所關注的過度設計。題主所問的應該是程序開發中的過度設計。
「過度設計」這個詞在日常使用中其實是個模糊,主觀的概念:基本上就是表達說話人認為,這個設計中某個東西其實沒有必要做或者不需要在當前版本做。為什麼說是主觀,因為「過度設計」這類評語通常是在設計審查階段提出的。而事實是在系統實際上線並穩定運行一段長時間之前,根本無法客觀地確定什麼是「適度設計」。
說起「過度設計」時,不少人會認為「適度設計」就是以最快的開發效率和最優的運行效率實現目前已知的所有需求。但這顯然不對,否則任何人只要能認字(讀需求文檔),會編程就能做出最合適的設計決定,還要架構師來幹什麼,程序設計也不會發展成今天的樣子(至少面向對象就沒必要出來了)。現實中在產品交付前需求是可能會變動的(可參考 @柯鼠 的例子,如果是持續服務型的產品,交付後的需求變動也需要考慮),而且開發過程中人員也可能發生變動。那麼總體來說,最優的設計應該把最可能發生的需求變動和人員變動考慮在內:
對付需求變動(所謂代碼靈活)至少需要做到代碼不重複(單點改動),高聚合(細分功能代碼,提倡小對象,短方法),低耦合(隔離變動);對付人員變動則至少需要做到代碼易讀,符合思維習慣。
在大部分情況下,靈活 - 易讀 - 效率 三者是一組 Trade-off (實在想不起中文表達是啥……)。更靈活,意味著更精細的功能分解和更抽象的概念(以便復用)。功能細分會影響局部開發效率(需要碼更多的字)和整體運行效率(介面調用和傳參增多),概念抽象影響易讀性(原則上每引入一個新的概念都會增加閱讀負擔)。同理,追求易讀也會一定程度上影響開發效率(碼更多的字)。最後問題歸結於:如何找到平衡點。
更大的問題是,這些變動都是」可能發生「,在設計期沒人能確定事實上到底會不會發生,會在哪個部位發生。如果以」實踐是檢驗真理的唯一標準「來看,最後證實蒙對了的,就是適度設計,蒙錯了,就是過度設計,但這種馬後炮對於新手在設計期的判斷沒有任何幫助。在現實中,唯一的依靠就是經驗 —— 資深程序員經過無數次」考慮不周「和」過度設計「的教訓,培養出來的什麼地方應該未雨綢繆的直覺 (或者按馬丁福勒的說法:代碼的氣味)。
因此,當你聽到有人說」這裡是過度設計「時,可能的情況是:
1. 說話人根據他自身經驗,判斷出目前不必要考慮這裡的未來變動,且如果考慮會大大增加工作量,或會顯著影響效率,或引入的概念難以理解而可能影響團隊合作2. 說話人無法判斷是否需要考慮這裡的未來變動,但他認為如果考慮會增加額外的工作量,影響效率,或他覺得你的設計難以理解。如果是1,則他的判斷有助於讓設計趨向於」合適設計「。如果是2,則他也是在蒙。而且,如果你對他的經驗和能力不充分了解,也無法確認事實上是哪種情況。
所以,總而言之,在現實中你聽到」過度設計「一類的評語,如果說話者不是你在技術上特別佩服的人或者你的頂頭上司,基本可以忽略(他認為是過度而已,反過來說你也可以認為他考慮不周),最多可以當一個提醒,複查一下設計。
==================================
第二個問題:如何避免過度設計既然在設計期無法準確定義」合適設計「,自然也無法準確定義「過度設計」,因此也不會有一套明確按部就班的方法去避免過度設計。但有一些指導原則可以有助於讓你的設計趨向合適設計:
1. 對自己參與過,並經過實踐證實」考慮不周「和」過度設計「的案例多做總結。特別應該注意,不要出現幾次了過度設計的失誤,就完全走向反面,不做任何設計。
2. 多請教有經驗的程序員或架構師3. 不論多簡單的項目,先做設計,再開工 (避免走向過度設計的反面)4. 理解各種經典的設計模式。你可以在充分理解後判斷在某種情況下使用某種模式是過度設計,即使判斷錯誤也不要緊,有下面幾點補救。但不建議在感覺難以理解的前提下認為所有使用這種模式的企圖都是過度設計。5. 在沒有任何經驗可以借鑒的情況下,不確定是否必要且工作量大或不易理解的靈活性設計一律砍掉,工作量小且不需引入新概念的靈活性設計盡量保留。6. 不要容忍已經出現的重複代碼,發現重複代碼隨時通過重構技巧改為單點7. 在編寫新代碼過程中儘可能把舊代碼中的「臭味」部分重構掉8. 儘可能保證較高(我現在公司的標準是85%以上)的單元測試覆蓋率,保證重構的效率。但對於一些小型項目或者本來就沒有單元測試的遺留項目,有時 「高單元測試覆蓋率」 本身就被認為是」過度設計「,要看管理層的理解了。======================================
最後說一下重構重構是解決「過度設計」的過渡方案,有助於度過從初級程序員到資深程序員之間青黃不接的階段,當積累了足夠經驗,才有可能一次性作出準確的設計判斷。而保證你敢去重構的基礎是單元測試覆蓋率(否則你在重構時手指抽筋引入了一個bug會被罵死)。此外,高單元測試覆蓋率特別是高分支覆蓋率(Branch Coverage)又能反過來鼓勵你進行更好的設計(盡量細分功能單元便於編寫測試,消除重複代碼避免重複測試,等等),能形成一個良性循環。TDD我還沒有真正實踐過,就不在這裡討論了。快速迭代中,如果為了實現一個潛在需求(目前還不是需求)要堆的代碼量和複雜度明顯不能被無視了就可以算過度設計了。
長周期迭代時,基本就是碰運氣了,預測未來是很難的,功能也不是白來的
一般被認為『過度設計』 的系統 都是設計的不好。設計,抽象不是壞事,但是抽象合理的系統必定是容易理解的,讓複雜度下降的而設計的不好的系統,會導致複雜度上升,這就被認為是『過度設計』了
殺毒軟體的遊戲加速,聊天軟體的在線支付。