設(shè)計-直接不等于簡單
了解XP(極限編程)的人都知道,XP有一項實踐叫做簡單設(shè)計(simple design),站在這項實踐對立面的是過度設(shè)計。當(dāng)我們從客戶價值的中心視角去審視那些我們遇到過的過度設(shè)計,自然而然就會得出一個結(jié)論:
“又TM被那些美其名曰項目經(jīng)理和程序員的孫子們給忽悠了,這些功能我其實都用不到,但我還花了這么多冤枉錢去購買,下次議價時一定要砍掉80%的預(yù)算。”
一旦得出這個結(jié)論,那么很快客戶和開發(fā)團(tuán)隊將陷入無止境的撕逼狀態(tài),群體攻擊增強(qiáng)300%,單體理智降低80%,所以為了避免程序猿的世界被破壞,并從根本上保障碼農(nóng)群體可憐的經(jīng)濟(jì)來源,就應(yīng)當(dāng)想辦法給客戶這樣一種錯覺:
“你要的功能必須值這個價,如果想要新增一個功能就應(yīng)該要額外收費。”
對于開發(fā)人員而言,想在這場博弈中獲勝的最佳方法就是砍掉那些完全只為滿足自我虛榮心(以此證明自己技藝是如何爐火純青)的多余設(shè)計和實現(xiàn),只完美地產(chǎn)出客戶真正需要和關(guān)心的功能,這就是簡單設(shè)計。
似乎簡單的直接設(shè)計
理論總是非常easy,但是,請注意這里的但是,由于漢字的博大精深和內(nèi)涵豐富,再遇上程序員這種伴隨二進(jìn)制進(jìn)化的只有0和1二個極端的特殊生物,“簡單”一詞的含義被引申到了更廣的范圍,演化成了簡單粗暴,出現(xiàn)了一種在編碼中隨處可見的風(fēng)景——我稱之為直接設(shè)計(directly design)。
直接設(shè)計看上去像是一種“按圖索驥”的編程方法,開發(fā)人員將流程圖上的處理及分支用直白的代碼表達(dá)出來,比如最近在工作中遇到的一個例子:
設(shè)備對于端口的獲得信息默認(rèn)情況下需要進(jìn)行處理,當(dāng)端口被配置為A或B類型時,則該端口獲得的信息無需處理,轉(zhuǎn)化為流程圖如下。
產(chǎn)生的代碼如下: 例1
@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);
}
}
也許團(tuán)隊中有那么一兩個了解過clean code和重構(gòu)的人,那么這段代碼可能演變成如下: 例2
@Override
public void onMsgRecvdFromPort(RecvMsg msg) {
checkNotNull(msg);
if (msg.getIn().getPortType() == InPortType.A || msg.getIn().getPortType() == InPortType.B) {
doRecord();
} else {
handleMsg(msg);
}
}
但這還不夠,再改造一下: 例3
@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;
}
現(xiàn)在看上去似乎舒服多了,代碼也好理解了,進(jìn)行到這一步代碼可以算是大的提升,但是這就結(jié)束了嗎?其實這只是轉(zhuǎn)嫁了問題,問題并沒有結(jié)束,因為現(xiàn)在 isPortTypeAOrB方法開始變得復(fù)雜難懂起來。不論編碼資歷深淺,大多數(shù)開發(fā)人員都寫過類似例1的代碼,這些直接設(shè)計總是自覺或不自覺地跑出 來,像個幽靈一樣。那么這些直接設(shè)計從何而來?
審視自己的經(jīng)歷,直接設(shè)計代碼產(chǎn)生的原因有很多,歸結(jié)起來有以下幾種可能性:
-
習(xí)慣于面向過程編程的開發(fā)人員轉(zhuǎn)向面向?qū)ο?,慣性使然
-
新手們被要求嚴(yán)格地按規(guī)劃的流程編碼,這是最快地讓新手熟練起來的方法
-
開發(fā)人員誤解了簡單的含義,認(rèn)為簡單就是直接,忽視了設(shè)計,也即簡單而不設(shè)計
人人都愛直接設(shè)計,不只是開發(fā)人員,因為那樣不費腦力,有章可循,且按圖索驥后責(zé)任就變成了流程的設(shè)計人員,既可以輕輕松松,又能趨利避害,不這么 做似乎于情于理都很難說過去。其實直接設(shè)計并不代表代碼質(zhì)量有問題,相反只要意圖足夠清晰和簡單,那么還是要推薦直接設(shè)計,畢竟開發(fā)人員都是這樣被教育出 來的。但是直接設(shè)計有一個很突出的缺陷——丑陋,因為總是會把過多的細(xì)節(jié)暴露出來,尤其是在分支處理上,就像上面的例1那樣。
也許有人覺得這樣直接挺清晰,挺容易理解,其實問題也就在這里,現(xiàn)在這樣的分支只有兩個,當(dāng)用戶覺得這樣的需求還不能滿足需要時,就會要求更多,也許會有5個,10個甚至近百個分支,那時對于開發(fā)人員而言就要不斷地增加新的分支代碼,就像下面的代碼這樣。
@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);
}
}
并且在新增分支時還要小心翼翼地考慮與原有分支的邏輯關(guān)系,嵌套分支看來是在所難免了,用不了幾個迭代,這些代碼就會變得一堆意大利面條。
也許,萬幸的是,功能都實現(xiàn),你幸福地點上一根煙,滿足地看著自己的杰作,突然,有個新手菜鳥心懷崇敬地問你:“大牛,這段代碼是什么意思?”,你 盯著代碼半天心里嘀咕著,這TM是什么鬼,我怎么也看不懂了,然后只好敷衍地回答一句“這個不明白嗎?回去看看設(shè)計文檔!”,好不容易打發(fā)走了這個新手, 項目經(jīng)理找到了你,告訴了你一個晴天霹靂,客戶又改需求了,可能又要新增十幾個分支,你眼前一黑,感嘆一聲又要加班了,但又不得不重新重頭解讀一遍自己創(chuàng) 作的一切,看看哪里能夠插入一個新需求,于是加班又開始了。
簡單設(shè)計需要設(shè)計
直截了當(dāng)?shù)卦O(shè)計過多地暴露細(xì)節(jié)造成擴(kuò)展性和維護(hù)性也直截了當(dāng)?shù)叵陆担@種結(jié)局是所有開發(fā)人員都努力想避免的,如此看來簡單設(shè)計并不簡單,關(guān)鍵是設(shè)計,因為簡單設(shè)計更需要設(shè)計,套用一句經(jīng)典的廣告語:簡約而不簡單,這才是簡單設(shè)計想到達(dá)到的目的。現(xiàn)在試著重新解讀簡單設(shè)計,個人認(rèn)為簡單設(shè)計原則可以分成三個層次:
-
實現(xiàn)具有用戶價值的需求,簡單的說就是用戶要什么你就給他什么
-
代碼設(shè)計應(yīng)當(dāng)職責(zé)簡單,簡單地說就是做好一件事
-
設(shè)計應(yīng)盡可能針對一到兩個問題展開,做到即設(shè)計要簡單,足夠針對性的解決問題即可
讓我們看看從上面角度怎么來設(shè)計,仍然以上面的例子為例。根據(jù)這個原則,將上述需求實例化,可以得到:
-
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
將端口類型進(jìn)行歸納,可以發(fā)現(xiàn)其實端口是否處理消息由端口類型決定,一種端口類型是不需要處理消息類型,而另一種則是需要處理類型,因此端口消息處 理只需要關(guān)心哪些端口是屬于需要處理的類型即可。從這點出發(fā)可以看出例1做了太多可以委托他人去做的事情,因此設(shè)計上需要考慮將功能分離,特別是判斷邏輯 與功能主體剝離,使得單個主體的功能盡量簡單來滿足簡單設(shè)計的第二條原則,按照上述思路,轉(zhuǎn)化為如下代碼:
@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);
}
doRecord();
}
private boolean filter(RecvMsg msg) {
return DisabledPortFilter.getInstance().contains(msg.getIn());
}
而DisabledPortFilter負(fù)責(zé)管理禁用端口,提供注冊及過濾功能,如下:
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定義如下:
public interface FilterRule {
public boolean matchFilter(InPort inPort);
}
將例1中在一個方法中執(zhí)行的過程分解到多個類中,每個類的職責(zé)更為單一,將復(fù)雜的過濾邏輯通過轉(zhuǎn)化放在各個實現(xiàn)類中,也可以幫助開發(fā)者及維護(hù)者能夠 在某一時間點只關(guān)注其中某一中過濾規(guī)則。完成上述轉(zhuǎn)化后,原來可能冗余繁復(fù)的分支處理消失了,取而代之的是短短的幾行簡單易懂的代碼。并且轉(zhuǎn)化后還帶來了 維護(hù)上的便利與代碼擴(kuò)展性的提升,當(dāng)客戶新增需求時,只需要增加對應(yīng)的FilterRule實現(xiàn),并注冊到DisabledPortFilter中就可 以,而不用去修改原有代碼,不知不覺中又契合了OCP原則。 對照前后例子,發(fā)生變化原因是針對邏輯判斷與功能主體分離這一點問題進(jìn)行了設(shè)計,后面的設(shè)計都是在此基礎(chǔ)上展開,一次只設(shè)計一個切入點使得開發(fā)人員更容易 控制開發(fā)思路,而不至于過多復(fù)雜的設(shè)計帶來的思維混亂,因此簡單設(shè)計原則中的第三條顯得尤為重要,很多時候是我們自己想的太多而導(dǎo)致停滯不前,舉步維艱。
簡單設(shè)計之路
簡單設(shè)計是一條光明大道,但通向簡單設(shè)計的路卻并不簡單,布滿荊棘,很多時候并非我們不知道簡單設(shè)計,而是在一次次與時間、進(jìn)度博弈的過程中自覺或 不自覺地放棄了簡單設(shè)計,不少簡單設(shè)計只需要我們再多想那么一點點,捅破這層窗戶紙并不難,要做的只是多想一點,多看一眼,往往這片刻的思考就會對我們的 編碼產(chǎn)生巨大的影響,這也正是通向簡單設(shè)計道路上唯一可以依靠的工具,你要做的只是多想一點,多看一眼。