|
做不完的應(yīng)用軟件
我爸是個鄉(xiāng)村小學(xué)教師,對我所從事的軟件行業(yè)一無所知,但是他對我的工作穩(wěn)定性表示懷疑:“你這做軟件的,要是有一天軟件做完了,你豈不是要失業(yè)了?”也許他想起了他作為老師的情況,教完一批學(xué)生,下一批又上來了,一茬一茬的。于是又問我:“你們是不是一個軟件接著一個軟件做?”我回答他:“不是,就一個軟件,好幾十個人得做好幾年呢。”解釋了很多次仍舊沒有消除他的疑問:“你們做軟件怎么會一直做下去?怎么沒有個做完的時候呢?”。
如果他在通往張江的地鐵上,知道有那么多我傷不起的IT同類們,他也許會更加迷惑。為什么如此龐大的程序員大軍,日復(fù)一日年復(fù)一年地敲著代碼,生產(chǎn)出無數(shù)的軟件,可是他們不用擔(dān)心失業(yè)?為什么需要那么多看上去類似的軟件?為什么這些軟件永遠沒有做完的那天?
應(yīng)用軟件獨一無二的地方
答案其實很簡單,我們做的每一套軟件,都是為了解決某個領(lǐng)域的業(yè)務(wù)需求。而業(yè)務(wù)需求永遠沒有停止變化的一天,這就是為什么應(yīng)用軟件永遠也做不完的原因。我剛開始工作時,在一個小團隊里為一家公司定制開發(fā)ERP,其實就是一個中小型的管理系統(tǒng),在知道國內(nèi)有用友金蝶在做ERP的時候,覺得很奇怪:有那么好的公司在做ERP,我們還有做的必要嗎?客戶怎么不去買用友金蝶呢?現(xiàn)在想來這想法很幼稚,因為每家公司的業(yè)務(wù)都是獨一無二的,因此每個應(yīng)用軟件,即使都叫ERP,它也是獨一無二的。
把獨一無二的東西分離出去
想想我們?yōu)榱藰?gòu)建一個應(yīng)用軟件起來,要做哪些事?最基本的三件是:Domain Business Logic、Presentation(UI展現(xiàn))、Persistence(存數(shù)據(jù)庫)。還有就是Authentication,Authorization,Cache等等。復(fù)雜的系統(tǒng)還包括:與其它系統(tǒng)的集成,提供Service(API)等等。
就說最基本的3件吧,稍微思考一下就會發(fā)現(xiàn),只有Business Logic是獨一無二的。
Presentation層是個重復(fù)重復(fù)再重復(fù)的事情,再怎么不同的應(yīng)用,我們都可以用同一套工具來實現(xiàn)它:用Grid來展現(xiàn)多條記錄;用Combobox來提供選擇;用MVC模式來分離數(shù)據(jù)與展現(xiàn)……框架應(yīng)運而生。
Persistence也是個重復(fù)重復(fù)又重復(fù)的事情,關(guān)系型數(shù)據(jù)庫,ORM框架,NoSql……同樣約定俗成。
所有的東西都可以找到框架,這就是為什么寫應(yīng)用軟件,跟寫游戲,或者寫操作系統(tǒng)等比起來,是最沒有技術(shù)含量的。
然而唯獨“業(yè)務(wù)邏輯”沒有框架。正是這“業(yè)務(wù)邏輯”,讓每個應(yīng)用軟件區(qū)別于其它應(yīng)用軟件。因此我們決定要做一個應(yīng)用軟件,我們要做的就是實現(xiàn)客戶的業(yè)務(wù)邏輯,這是唯一真正的目的。持久化,UI,Cache……,都是手段。
如何實現(xiàn)業(yè)務(wù)邏輯?
現(xiàn)在知道了,我們做的每一個系統(tǒng),都在為客戶交付獨一無二的業(yè)務(wù)價值。這就解決了程序員們“存在的意義”的哲學(xué)問題。那么如何實現(xiàn)業(yè)務(wù)邏輯呢?把客戶的業(yè)務(wù)需求轉(zhuǎn)化為解決方案的過程,就是設(shè)計的過程。因此,這個問題可以這樣問:通過何種的設(shè)計,讓客戶的業(yè)務(wù)需求得到滿足呢?這個問題很傻,因為顯而易見是通過寫代碼實現(xiàn)啊,這不是常識嗎?很可惜這不是常識,因為很多團隊把設(shè)計寫在設(shè)計文檔里了。他們把寫文檔說成設(shè)計,把寫代碼說成實現(xiàn)。大錯特錯,代碼是唯一的設(shè)計,MsBuild把代碼build成exe才是實現(xiàn)!用Unit Test確保自己的設(shè)計(即代碼)正確反應(yīng)了自己腦中的設(shè)計意圖,用Integration Test來確保自己的設(shè)計(即代碼)正確地滿足客戶的需求,有著這種認識和追求的程序員,是我想要共事的程序員。
想起來,臺灣習(xí)慣把程序員說成“設(shè)計師”,是更貼切的。把代碼當(dāng)做設(shè)計的程序員是幸福的,比較一下大樓的設(shè)計人員,當(dāng)他們設(shè)計好的方案(設(shè)計圖紙)一旦被建筑工人們開始“實現(xiàn)”的時候,他們的設(shè)計幾乎就不能再改了,因為“實現(xiàn)”的成本太昂貴了。而作為軟件設(shè)計師,我們的“建筑工人”MsBuild包工頭以及它的團隊(CPU,RAM等小兵)是多么的廉價和高效,幾秒鐘就把我們的設(shè)計給實現(xiàn)了。這就是我們能夠利用“重構(gòu)”技術(shù)的理由。(想象一下如果大樓設(shè)計人員也這么說:“你們先按照這方案蓋起來,我看效果,然后再調(diào)整(重構(gòu))”……)
與傳統(tǒng)行業(yè)的設(shè)計師相比,我們軟件設(shè)計師能得到的反饋更快更多(因為我們面對的是電腦),這就是我們幸福的地方,也是我們應(yīng)該利用的地方。準(zhǔn)備寫個“軟件開發(fā)中的反饋系統(tǒng)”系列文章闡述此問題。
在哪里實現(xiàn)業(yè)務(wù)邏輯?
用代碼實現(xiàn)業(yè)務(wù)邏輯,那么,在代碼的哪個地方呢?有很多個地方:
1,前些年很常見如今被人很鄙視的一種是,存儲過程。這種曾經(jīng)非常流行的技術(shù),自然有它產(chǎn)生的原因。存儲過程是什么?是數(shù)據(jù)庫里的東西,而且是關(guān)系型數(shù)據(jù)庫里的東西。很多人的思維是這樣的:當(dāng)他試圖理解一個業(yè)務(wù)邏輯時,他心里想的是表以及表與表之間的關(guān)系,這就是Database-Driven邏輯,在這種邏輯下,把業(yè)務(wù)邏輯寫在存儲過程里,是很自然的事情。如果把思維切換到Domain-Driven的模式中:業(yè)務(wù)邏輯是我的核心,持久化只是一個輔助的手段,我可以用關(guān)系型數(shù)據(jù)庫,也可以用NoSql,而NoSql根本沒有存儲過程,如此,你把業(yè)務(wù)邏輯寫在存儲過程中讓人情何以堪啊?
2,寫在UI里,這就要提到當(dāng)年的RAD之王Delphi了。并不是說在Delphi里只能這么做,而是說Delphi里很多人就這么做,UI直接綁定DataSet,用戶點擊了某按鈕,直接在IDE里雙擊該按鈕,生成Btn1_Click方法,把業(yè)務(wù)邏輯通通寫在那。這么做有一萬種缺點,但有一個優(yōu)點,就是RAD中的R(Rapid)。哥用這種方式寫過好幾年的Delphi,不堪回首。
3,寫在MVC的Controller里,其實等同于2。現(xiàn)在ASP.NET MVC框架很熱,可是很多人不知道MVC本質(zhì)上是啥東西。雖然它有個“M”,可是他在分層的架構(gòu)體系里,只是非核心的Presentation層的一個pattern而已。跟Domain層毫無關(guān)系。
4,最好的方式,當(dāng)然是寫在一個獨立的Domain Layer里。別忘了業(yè)務(wù)邏輯是一個應(yīng)用系統(tǒng)唯一獨一無二的地方。
如何實現(xiàn)Domain Layer(Business Logic Layer)?
我做過幾次技術(shù)面試,一般都會有個問題:“你能說說你對架構(gòu)的理解嗎?”得到的回答,第一句往往是:“關(guān)于架構(gòu),一般是分成3層,Presentation,Business Logic,Persistence……”。這句話即使是很Junior的人也能說得上來,可是再往下問就能問出有意思的東西了:3層之間的依賴關(guān)系是怎樣的?
一般的回答是:Presentation依賴于Business Logic, Business Logic依賴于Persistence。
可是既然每個應(yīng)用系統(tǒng)的“業(yè)務(wù)邏輯”才是應(yīng)用系統(tǒng)存在的理由,才是開發(fā)它的目的所在。而UI展現(xiàn)、數(shù)據(jù)庫存儲、Cache等都是為了實現(xiàn)“業(yè)務(wù)邏輯”這個目的所提供的手段,都有成熟的框架、模式可用,都可以是雷同的。
那么為什么“業(yè)務(wù)邏輯”要依賴于“存儲技術(shù)”?為什么“目的”要依賴于“手段”?
--------------------------------------------
在此對上篇做下補充說明:
1,因本人畢業(yè)以來從事的項目全是業(yè)務(wù)邏輯復(fù)雜的企業(yè)應(yīng)用軟件,ERP,SCM,HRP,CRM……,這種系統(tǒng),如Martin Fowler在PEAA一書中所說,是適合使用Domain Model的,上文和本篇討論的都是基于這樣的場景和前提。
2,正如一哥們回復(fù)中說的,天下沒有絕對的東西,我們都在寫隨筆,不是寫論文。這兩篇文章只是提供一種看待問題的視角,看問題的視角多了,到了具體的項目,就會有更多的選擇。
3,寫上篇時沒想到要分上下篇,導(dǎo)致整個上篇沒有說明啥叫“裸奔”,不過從評論看,大部分人都讀懂了:就是讓“領(lǐng)域模型”不依賴于其它任何東西(如數(shù)據(jù)訪問層)。
天氣熱了,實在不想下了班還鼓搗技術(shù),不過想想還是一鼓作氣寫完拉倒。
邏輯依賴與物理依賴
上篇留下的問題是:為什么“業(yè)務(wù)邏輯”要依賴于“存儲技術(shù)”?為什么“目的”要依賴于“手段”?
其實“目的”依賴于“手段”并沒有什么問題,但更準(zhǔn)確的說法應(yīng)該是“目的”受約束于“手段”,具體說就是“業(yè)務(wù)邏輯層”受約束于“數(shù)據(jù)存儲層”,舉個例子,如果使用NHibernate作為ORM框架,設(shè)計的“領(lǐng)域模型”一定是把所有屬性都設(shè)置為virtual,為了遷就于NHibernate的LazyLoad實現(xiàn)技術(shù)。這種遷就或者依賴是無法消除的,然而這里說的是概念上或邏輯上的依賴。
如果到了具體實現(xiàn)上,仍然存在這種依賴,就成了物理上的依賴,簡單地說就是BLL這個assembly會對DAL這個assembly有個引用。物理依賴有什么問題?
反饋延遲帶來的傷害
先離題一下說說反饋。舉個例子,我們拿著杯子去飲水機接水,隨著水位的上升,我們知道何時應(yīng)該停止,這就是眼睛看到水位后,大腦給出的反饋。如果反饋延遲(哪怕只延遲2秒)甚至根本沒有反饋,會有什么后果?水溢出來了,大腦才反應(yīng)過來,后果一定是手被燙到。
簡單的例子可以說明反饋被延遲帶來的危害。然而在軟件開發(fā)中,很多團隊不斷地被延遲的反饋所反復(fù)蹂躪傷害。此話怎講呢?
舉個例子吧,“代碼即設(shè)計”,如果代碼就是我們的設(shè)計,那么如何保證我們的設(shè)計正確?很多團隊最常見的辦法是人肉測試。把代碼打包成軟件,然后丟給測試人員甚至客戶。在我經(jīng)歷過的一個瀑布式軟件過程里,今天寫好的代碼,也許要一個月后才會到測試人員手中,半年后到客戶手中,也就是說,外界對我們設(shè)計(代碼)的驗證和反饋周期,需要幾個月之久。這是多么大的延遲,2秒延遲就會燙傷我們的手,幾個月,我們傷的起嗎?
如何加速反饋
這就是“迭代開發(fā)”被引入的一個理由:縮小反饋周期。一個迭代(常見的是2周)內(nèi)必須把反饋圈給結(jié)束掉,也就是2周內(nèi)完成一個Feature的需求分析、設(shè)計、代碼、測試等所有環(huán)節(jié)。從這個角度出發(fā),如果一個迭代里不能getting things done,那不叫迭代,那就叫“兩周”。
對于一個Feature來說,兩周的反饋周期是可以接受的,畢竟每兩周有個功能點給客戶看看,確保我們do the right thing,很不錯了。
然而如何保證我們do things right(比如,設(shè)計和可維護性等等足夠好)呢?還有,這兩周做的正確的東西,如何保證隨著功能的不斷增加而不會在將來被破壞呢(答案:回歸測試)?如果每兩周都人肉回歸以前做過的所有功能,那就需要太多QA了。
答案就是自動化測試。Unit Test保證do things right;驗收測試/集成測試來保證do right things。
自動化測試金字塔
如圖,意思是什么呢?如果一個項目的所有自動化測試用例是100,那么最下面的Unit Test應(yīng)該占80個左右,中間的集成測試占15個左右,上面的UI驅(qū)動的驗收測試占5個左右。(還有個最上面的人肉測試,那是浮云:))為啥呢?因為Unit Test的ROI(投資回報率)最高,它上手容易、運行快,UI驅(qū)動的驗收測試的ROI最低,運行慢、維護成本高(因為UI是很易變的,UI一變,UI測試腳本就得改。)
所以一個團隊如果要開始自動化測試,最好從Unit Test開始。而最應(yīng)該寫Unit Test的地方是哪個地方呢?毫無疑問,是我們的“目的層”——“領(lǐng)域模型層”。
Persistence Ignorance
回到我們的問題,“領(lǐng)域模型層”對“數(shù)據(jù)存儲層”有物理上的依賴,導(dǎo)致的不好的結(jié)果就是,很難寫Unit Test。想象一下,有個Customer類,它的AddOrder()方法里面調(diào)用了DAL層的東西,也就是連接了數(shù)據(jù)庫,那我跑我的UT時也一定要連數(shù)據(jù)庫。連數(shù)據(jù)庫的UT那不叫UT。
怎么辦呢?“依賴反轉(zhuǎn)”,Inversion Of Control,IOC。具體做法是:本來BLL依賴于DAL,現(xiàn)在抽一個接口IDAL,讓BLL依賴于IDAL,DAL從IDAL繼承。從Assembly上來說,BLL和IDAL放到一個Assembly里,DAL放到另一個Assembly,那么DAL這個Assembly現(xiàn)在對BLL那個Assembly有個依賴了。——這樣,就把依賴給反轉(zhuǎn)了。然后通過Dependency Injection,在運行時把DAL作為IDAL的運行時實例,注入到BLL中。這就是IOC和DI的關(guān)系,他們其實不是一個東西,只不過很相關(guān),有時就用IOC或DI泛指這項技術(shù)了。
BLL對DAL的依賴,從編譯期延遲到了運行期,編譯期對DAL沒有依賴,只對IDAL有依賴,這就是Persistence Ignorance(不知的請google之)。
這有多重要?
BLL(領(lǐng)域模型)開始裸奔了,它對其它層沒有依賴,我們可以為他寫豐富的Unit Test,這有多重要?
每個unit test都用其方法名說明了我們的設(shè)計意圖,甚至小片業(yè)務(wù)邏輯,比如有個測試用例,方法名叫“should_promote_to_VIP_when_customer_buying_platinum_card”,如果讓你接手一個別人留下的代碼,你不是很清楚里面的業(yè)務(wù)邏輯,你是愿意去看文檔?還是愿意去看他留下的存儲過程、或者100行又臭又長的方法?還是愿意看這樣的一句話:“當(dāng)客戶買了白金卡后,應(yīng)該把它提升為VIP”?
unit test的覆蓋率足夠高時,我們讀完所有的unit test方法名(只是名字),我們已經(jīng)了解了大部分的業(yè)務(wù)邏輯。
事實上,一個項目的維護成本往往是開發(fā)成本的四五倍甚至幾十倍(越差的代碼,這個比例越高)。另外大家也深有體會:讀代碼比寫代碼難。那么為了降低讀代碼或者維護別人/自己代碼的痛苦(當(dāng)老板的,降低維護成本意味著白花花的銀子啊),有啥理由不讓我們的“領(lǐng)域模型”裸奔呢?
豐滿的領(lǐng)域模型裸奔著向我們呼嘯而來
下圖是敏捷宣言簽署者之一的Alistair Cockburn的Hexagonal Architecture,很精彩的圖,留作參考資料,大家意會,不解釋了。
it知識庫:為什么要讓我們的“領(lǐng)域模型”裸奔?,轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請第一時間聯(lián)系我們修改或刪除,多謝。