設計-簡約而不簡單


本文來自hxfirefox,他是筆者在某國內大型企業提供敏捷XP咨詢項目的內部教練。本文也是由他交給筆者幫助review,同時也授權發布在筆者的博客中。

原文地址為:直接不等於簡單

碼農的博弈

了解XP(極限編程)的人都知道,XP有一項實踐叫做簡單設計(simple design),站在這項實踐對立面的是過度設計。當我們從客戶價值的中心視角去審視那些我們遇到過的過度設計,自然而然就會得出一個結論:

“又TM被那些美其名曰項目經理和程序員的孫子們給忽悠了,這些功能我其實都用不到,但我還花了這么多冤枉錢去購買,下次議價時一定要砍掉80%的預算。”

img1

一旦得出這個結論,那么很快客戶和開發團隊將陷入無止境的撕逼狀態,群體攻擊增強300%,單體理智降低80%,所以為了避免程序猿的世界被破壞,並從根本上保障碼農群體可憐的經濟來源,就應當想辦法給客戶這樣一種錯覺:

“你要的功能必須值這個價,如果想要新增一個功能就應該要額外收費。”

對於開發人員而言,想在這場博弈中獲勝的最佳方法就是砍掉那些完全只為滿足自我虛榮心(以此證明自己技藝是如何爐火純青)的多余設計和實現,只完美地產出客戶真正需要和關心的功能,這就是簡單設計。

似乎簡單的直接設計

理論總是非常easy,但是,請注意這里的但是,由於漢字的博大精深和內涵豐富,再遇上程序員這種伴隨二進制進化的只有0和1二個極端的特殊生物,“簡單”一詞的含義被引申到了更廣的范圍,演化成了簡單粗暴,出現了一種在編碼中隨處可見的風景——我稱之為直接設計(directly design)

直接設計看上去像是一種“按圖索驥”的編程方法,開發人員將流程圖上的處理及分支用直白的代碼表達出來,比如最近在工作中遇到的一個例子:

設備對於端口的獲得信息默認情況下需要進行處理,當端口被配置為A或B類型時,則該端口獲得的信息無需處理,轉化為流程圖如下。

flowchart

產生的代碼如下: 例1

1
2
3
4
5
6
7
8
9
10
11
12
@Override public void onMsgRecvdFromPort(RecvMsg msg) {  checkNotNull(msg);  if (msg.getIn().getPortType() == InPortType.A) {  doRecord();  } else if (msg.getIn().getPortType() == InPortType.B) {  doRecord();  } else {  handleMsg(msg);  } } 

也許團隊中有那么一兩個了解過clean code和重構的人,那么這段代碼可能演變成如下: 例2

1
2
3
4
5
6
7
8
9
10
@Override public void onMsgRecvdFromPort(RecvMsg msg) {  checkNotNull(msg);  if (msg.getIn().getPortType() == InPortType.A || msg.getIn().getPortType() == InPortType.B) {  doRecord();  } else {  handleMsg(msg);  } } 

但這還不夠,再改造一下: 例3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override public void onMsgRecvdFromPort(RecvMsg msg) {  checkNotNull(msg);  if (!isPortTypeAOrB(msg)) {  handleMsg(msg);  }  doRecord(); } private boolean isPortTypeAOrB(RecvMsg msg) {  return msg.getIn().getPortType() == InPortType.A || msg.getIn().getPortType() == InPortType.B; } 

現在看上去似乎舒服多了,代碼也好理解了,進行到這一步代碼可以算是大的提升,但是這就結束了嗎?其實這只是轉嫁了問題,問題並沒有結束,因為現在isPortTypeAOrB方法開始變得復雜難懂起來。不論編碼資歷深淺,大多數開發人員都寫過類似例1的代碼,這些直接設計總是自覺或不自覺地跑出來,像個幽靈一樣。那么這些直接設計從何而來?

審視自己的經歷,直接設計代碼產生的原因有很多,歸結起來有以下幾種可能性:

  • 習慣於面向過程編程的開發人員轉向面向對象,慣性使然
  • 新手們被要求嚴格地按規划的流程編碼,這是最快地讓新手熟練起來的方法
  • 開發人員誤解了簡單的含義,認為簡單就是直接,忽視了設計,也即簡單而不設計

人人都愛直接設計,不只是開發人員,因為那樣不費腦力,有章可循,且按圖索驥后責任就變成了流程的設計人員,既可以輕輕松松,又能趨利避害,不這么做似乎於情於理都很難說過去。其實直接設計並不代表代碼質量有問題,相反只要意圖足夠清晰和簡單,那么還是要推薦直接設計,畢竟開發人員都是這樣被教育出來的。但是直接設計有一個很突出的缺陷——丑陋,因為總是會把過多的細節暴露出來,尤其是在分支處理上,就像上面的例1那樣。

也許有人覺得這樣直接挺清晰,挺容易理解,其實問題也就在這里,現在這樣的分支只有兩個,當用戶覺得這樣的需求還不能滿足需要時,就會要求更多,也許會有5個,10個甚至近百個分支,那時對於開發人員而言就要不斷地增加新的分支代碼,就像下面的代碼這樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override public void onMsgRecvdFromPort(RecvMsg msg) {  checkNotNull(msg);  if (msg.getIn().getPortType() == InPortType.A) {  doRecord();  } else if (msg.getIn().getPortType() == InPortType.B) {  doRecord();  } else if (msg.getIn().getPortType() == InPortType.C) {  doRecord();  } else if (msg.getIn().getPortType() == InPortType.D) {  doRecord();  } else if (msg.getIn().getPortType() == InPortType.E) {  doRecord();  }  ...  ...  else {  handleMsg(msg);  } } 

並且在新增分支時還要小心翼翼地考慮與原有分支的邏輯關系,嵌套分支看來是在所難免了,用不了幾個迭代,這些代碼就會變得一堆意大利面條。

pasta_img

也許,萬幸的是,功能都實現,你幸福地點上一根煙,滿足地看着自己的傑作,突然,有個新手菜鳥心懷崇敬地問你:“大牛,這段代碼是什么意思?”,你盯着代碼半天心里嘀咕着,這TM是什么鬼,我怎么也看不懂了,然后只好敷衍地回答一句“這個不明白嗎?回去看看設計文檔!”,好不容易打發走了這個新手,項目經理找到了你,告訴了你一個晴天霹靂,客戶又改需求了,可能又要新增十幾個分支,你眼前一黑,感嘆一聲又要加班了,但又不得不重新重頭解讀一遍自己創作的一切,看看哪里能夠插入一個新需求,於是加班又開始了。

bad_condition

簡單設計需要設計

直截了當地設計過多地暴露細節造成擴展性和維護性也直截了當地下降,這種結局是所有開發人員都努力想避免的,如此看來簡單設計並不簡單,關鍵是設計,因為簡單設計更需要設計,套用一句經典的廣告語:簡約而不簡單,這才是簡單設計想到達到的目的。現在試着重新解讀簡單設計,個人認為簡單設計原則可以分成三個層次:

  • 實現具有用戶價值的需求,簡單的說就是用戶要什么你就給他什么
  • 代碼設計應當職責簡單,簡單地說就是做好一件事
  • 設計應盡可能針對一到兩個問題展開,做到即設計要簡單,足夠針對性的解決問題即可

讓我們看看從上面角度怎么來設計,仍然以上面的例子為例。根據這個原則,將上述需求實例化,可以得到:

  • when port type == A, it should not handle message
  • when port type == B, it should not handle message
  • when port type != A && != B, it should handle message

將端口類型進行歸納,可以發現其實端口是否處理消息由端口類型決定,一種端口類型是不需要處理消息類型,而另一種則是需要處理類型,因此端口消息處理只需要關心哪些端口是屬於需要處理的類型即可。從這點出發可以看出例1做了太多可以委托他人去做的事情,因此設計上需要考慮將功能分離,特別是判斷邏輯與功能主體剝離,使得單個主體的功能盡量簡單來滿足簡單設計的第二條原則,按照上述思路,轉化為如下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override public void onMsgRecvdFromPort(RecvMsg msg) {  checkNotNull(msg);  parseMsg(msg); } private void parseMsg(RecvMsg msg) {  if (!filter(msg)) { // only ports not in disabled list could be parsed  handleMsg(msg);
return;
} doRecord(); } private boolean filter(RecvMsg msg) { return DisabledPortFilter.getInstance().contains(msg.getIn()); }

而DisabledPortFilter負責管理禁用端口,提供注冊及過濾功能,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class DisabledPortFilter {  // FilterRule in HashMap means rule for filting with port  // Sometimes you need to composite multi-conditions to filting, not only type of port  // FilterRule is an interface, so any one wants to use filter should offer an implementation  private HashMap<InPort, FilterRule> disableHandleList = Maps.newHashMap();  private static DisabledPortFilter portFilter = new DisabledPortFilter();  private DisabledPortFilter() {  }  public static DisabledPortFilter getInstance() {  return portFilter;  }  public void registDisabledPort(InPort inPort, FilterRule rule) {  disableHandleList.put(inPort, rule);  }  public void unregistDisabeldPort(InPort inPort) {  disableHandleList.remove(inPort);  }  public boolean contains(InPort in) {  return !disableHandleList.get(in).matchFilter(in);  } } 

FilterRule定義如下:

1
2
3
public interface FilterRule {  public boolean matchFilter(InPort inPort); } 

將例1中在一個方法中執行的過程分解到多個類中,每個類的職責更為單一,將復雜的過濾邏輯通過轉化放在各個實現類中,也可以幫助開發者及維護者能夠在某一時間點只關注其中某一中過濾規則。完成上述轉化后,原來可能冗余繁復的分支處理消失了,取而代之的是短短的幾行簡單易懂的代碼。並且轉化后還帶來了維護上的便利與代碼擴展性的提升,當客戶新增需求時,只需要增加對應的FilterRule實現,並注冊到DisabledPortFilter中就可以,而不用去修改原有代碼,不知不覺中又契合了OCP原則。 對照前后例子,發生變化原因是針對邏輯判斷與功能主體分離這一點問題進行了設計,后面的設計都是在此基礎上展開,一次只設計一個切入點使得開發人員更容易控制開發思路,而不至於過多復雜的設計帶來的思維混亂,因此簡單設計原則中的第三條顯得尤為重要,很多時候是我們自己想的太多而導致停滯不前,舉步維艱。

簡單設計之路

簡單設計是一條光明大道,但通向簡單設計的路卻並不簡單,布滿荊棘,很多時候並非我們不知道簡單設計,而是在一次次與時間、進度博弈的過程中自覺或不自覺地放棄了簡單設計,不少簡單設計只需要我們再多想那么一點點,捅破這層窗戶紙並不難,要做的只是多想一點,多看一眼,往往這片刻的思考就會對我們的編碼產生巨大的影響,這也正是通向簡單設計道路上唯一可以依靠的工具,你要做的只是多想一點,多看一眼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM