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
























