來源:南柯之石
上回的最後,來了兩個使用者,分別提出了兩個不同的需求。一個要求用兩個開關控制一個燈,一個要求用一個開關控制所有的燈。本回將就這兩個需求進行分析。我寫這段話的時候並沒有想出這個需求的具體方案,重要的過程,思路有時候比結果更重要。所以,我的方案可能會”跑偏”;但是如果你能從過程中體會到些什麼,那這篇就沒有白寫。
兩個開關控制一個燈。這個問題好像很簡單,把兩個Switcher的Switchee都設定為同一個燈不就結了嗎?畫個物件圖會是這個樣子。
圖1 由雙開關控制的燈
有問題嗎?
使用者的真實需求
考慮一下這個問題。如果你用Switcher1開了燈,再去開一下Switcher2,燈應該是保持開著還是關了呢?從技術人員的角度來講,呼叫的Switcher的開,當然應該保持開啦。但是策劃會說,這兩個開關應該是相互作用的,還拿出了電路圖給我看。這是的確是張真實情況下的雙開關電路圖。
圖2 雙路開關電路
Switcher1的開關,撥到左邊是開還是關,取決於Switcher2現在是撥在左邊兒還是右邊。電路圖的天然連通性就自然而然地做到了這一點。現實中的Switcher1不會去問Switcher2:嘿,哥們,你現在是個啥狀態?而我們的程式碼中的兩個Switcher間也不應該有什麼交集。
總而言之,在這個需求的要求下,使用者要做的,就是撥一下開關而已(圖3中JustSwitch方法的作用)。
對當前設計的改進
在以上需求的約束下,就第一篇開始所寫的Switcher而言,就會存在著一個問題。先不說雙開關,單單一個開關我們的設計就是不符合產品策劃的要求。因為之前寫的Switcher類是有兩個函式作開關控制的。
1 2 3 4 5 6 |
public class Switcher { public ISwitchable Switchee { get; set; } public void TurnOn() { Switchee.TurnOn(); } public void TurnOff() { Switchee.TurnOff(); } } |
程式碼1
這是有問題的。因為Switcher是直接給使用者用的。你覺得使用者是想用哪種開關呢?
總不能讓使用者根據現在燈是開著還是關著讓使用者按不同的按鈕。(使用不同的函式。)所以Switcher的程式碼應該是這個樣子的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Switcher { private bool isOn; public ISwitchable Switchee { get; set; } public void JustSwitch() { // 根據當前狀態選擇正確的操作。 if (isOn) <span style="color: #0000ff;"> </span>{ Switchee.TurnOff(); isOn = false; } else { Switchee.TurnOn(); isOn = true; } } } |
程式碼2
Switcher自己儲存最後一次操作的結果(當前狀態),並自動選擇正確的操作。
支援雙開關
當每個燈只有一個開關的時候,這個程式碼沒有任何問題。但是出現兩個開關的話就沒這麼好辦了,自己儲存的狀態是無效的,可能會被另一個開關改掉。如果要達到和電路圖一樣的效果,Switcher1要麼問Switcher2現在是什麼狀態,要麼問Light是什麼狀態。
直覺上,問Switcher2這事兒不是個好選擇,因為以後還可能會有Switcher3、4。但燈就一個。但是等等,我們現在的介面是什麼樣的?
圖3. 現在的設計
ISwitchable介面只定義了TurnOn和TurnOff兩個函式,沒有可以用於查詢燈的當前狀態的方法。這太糟糕了,這意味著介面要改了。改介面永遠是最糟糕的事情。《軟體框架設計的藝術》裡說”API就如同恆星,一旦出現,便與我們永恆存在。”,聽上去介面寫了就不能改,但是我們的情況要好很多,這個介面是公司自己定義的,沒有別人用過。所以改改無妨。J只要小小的加一個方法就可以了。
圖4. 新增查詢介面以支援雙開關
Switcher的程式碼會是這樣的。Switcher暴露給使用者的應該只有 一個介面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public interface ISwitchable { void TurnOn(); void TurnOff(); <span style="color: #000000;"> </span>bool IsOn(); } public class Switcher { public ISwitchable Switchee { get; set; } public void JustSwitch() { // 根據當前狀態選擇正確的操作。 if (Switchee.IsOn()) Switchee.TurnOff(); else Switchee.TurnOn(); } } |
程式碼3
另一個極品方案
軟體開發與建築施工的最大區別是,軟體開發可以選擇先蓋地下室還是天花板。
——我
當我們把要做的事情抽象一下,就能很容易地從更高的層次思考問題。比如上面,開關要知道燈的狀態。可以抽象為:
圖5. 開關開燈例子的高度抽象
各位可以想到什麼設計上的問題?比較明顯的問題有兩個。
●拉模式 VS推模式。既然圖中為拉模式,那麼另一個思路就是推模式。也許你聽說一個說法,就是推模式比拉模式要好。但是如果真把推模式用在開關開燈的例子上,就成了燈的亮與熄,要去通知開關,以便開關下次Switch的時候,能做出正確地動作。想到這裡,我邪惡地笑了。這得多蛋痛啊?模式的應用,永遠要看上下文。為了撫慰一些推模式死忠們脆弱的心靈,下面會介紹一個可行的推模式開燈設計。
●A依賴B。雖然我們有ISwitchable介面,開關不直接依賴燈,但是你看,我們為什麼要在ISwitchable介面裡加入IsOn函式呢?因為開關需要知道燈的狀態。所以說,他們之間不但存在著依賴,而且還直接決定了介面的定義。但是與我第一篇文章中介紹的DIP原則是否衝突呢?這取決於你對開關的定位。如果只是單純的開關,那麼IsOn函式的引入,就是對燈的功能的抽象,也就違反了DIP原則;如果你希望開關有點兒AI,那麼顯然它得知道更多的資訊(但是這違反了單一責任原則)。所以看上去,無論從哪個角度來講,IsOn的引入都是要違反XXXX原則的。
好,為了不違反所有現有的原則。構想出設計出如下的設計:
圖6. 引入AI系統對開關操作進行決策(拉模式)
理智一些吧,我們的開關公司沒有上市,既沒有資本做AI系統,也沒有卡馬克這種不要錢只要漢堡和網路的技術狂人,我們的使用者也不會像暗黑的死忠一樣傻等10年,然後等到一個需要接網線才能使用的電燈開關還能用得很愉悅。在這個發展階段要做的,只是儘快滿足當前的需求。
在達成需求前,技術方案的完美度,永遠是第二位的。第一位的,是有效率地執行 + 新穎的思路和方向。思路放後面,是因為對99.9%的情況來說,最不值錢的就是點子,你能想到的,別人可能都已經做出來了。即使是Jobs這種用新意折服世界的人,也要靠”現實扭曲力場”的幫助把自己的觀點有效地推行下去。
所以,這個方案雖然很不錯,但是我不願意繼續討論了,因為這在現實中沒有意義,脫離現實的例子也就不再是好例子了。
我還想說一句,做專案和做人,都不能走極端。另一個極端是:以敏捷之名,無視一切編碼前的設計。我猜這在群人眼中,這個系列的文章沒有任何意義。
———————————————————牢騷的休止符—————————————————————
再一個方案
有人可能會說,讓燈自己控制自己的狀態,也可以解決問題。像下面這樣。
圖7 另一種解決方案
然後把Light類實現成這個樣子:(多酷啊,目前為止程式碼量最少的方案)
1 2 3 4 5 6 7 8 9 |
public class Light : ISwitchable { private bool isGlowing; public void JustSwitch() { isGlowing = !isGlowing; } } |
程式碼 4
這個設計的確可行,但是哪個方案更好呢?這個問題就留給各位讀者吧。就拿幾個Principle逐個分析下應該就可以分析出個所以然來。(下一節有簡單提示)
設計思想(原則)及技術方案的濫用
每種設計都有他的思路和道理。你覺得不可理喻的設計可能恰恰是別人深思熟慮的結果,只是每個人的思路不一樣,結果自然也不相同。但是如果設計思路被某種設計思想佔據了絕對主導的地位,就可能會出現設計上的偏差。
我把濫用大體上總結成如下三種:
1、單純的濫用。因為我會這麼做,所以就順便這麼做了。他們的理論基礎是:雖然現在沒有這要的需求,誰知道以後有沒有呢?我多做一點兒還不好?最典型的症狀是,所有的類,都有相應的介面,全部使用Dependency Injection來例項化。這不是有病麼?
2、這會引出一個比較大的話題,就是怎樣的設計算是過度設計?這需要單獨寫一節來討論這個問題。就不在這裡展開了。
3、程度上的濫用。圖7的設計,就體現了一個叫做”Tell, Don’t Ask“的原則,或者說是為了將這個原則”發揮到極致”而形成的設計。而在這個原則之下寫出的程式碼4又是如此簡潔和優美;以至於讓想出這個方案的人,很難主動拋棄這個方案。直到看到這個方案不能滿足的需求才肯承認問題。
4、用範圍濫用。把某個原則或是技術方案當萬金油。只要能用得上,就一定要用上一用。導致一葉蔽目,不願意尋求其它更加合適的技術方案(專案時間緊是個最常見的藉口,一知道某個方案可行,就馬上付諸行動)。一時興起,畫了個漫畫。
圖8. 因為熟悉或懶惰而捨近求遠
第一篇就說過,優秀的設計不是藉由幾個原則、模式就可以保證的。何況是某一個原則呢?物極必反。今天就不再囉嗦了。大家也都懂。設計過程中對某個特定的設計思想或是技術過於執著,往往會形成一個雖可行、卻畸形的設計。圖7即是一個例子。
一個真實的案例
有一家著名的諮詢公司,2009年接了一個銀行的大單,為期一年,預計可以賺到五個億。但是這個專案現在都還沒有結束,專案延期不僅要給銀行賠償,還要繼續免費給銀行把這個專案做完。(想想國內公司會怎麼做?)2010-2011年度,公司接的其它專案賺的錢幾乎全部貼給了這個專案。當年全公司員工沒有獎金,部分相關高層降職降薪。
為什麼?一個可以賺到五個億的專案卻虧了幾個億?
目前專案內員工的工作效率極低,一個普通開發者要兩天才能完成一個報表的修改。(現階段是修改,不是全新開發)。所以說人員成本非常大。專案拖上一個月,數百萬就打了水漂。
那麼效率為什麼這麼低?他們所有的業務邏輯都用PL-SQL實現,大的報表,涉及到的PL-SQL動輒上萬行,而且層層呼叫,加之整個系統有數千個表。程式碼的測試,都要先去資料庫造假資料。效率能高就怪了。
為什麼要這麼搞?因為一開始做系統設計的人,對PL-SQL比較熟悉,對Java不熟悉,所以就把Java當成了UI Wrapper來用。狗屎吧。當然還會有很多其它的因素,但是在技術層面,這絕對是重要因素之一。因為自己對某個技術比較熟悉,而不願意在專案中瞭解和應用更適合的其它技術方案的人套上CTO之類的外衣,恆等於攪屎棍。
支援開關控制多個燈
簡單而言,要讓一個開關去控制所有的燈。聽上去很簡單,而且有很多種實現方式。但是如果仔細想想,會發現有很多問題。
需求分析,不同於簡單的需求整理
使用者給出的需求總會是很概括甚至模糊的,不是使用者懶得說,而是使用者覺得自己已經說清楚了。以開燈為例,使用者需求就是:”要有一個統一開關。”,你如果再去追問使用者,要怎麼個統一法?使用者可能就會不高興了,因為他覺得這是你應該解決的問題。但是如果簡單地把”要有統一開關”這個使用者需求直接寫進需求文件的話,就等著專案失敗或是延期吧。
需求分析的第一步,就是要對使用者的需求進行分解、細節,找出合理用例。比如:使用者要求的是,要有統一開關,但是並沒有說每個燈就沒有自己的開關。如果每個燈又都有自己的開關,統一開關應該如何與各個專屬開關協作呢?從這個角度,就可以找到一些用例。
1、兩個燈,都開著。這時去按統一開關,應該是全關對吧。也就是說,統一開關應該知道當前燈的狀態。並按當前燈的狀態去執行操作。
2、三個燈,兩個開著。這時去按統一開關呢?統一開關的意思就是要有統一的行為,使用者肯定不會希望這個統一開關的行為是:把開著的燈關掉,把關著的燈開啟。怎麼辦?統計現在開著的比率?開著的多就全關?那如果是兩個燈,一個開著,一個關著呢?還有一個辦法是,讓統一開關使用程式碼2的方案:自己記住上次的操作結果。上次是全關,這次就全開;反之亦然。
也就是說,用例1和用例2都是合理的,但卻是衝突的。這種衝突甚至是一種邏輯上的衝突,已經不是技術侷限性的問題了。這種情況,在軟體開發中也是很常見的。這時,我們拿著自己的分析、自己的想法去詢問使用者的意見,使用者就會很樂意了。人們都喜歡做選擇題,而不是做問答題。不是麼?
現實中的電路
現實中的電路圖有兩種做法。(非標準電路,請意會。強弱電相關專業也許可以參考這裡)
圖9. 現實中的兩種簡單的總控加分支開關電路
前一張圖,分支開關有絕對的控制權;後一張圖中開關中有一個二極體,開關的開合用於控制二極體的極性,總開關的作用就是:把開著的關掉,把關著的開啟。
看上去就是兩種開關嘛。
基於派生類的方案
為了讓一個開關可以控制多個燈,對現有程式設計上的改動很小。類圖如下:
圖10. 多控開關設計
通過派生新的Switcher,來提供不同的功能。在當前的需求下,這個方案是可行的。未來的需求,就留給未來去解決吧。
小結
本節本來是想講講需求決定設計這個理念,從兩個需求引出兩個互不相容的設計方案。所以找了兩個需求一起講,但是最後這兩個需求並沒多大的衝突,也就沒有達到預期的目標。不過想說的倒是說出來了。就是專案的設計,最終還是要依賴於需求,任何要做出能夠適應未來需求的設計的想法,都是不切實際、勞民傷財的。(不知道有多少人會把這句話曲解為”設計無用論”,如果我認為設計無用,還會寫這個系列嗎?)
這個系列並不是要講設計模式,也不是用小例子做各種分析。只是想通過最簡單的例子講一些大道理(原則)。如果沒有合適的大道理可說,分析出一百個需求、做出一百個設計,也只是魚,而非我想說的漁。
距上節的釋出也已經很久了,因為每回發表時,都會先想好下回寫什麼、怎麼寫,並結尾留個引子。這次很可惜,想好了下回寫什麼,卻死活想不出合適的、簡單的需求來引出這個問題,也就不知道怎麼寫了。所以下回什麼能寫好我也不知道。大體上,也許還會有下而一些道理希望能和大家分享:
●過度設計及關於”過度”的度量。
●區域性設計最優化與全域性次優化之間的權衡。
●什麼是設計經驗及如何正確地借鑑。
新的使用者
下一回講哪個還沒有講好,但是使用者的需求卻如潮水般湧來。先權且記下:
●在開關上加一個小燈,表示現在的燈是開還是關。(因為開關和燈可能並不在一起)
●通過開關調節燈的亮度。
●在開關上顯示燈的耗電量、溫度、預期壽命等。
●定時開關。
●開關聲控、手勢控。
●開關許可權控制。