<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Johnny's Collections

    生活總是有太多的無奈與失望,讓我們以在努力學習和工作中獲得的成就感和快樂來沖淡它們。

    BlogJava 首頁 新隨筆 聯系 聚合 管理
      10 Posts :: 0 Stories :: 80 Comments :: 0 Trackbacks

    2010年4月27日 #

        一直對BIO、NIO、AIO不太理解,特別是阻塞與異步的區別。Google了一下,一篇文章中的4張圖很形象的表述了4種IO模型的原理和區別,收藏一下。

        首先,貼一張表示四種IO模型的圖:



    同步阻塞IO:


    同步非阻塞IO:



    異步阻塞IO:


    異步非阻塞IO:
    posted @ 2012-05-20 00:12 Johnny.Liang 閱讀(929) | 評論 (0)編輯 收藏

    6月下旬,由于個人發展的原因,離開了做銀行外包的F公司,也離開了S城,結束了2個半月短暫的S城之旅,重新回到了土生土長的G城,也加入了一家互聯網公司,工作這么多年,都是做企業級應用,這次是我第一次從事互聯網技術工作,我終于落網了。

    從事互聯網技術工作與企業級應用有很大的不同,考慮的東西更多了。由于缺乏互聯網相關的開發經驗,很多東西需要去學習,同時,加入新公司后,終于真正的當上了Leader,這次終于可以自己一手一腳的組件自己的團隊了,所以,除了技術工作,還有一大堆管理工作要做。幾個月來,晚上9點半過后下班,周末帶電腦回家繼續工作已經成為了習慣。總之,每天都有忙不完的工作,生活的70%時間都在工作上。

    正因如此,本來想堅持寫的博客暫停了,《領域驅動設計》的系列文章也暫停了很久。在此,向一直關注我的文章的同學說聲抱歉,我可能有一段時間要暫停寫博了。不過,技術依然是我的最愛,IT依然是我的事業,等我在新公司的工作上軌之后,我就會回來。各位,再會!

    posted @ 2010-12-14 22:46 Johnny.Liang 閱讀(1146) | 評論 (3)編輯 收藏

    領域驅動設計系列文章(3)——有選擇性的使用領域驅動設計

     

    本系列的第一篇博文拋磚引玉,大談領域驅動設計的優勢,這里筆者還是希望以客觀的態度,談談領域驅動設計的缺點及其不適合使用的場景,以讓讀者可以有選擇性的使用領域驅動設計。

     

           我們知道,沒有最好,只有最合適,設計也是一樣。因此,所謂設計,就是以你和你的團隊的知識、經驗和智慧,全面充分的考慮各種內外因素后,在你們的設計方案中作出合理的選擇的過程。而這些影響你們選擇的因素主要有:

     

    • 技術框架的特征和約束(如果你的項目決定使用C語言進行開發,那么首先在設計方法上,就需要使用面向過程而非面向對象的設計方法)。

     

    • 時間的壓力和約束(你永遠不可能告訴你的老板,給我10年時間,我和我的團隊將為你設計出世界上最優秀的軟件)。

     

    • 你的團隊的能力、經驗、性格、價值觀等因素的約束(你不能期望一個長期從事遺留系統維護項目或大部分成員是缺乏經驗的高校畢業生的團隊能很好的按照你的設計意圖去實現你的高度抽象的優秀設計,同時你也別指望一幫家里經濟條件不錯,本著過來熬時間的家伙會樂意與你一起刻苦鉆研、精益求精)。

     

    • 你的系統的特征(如果你想把一個足夠簡單而且在可以預計的將來都不存在很大規模的需求變更的系統設計得很復雜,很精妙,具有很好的擴展性,但要為此付出巨大的時間、人力成本,這顯然是一種不理智的過度設計(Over design))。

     

    • 其他外在因素的約束(你的項目需要參與投標,你必須壓縮人力、時間等以讓你的項目成本成為巨大的競爭資本)。

     

    當然,上述的考慮因素站在比較高的角度,通常是項目經理、架構師需要考慮的問題,但這當中你應該會得到一些啟發?;氐轿覀兊闹黝},我們首先看看,領域驅動設計相對于傳統的面向過程式的設計,有什么缺點:

     

    • 復雜化:面向過程思維之所以一直很受歡迎,是因為它很直觀,非常符合大部分人的思維習慣,大部分人拿到一個問題,通常都是會很直觀的想第一步做什么、第二步做什么,如果怎樣,應該怎樣,否則怎樣……,可以說,任何水平的程序員,都能很好的使用面向過程的方法進行設計和開發。同時,由于我們教育水平的落后和整體IT環境的制約,可以這樣說,真正掌握面向對象思維和設計方法的程序員的比例非常低(雖然絕大部分都使用面向對象的語言和工具),而本身面向對象思維要求人有很好的抽象思維能力,因為你需要把一個復雜的系統一層層的抽象為簡單的部分,需要把現實世界的事物(有些是可見的,但有些是不可見)合理的抽象為計算機世界的不同元素。這些都不是一些很容易做的事情,要做得好,就更難。


    • 團隊的抗拒:如果你的團隊(很大可能)大部分人都習慣于用很直觀的面向過程的方式進行設計和開發,你需要推動你的團隊轉換思維來采用面向對象的方式進行領域驅動設計,通常會遭到多數人的抗拒。因為人都是有惰性的,他們習慣安于現狀,而改變是痛苦的,他們要付出額外的努力,需要進行學習,但以筆者的經驗,相當一部分程序員,特別是有一定工作年限的程序員,他們從事IT工作都只是為了獲得一份不錯的報酬,因此他們的學習動力非常有限,而且,他們都相當自負,被說服的難度比較大。



    • 管理、開發和維護的成本高:復雜度更高,意味著你需要花更多的時間進行設計,同時需要花出額外的時間進行必要的培訓,而且需要有更完善的文檔(設計文檔,API文檔,培訓文檔等)。領域驅動設計的抽象程度比較高,因此必需有良好的文檔,否則,隨著項目的不斷迭代、升級和維護,它很容易因為后來者的誤解而慢慢回歸面向過程的設計,甚至會變得“四不象”,領域驅動設計的成本優勢是隨著時間的推移慢慢體現的(見下圖),如果出現這種情況,所有前面付出的努力都會付諸東流。


    系統的初始階段,領域驅動設計需要付出更大的成本,但隨著時間的推移,領域驅動設計的成本效益優勢會逐步顯現

    • 性能比較低:使用純面向對象的方式進行領域驅動設計的程序,其系統開銷通常要比面向過程設計的程序高,從而性能相對較低(關于具體的例子,后續的博文會提及)。

     

    那么,假設我們在時間、團隊能力及各種資源都允許的情況下,是否就可以麻木的全盤使用領域驅動設計呢?正如本博文的標題一樣,答案是否定的,我們需要有選擇性的使用。讓我們來看看一些指導性原則:

     

    • 使用領域驅動設計,并不代表整個系統的方方面面都必須遵從領域驅動設計的原則,需要根據實際情況,讓適合的部分使用領域驅動設計,讓不適合的部分使用面向過程的設計。


    • 對于那些業務規則非常簡單,通常只有增、刪、改、查的簡單操作,而且也不大可能發生大規模需求變更的模塊,可以讓業務實體成為一個“貧血模型”,例如一些基礎數據:系統參數、商品類型、國家、地址信息(注:對于這點,本人持保留態度,因為這些業務雖然非常簡單,但既然選擇了領取驅動設計,即使把這些業務實體設計為“充血模型”,即把極其簡單的業務邏輯也封裝在業務實體中,也并不比使用“貧血模型”成本高,而它卻帶來了統一設計風格的好處)。


    • 對于查詢操作,特別是復雜的查詢操作,出于性能的考慮,可以用結構化查詢邏輯一次性完成,并把這些邏輯封裝在Repository(即技術上的DAO)中(這方面的具體例子,后面關于“查詢通道”和“領域對象倉庫”的博文會更具體的闡述)。我們可以看到,對于一些業務視圖,以及報表模塊,很明顯不適合使用面向對象的方式設計,因為這些“視圖”和“報表”,本質上就不是業務實體。


    • 同樣出于性能的考慮,在業務實體的實現邏輯中,某些操作不適合過度偏執的使用面向對象方式。例如,在“訂單”的“新增訂單明細”(order.addOrderItem(orderItem))中,如果業務邏輯規定一張訂單中包含優惠商品的明細數目不能超過20條,使用面向對象的方式,就需要把訂單中的所有訂單明細全部加載,然后逐個明細判斷其對應的商品是否優惠商品,再統計出優惠商品的數目,這樣很明顯是低效率和高開銷的,這里只需要使用Repository提供的一個統計方法,用一個結構化查詢邏輯返回統計結果即可,而這就是非面向對象的方式。


    本博文給有志于領域驅動設計的讀者潑了一下冷水,提出一些“反模式”(Bitter),是為了讓讀者冷靜一下,在領域驅動設計過程中作出更靈活和更合理的選擇。關于這方面的論述,筆者在這里淺嘗則止,限于水平、經驗和表達能力,不敢胡亂賣弄,建議讀者可以參考閱讀Martin Fowler的《Patterns of Enterprise Application Architecture》一書的相關觀點。

    posted @ 2010-06-26 17:47 Johnny.Liang 閱讀(4102) | 評論 (2)編輯 收藏

    上一篇文章作為一個引子,說明了領域驅動設計的優勢,從本篇文章開始,筆者將會結合自己的實際經驗,談及領域驅動設計的應用。本篇文章主要討論一下我們經常會用到的一些對象:VO、DTODOPO。

    由于不同的項目和開發人員有不同的命名習慣,這里我首先對上述的概念進行一個簡單描述,名字只是個標識,我們重點關注其概念:

     

    概念:

    VOView Object):視圖對象,用于展示層,它的作用是把某個指定頁面(或組件)的所有數據封裝起來。

    DTOData Transfer Object):數據傳輸對象,這個概念來源于J2EE的設計模式,原來的目的是為了EJB的分布式應用提供粗粒度的數據實體,以減少分布式調用的次數,從而提高分布式調用的性能和降低網絡負載,但在這里,我泛指用于展示層與服務層之間的數據傳輸對象。

    DODomain Object):領域對象,就是從現實世界中抽象出來的有形或無形的業務實體。

    POPersistent Object):持久化對象,它跟持久層(通常是關系型數據庫)的數據結構形成一一對應的映射關系,如果持久層是關系型數據庫,那么,數據表中的每個字段(或若干個)就對應PO的一個(或若干個)屬性。

     

    模型:

           下面以一個時序圖建立簡單模型來描述上述對象在三層架構應用中的位置


     

          

    l         用戶發出請求(可能是填寫表單),表單的數據在展示層被匹配為VO

    l         展示層把VO轉換為服務層對應方法所要求的DTO,傳送給服務層。

    l         服務層首先根據DTO的數據構造(或重建)一個DO,調用DO的業務方法完成具體業務。

    l         服務層把DO轉換為持久層對應的PO(可以使用ORM工具,也可以不用),調用持久層的持久化方法,把PO傳遞給它,完成持久化操作。

    l         對于一個逆向操作,如讀取數據,也是用類似的方式轉換和傳遞,略。

     

    VODTO的區別

           大家可能會有個疑問(在筆者參與的項目中,很多程序員也有相同的疑惑):既然DTO是展示層與服務層之間傳遞數據的對象,為什么還需要一個VO呢?對!對于絕大部分的應用場景來說,DTOVO的屬性值基本是一致的,而且他們通常都是POJO,因此沒必要多此一舉,但不要忘記這是實現層面的思維,對于設計層面來說,概念上還是應該存在VODTO,因為兩者有著本質的區別,DTO代表服務層需要接收的數據和返回的數據,而VO代表展示層需要顯示的數據。

           用一個例子來說明可能會比較容易理解:例如服務層有一個getUser的方法返回一個系統用戶,其中有一個屬性是gender(性別),對于服務層來說,它只從語義上定義:1-男性,2-女性,0-未指定,而對于展示層來說,它可能需要用“帥哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。說到這里,可能你還會反駁,在服務層直接就返回“帥哥美女”不就行了嗎?對于大部分應用來說,這不是問題,但設想一下,如果需求允許客戶可以定制風格,而不同風格對于“性別”的表現方式不一樣,又或者這個服務同時供多個客戶端使用(不同門戶),而不同的客戶端對于表現層的要求有所不同,那么,問題就來了。再者,回到設計層面上分析,從職責單一原則來看,服務層只負責業務,與具體的表現形式無關,因此,它返回的DTO,不應該出現與表現形式的耦合。

           理論歸理論,這到底還是分析設計層面的思維,是否在實現層面必須這樣做呢?一刀切的做法往往會得不償失,下面我馬上會分析應用中如何做出正確的選擇。

     

    VODTO的應用

           上面只是用了一個簡單的例子來說明VODTO在概念上的區別,本節將會告訴你如何在應用中做出正確的選擇。

           在以下才場景中,我們可以考慮把VODTO二合為一(注意:是實現層面):

    l         當需求非常清晰穩定,而且客戶端很明確只有一個的時候,沒有必要把VODTO區分開來,這時候VO可以退隱,用一個DTO即可,為什么是VO退隱而不是DTO?回到設計層面,服務層的職責依然不應該與展示層耦合,所以,對于前面的例子,你很容易理解,DTO對于“性別”來說,依然不能用“帥哥美女”,這個轉換應該依賴于頁面的腳本(如JavaScript)或其他機制(JSTL、EL、CSS

    l         即使客戶端可以進行定制,或者存在多個不同的客戶端,如果客戶端能夠用某種技術(腳本或其他機制)實現轉換,同樣可以讓VO退隱

     

    以下場景需要優先考慮VO、DTO并存:

    l         上述場景的反面場景

    l         因為某種技術原因,比如某個框架(如Flex)提供自動把POJO轉換為UI中某些Field時,可以考慮在實現層面定義出VO,這個權衡完全取決于使用框架的自動轉換能力帶來的開發和維護效率提升與設計多一個VO所多做的事情帶來的開發和維護效率的下降之間的比對。

    l         如果頁面出現一個“大視圖”,而組成這個大視圖的所有數據需要調用多個服務,返回多個DTO來組裝(當然,這同樣可以通過服務層提供一次性返回一個大視圖的DTO來取代,但在服務層提供一個這樣的方法是否合適,需要在設計層面進行權衡)。

     

    DTODO的區別

           首先是概念上的區別,DTO是展示層和服務層之間的數據傳輸對象(可以認為是兩者之間的協議),而DO是對現實世界各種業務角色的抽象,這就引出了兩者在數據上的區別,例如UserInfoUser(對于DTODO的命名規則,請參見筆者前面的一篇博文),對于一個getUser方法來說,本質上它永遠不應該返回用戶的密碼,因此UserInfo至少比User少一個password的數據。而在領域驅動設計中,正如第一篇系列文章所說,DO不是簡單的POJO,它具有領域業務邏輯。

     

    DTODO的應用

           從上一節的例子中,細心的讀者可能會發現問題:既然getUser方法返回的UserInfo不應該包含password,那么就不應該存在password這個屬性定義,但如果同時有一個createUser的方法,傳入的UserInfo需要包含用戶的password,怎么辦?在設計層面,展示層向服務層傳遞的DTO與服務層返回給展示層的DTO在概念上是不同的,但在實現層面,我們通常很少會這樣做(定義兩個UserInfo,甚至更多),因為這樣做并不見得很明智,我們完全可以設計一個完全兼容的DTO,在服務層接收數據的時候,不該由展示層設置的屬性(如訂單的總價應該由其單價、數量、折扣等決定),無論展示層是否設置,服務層都一概忽略,而在服務層返回數據時,不該返回的數據(如用戶密碼),就不設置對應的屬性。

           對于DO來說,還有一點需要說明:為什么不在服務層中直接返回DO呢?這樣可以省去DTO的編碼和轉換工作,原因如下:

    l         兩者在本質上的區別可能導致彼此并不一一對應,一個DTO可能對應多個DO,反之亦然,甚至兩者存在多對多的關系。

    l         DO具有一些不應該讓展示層知道的數據

    l         DO具有業務方法,如果直接把DO傳遞給展示層,展示層的代碼就可以繞過服務層直接調用它不應該訪問的操作,對于基于AOP攔截服務層來進行訪問控制的機制來說,這問題尤為突出,而在展示層調用DO的業務方法也會因為事務的問題,讓事務難以控制。

    l         對于某些ORM框架(如Hibernate)來說,通常會使用“延遲加載”技術,如果直接把DO暴露給展示層,對于大部分情況,展示層不在事務范圍之內(Open session in view在大部分情況下不是一種值得推崇的設計),如果其嘗試在Session關閉的情況下獲取一個未加載的關聯對象,會出現運行時異常(對于Hibernate來說,就是LazyInitiliaztionException)。

    l         從設計層面來說,展示層依賴于服務層,服務層依賴于領域層,如果把DO暴露出去,就會導致展示層直接依賴于領域層,這雖然依然是單向依賴,但這種跨層依賴會導致不必要的耦合。

     

    對于DTO來說,也有一點必須進行說明,就是DTO應該是一個“扁平的二維對象”,舉個例子來說明:如果User會關聯若干個其他實體(例如Address、Account、Region等),那么getUser()返回的UserInfo,是否就需要把其關聯的對象的DTO都一并返回呢?如果這樣的話,必然導致數據傳輸量的大增,對于分布式應用來說,由于涉及數據在網絡上的傳輸、序列化和反序列化,這種設計更不可接受。如果getUser除了要返回User的基本信息外,還需要返回一個AccountIdAccountName、RegionId、RegionName,那么,請把這些屬性定義到UserInfo中,把一個“立體”的對象樹“壓扁”成一個“扁平的二維對象”,筆者目前參與的項目是一個分布式系統,該系統不管三七二十一,把一個對象的所有關聯對象都轉換為相同結構的DTO對象樹并返回,導致性能非常的慢。

     

     

    DOPO的區別

           DOPO在絕大部分情況下是一一對應的,PO是只含有get/set方法的POJO,但某些場景還是能反映出兩者在概念上存在本質的區別:

    l         DO在某些場景下不需要進行顯式的持久化,例如利用策略模式設計的商品折扣策略,會衍生出折扣策略的接口和不同折扣策略實現類,這些折扣策略實現類可以算是DO,但它們只駐留在靜態內存,不需要持久化到持久層,因此,這類DO是不存在對應的PO的。

    l         同樣的道理,某些場景下,PO也沒有對應的DO,例如老師Teacher和學生Student存在多對多的關系,在關系數據庫中,這種關系需要表現為一個中間表,也就對應有一個TeacherAndStudentPOPO,但這個PO在業務領域沒有任何現實的意義,它完全不能與任何DO對應上。這里要特別聲明,并不是所有多對多關系都沒有業務含義,這跟具體業務場景有關,例如:兩個PO之間的關系會影響具體業務,并且這種關系存在多種類型,那么這種多對多關系也應該表現為一個DO,又如:“角色”與“資源”之間存在多對多關系,而這種關系很明顯會表現為一個DO——“權限”。

    l         某些情況下,為了某種持久化策略或者性能的考慮,一個PO可能對應多個DO,反之亦然。例如客戶Customer有其聯系信息Contacts,這里是兩個一對一關系的DO,但可能出于性能的考慮(極端情況,權作舉例),為了減少數據庫的連接查詢操作,把CustomerContacts兩個DO數據合并到一張數據表中。反過來,如果一本圖書Book,有一個屬性是封面cover,但該屬性是一副圖片的二進制數據,而某些查詢操作不希望把cover一并加載,從而減輕磁盤IO開銷,同時假設ORM框架不支持屬性級別的延遲加載,那么就需要考慮把cover獨立到一張數據表中去,這樣就形成一個DO對應對個PO的情況。

    l         PO的某些屬性值對于DO沒有任何意義,這些屬性值可能是為了解決某些持久化策略而存在的數據,例如為了實現“樂觀鎖”,PO存在一個version的屬性,這個version對于DO來說是沒有任何業務意義的,它不應該在DO中存在。同理,DO中也可能存在不需要持久化的屬性。

     

    DOPO的應用

           由于ORM框架的功能非常強大而大行其道,而且JavaEE也推出了JPA規范,現在的業務應用開發,基本上不需要區分DOPO,PO完全可以通過JPAHibernate Annotations/hbm隱藏在DO之中。雖然如此,但有些問題我們還必須注意:

    l         對于DO中不需要持久化的屬性,需要通過ORM顯式的聲明,如:在JPA中,可以利用@Transient聲明。

    l         對于PO中為了某種持久化策略而存在的屬性,例如version,由于DO、PO合并了,必須在DO中聲明,但由于這個屬性對DO是沒有任何業務意義的,需要讓該屬性對外隱藏起來,最常見的做法是把該屬性的get/set方法私有化,甚至不提供get/set方法,但對于Hibernate來說,這需要特別注意,由于Hibernate從數據庫讀取數據轉換為DO時,是利用反射機制先調用DO的空參數構造函數構造DO實例,然后再利用JavaBean的規范反射出set方法來為每個屬性設值,如果不顯式聲明set方法,或把set方法設置為private,都會導致Hibernate無法初始化DO,從而出現運行時異常,可行的做法是把屬性的set方法設置為protected

    l         對于一個DO對應多個PO,或者一個PO對應多個DO的場景,以及屬性級別的延遲加載,Hibernate都提供了很好的支持,請參考Hibnate的相關資料。

     

     

        到目前為止,相信大家都已經比較清晰的了解VODTO、DO、PO的概念、區別和實際應用了。通過上面的詳細分析,我們還可以總結出一個原則:分析設計層面和實現層面完全是兩個獨立的層面,即使實現層面通過某種技術手段可以把兩個完全獨立的概念合二為一,在分析設計層面,我們仍然(至少在頭腦中)需要把概念上獨立的東西清晰的區分開來,這個原則對于做好分析設計非常重要(工具越先進,往往會讓我們越麻木)。第一篇系列博文拋磚引玉,大唱領域驅動設計的優勢,但其實領域驅動設計在現實環境中還是有種種的限制,需要選擇性的使用,正如我在《田七的智慧》博文中提到,我們不能永遠的理想化的去選擇所謂“最好的設計”,在必要的情況下,我們還是要敢于放棄,因為最合適的設計才是最好的設計。本來,系列中的第二篇博文應該是討論領取驅動設計的限制和如何選擇性的使用,但請原諒我的疏忽,下一篇系列博文會把這個主題補上,敬請關注。
    posted @ 2010-05-27 00:07 Johnny.Liang 閱讀(38247) | 評論 (16)編輯 收藏

        我一直很喜歡攀藤的植物,之前在家里的陽臺種了不少田七,偶然發現一個奇怪的問題,一些新長出來的嫩枝,過了幾天突然凋謝了,一直不知道為什么,直到最近我找到了答案。

       來到**城后,新”家”有個很大的窗,外面是防盜網,于是從家里帶來了兩顆田七苗,由于春天和向南的緣故,兩顆小苗長得很快,每天都見它們向上攀爬了一大 截。兩天前,發現它們爬到防盜網頂了,防盜網頂部是封閉的,它們沒有了向上發展的空間,長出一截吊在半空。今晚澆水的時候,我突然發現吊在半空的嫩枝都凋 謝了,我終于明白了原因。田七非常聰明,當發現“前面是絕路”的時候,它會很明智的選擇放棄。種過田七的人會知道,田七雖然是攀藤植物,但它是可以長出分 支的,而有些種植常識的人會知道,把生長過長的枝莖進行修剪,會讓養分更充足,從而促進主莖中長出分支,形成更好的植株形狀。我相信田七“放棄”的原因也 正在于此,相信很快它們就會長出分支來。

        這個小小的發現其實蘊藏著一種顯淺的生活智慧,這種智慧對于很多生物都是與生俱來的,但對于人來說,反而很多人都不懂,又或者做不到。

        借用我的前總監經常告誡我的一句話告誡大家:“設計其實是一種選擇,沒有最好的設計,只有最合適的設計。”
    posted @ 2010-05-26 00:32 Johnny.Liang 閱讀(2944) | 評論 (5)編輯 收藏

    領域驅動設計系列文章(1)——通過現實例子顯示領域驅動設計的威力

     

           曾經參與過系統維護或是在現有系統中進行迭代開發的軟件工程師們,你們是否有過這樣的痛苦經歷:當需要修改一個Bug的時候,面對一個類中成百上千行的代碼,沒有注釋,千奇百怪的方法和變量名字,層層嵌套的方法調用,混亂不堪的結構,不要說準確找到Bug所在的位置,就是要清晰知道一段代碼究竟是做了什么也非常困難,最終,改對了一個Bug,卻多冒出N個新Bug;同樣的情況,當你拿到一份新的需求,需要在現有系統中添加功能的時候,面對一行行完全過程式的代碼,需要使用一個功能時,不知道是應該自己編寫,還是應該尋找是否已經存在的方法,編寫一個非常簡單的新、刪、改功能,卻要費盡九牛二虎之力,最終發現,系統存在著太多的重復邏輯,閱讀、測試、修改非常困難。在經歷了這些痛苦之后,你們是否會不約而同的發出一個感慨:與其進行系統維護和迭代開發,還不如重新設計開發一個新的系統來得痛快?

           面對這一系列讓軟件嵌入無底泥潭的問題,基于面向對象思想的領域驅動設計方法是一個很好的解決方法。從事過系統設計的富有經驗的設計師們,對職責單一原則、信息專家、充血/貧血模型、模型驅動設計這些名詞或概念應該不會感到陌生。面向對象的設計大師Martin Fowler不止一次的在他的Blog和著作《企業應用架構模式》中倡導過上述概論在設計中的巨大威力,而另外一位領域模型的出色專家Eric Evans的著作《領域驅動設計》也為我們提供了不少寶貴的經驗和方法。

           筆者從事系統設計多年,將會在本系列文章中把本人對領域驅動設計的理解,結合工作過程中積累的實際項目經驗進行淺析,希望與大家交流學習。

           在本系列博文的開篇中,我將會拿出一個顯示的例子,先用傳統的面向過程方式,使用貧血模型進行設計,然后再逐步加入需求變更,讓讀者發現,隨著系統的不斷變更,基于貧血模型的設計將會讓系統慢慢陷入泥潭,越來越難于維護,然后再用基于面向對象的領域驅動設計重新上述過程,通過對比展示領域驅動設計對于復雜的業務系統的威力。


           假設現在有一個銀行支付系統項目,其中的一個重要的業務用例是賬戶轉賬業務。系統使用迭代的方式進行開發,在1.0版本中,該用例的功能需求非常簡單,事件流描述如下:

    主事件流:

    1)  用戶登錄銀行的在線支付系統

    2)  選擇用戶在該銀行注冊的網上銀行賬戶

    3)  選擇需要轉賬的目標賬戶,輸入轉賬金額,申請轉賬

    4)  銀行系統檢查轉出賬戶的金額是否足夠

    5)  從轉出賬戶中扣除轉出金額(debit),更新轉出賬戶的余額

    6)  把轉出金額加入到轉入賬戶中(credit),更新轉入賬戶的余額

    備選事件流:

    4a)如果轉出賬戶中的余額不足,轉賬失敗,返回錯誤信息

     

    面向過程的設計方式(貧血模型)

     

    設計方案如下(忽略展示層部分):

    1)  設計一個賬戶交易服務接口AccountingService,設計一個服務方法transfer(),并提供一個具體實現類AccountingServiceImpl,所有賬戶交易業務的業務邏輯都置于該服務類中。

    2)  提供一個AccountInfo和一個Account,前者是一個用于與展示層交換賬戶數據的賬戶數據傳輸對象,后者是一個賬戶實體(相當于一個EntityBean),這兩個對象都是普通的JavaBean,具有相關屬性和簡單的get/set方法。

     

    下面是AccountingServiceImpl.transfer()方法的實現邏輯(偽代碼):

     


    public class AccountingServiceImpl implements AccountingService {

           
    public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountingServiceException {

                  Account srcAccount 
    = accountRepository.getAccount(srcAccountId);

                  Account destAccount 
    = accountRepository.getAccount(destAccountId);

                  
    if(srcAccount.getBalance().compareTo(amount)<0)

                         
    throw new AccountingServiceException(AccountingService.BALANCE_IS_NOT_ENOUGH);

                  srcAccount.setBalance(srcAccount.getBalance().sbustract(amount));

                  destAccount.setBalance(destAccount.getBalance().add(amount));

           }

    }

     

    public class Account implements DomainObject {

           
    private Long id;

           
    private Bigdecimal balance;

          

    /**

     * getter/setter

     
    */

    }

     

           可以看到,由于1.0版本的功能需求非常簡單,按面向過程的設計方式,把所有業務代碼置于AccountingServiceImpl中完全沒有問題。

           這時候,新需求來了,在1.0.1版本中,需要為賬戶轉賬業務增加如下功能,在轉賬時,首先需要判斷賬戶是否可用,然后,賬戶的余額還要分成兩部分:凍結部分和活躍部分,處于凍結部分的金額不能用于任何交易業務,我們來看看變更后的代碼:

     


    public class AccountingServiceImpl implements AccountingService {

           
    public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountingServiceException {

                  Account srcAccount 
    = accountRepository.getAccount(srcAccountId);

                  Account destAccount 
    = accountRepository.getAccount(destAccountId);

                  
    if(!srcAccount.isActive() || !destAccount.isActive())

                         
    throw new AccountingServiceException(AccountingService.ACCOUNT_IS_NOT_AVAILABLE);

                  BigDecimal availableAmount 
    = srcAccount.getBalance().substract(srcAccount.getFrozenAmount());

                  
    if(availableAmount.compareTo(amount)<0)

                         
    throw new AccountingServiceException(AccountingService.BALANCE_IS_NOT_ENOUGH);

                  srcAccount.setBalance(srcAccount.getBalance().sbustract(amount));

                  destAccount.setBalance(destAccount.getBalance().add(amount));

           }

    }

     

    public class Account implements DomainObject {

           
    private Long id;

           
    private BigDecimal balance;

           
    private BigDecimal frozenAmount;

          

    /**

     * getter/setter

     
    */

    }

     

           可以看到,情況變得稍微復雜了,這時候,1.0.2的需求又來了,需要在每次交易成功后,創建一個交易明細賬,于是,我們又必須在transfer()方面里面增加創建并持久化交易明細賬的業務邏輯:

                 

         AccountTransactionDetails details= new AccountTransactionDetails(…);
         accountRepository.save(details);

          

           業務需求不斷復雜化:賬戶每筆轉賬的最大額度需要由其信用指數確定、需要根據銀行的手續費策略計算并扣除一定的手續費用……,隨著業務的復雜化,transfer()方法的邏輯變得越來越復雜,逐漸形成了上文所述的成百上千行代碼。有經驗的程序員可能會做出類此“方法抽取”的重構,把轉賬業務按邏輯劃分成若干塊:判斷余額是否足夠、判斷賬戶的信用指數以確定每筆最大轉賬金額、根據銀行的手續費策略計算手續費、記錄交易明細賬……,從而使代碼更加結構化。這是一個好的開始,但還是顯然不足。

           假設某一天,系統需求增加一個新的模塊,為系統增加一個網上商城,讓銀行用戶可以進行在線購物,而在線購物也存在著很多與賬戶貸記借記業務相同或相似的業務邏輯:判斷余額是否足夠、對賬戶進行借貸操作(credit/debit)以改變余額、收取手續費用、產生交易明細賬……

           面對這種情況,有兩種解決辦法:

    1)  AccountingServiceImpl中的相同邏輯拷貝到OnlineShoppingServiceImplementation

    2)  OnlineShoppingServiceImpl調用AccountingServiceImpl的相同服務

    顯然,第二種方法比第一種方法更好,結構更清晰,維護更容易。但問題在于,這樣就會形成網上商城服務模塊與賬戶收支服務模塊的不必要的依賴關系,系統的耦合度高了,如果系統為了更靈活的伸縮性,讓每個大業務模塊獨立進行部署,還需要因為兩者的依賴關系建立分布式調用,這無疑增加了設計、開發和運維的成本。

    有經驗的設計人員可能會發現第三種解決辦法:把相同的業務邏輯抽取成一個新的服務,作為公共服務同時供上述兩個業務模塊使用。這只是筆者將會馬上討論的方案——使用領域驅動設計。

     

     

     

    面向過程的領域驅動設計方式(充血模型)

     

           為了節省篇幅,這里就直接以最復雜的業務需求來進行設計。

    領域驅動設計的一個重要的概念是領域模型,首先,我們根據業務領域抽象出以下核心業務對象模型:


     

    Account:賬戶,是整個系統的最核心的業務對象,它包括以下屬性:對象標識、賬戶號、是否有效標識、余額、凍結金額、賬戶交易明細集合、賬戶信用等級。

    AccountTransactionDetails:賬戶交易明細,它從屬于賬戶,每個賬戶有多個交易明細,它包括以下屬性:對象標識、所屬賬戶、交易類型、交易發生金額、交易發生時間。

    AccountCreditDegree:賬戶信用等級,它用于限制賬戶的每筆交易發生金額,包含以下屬性:對象標識、對應賬戶、信用指數。

    BankTransactionFeeCalculator:銀行交易手續費用計算器,它包含一個常量:每筆交易的手續費上限。

     

    我們知道,領域對象除了具有自身的屬性和狀態之外,它的一個很重要的標志是,它具有屬于自己職責范圍之內的行為,這些行為封裝了其領域內的領域業務邏輯。于是,我們進行進一步的建模,根據業務需求為領域對象設計業務方法:


     

    根據職責單一的原則,我們把功能需求中描述的功能合理的分配到不同的領域對象中:

    Account

    • credit:向銀行賬戶存入金額,貸記
    • debit:從銀行賬戶劃出金額,借記
    • transferTo:把固定金額轉入指定賬戶
    • createTransactionDetails:創建交易明細賬
    • updateCreditIndex:更新賬戶的信用指數

    (我們可以看到,后兩個業務方法被聲明為protected,具體原因見后述)

     

    AccountCreditDegree

    • getMaxTransactionAmount:獲取所屬賬戶的每筆交易最大金額

     

    BankTransactionFeeCalculator

    • calculateTransactionFee:根據交易信息計算該筆交易的手續費

     

    經過這樣的設計,前例中所有放置在服務對象的業務邏輯被分別劃入不同的負責相關職責的領域對象當中,下面的時序圖描述了AccountingServiceImpl的轉賬業務的實現邏輯(為了簡化邏輯,我們忽略掉事物、持久化等邏輯):


     

    再看看AccountingServiceImpl.transfer()的實現邏輯:

     


    public class AccountingServiceImpl implements AccountingService {

           
    public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountDomainException {

                  Account srcAccount 
    = accountRepository.getAccount(srcAccountId);

                  Account destAccount 
    = accountRepository.getAccount(destAccountId);

                  srcAccount.transferTo(destAccount,amount);

           }

    }

     

    我們可以看到,上例那些復雜的業務邏輯:判斷余額是否足夠、判斷賬戶是否可用、改變賬戶余額、計算手續費、判斷交易額度、產生交易明細賬……,都不再存在于AccountingServiceImplementationtransfer方法中,它們被委派給負責這些業務的領域對象的業務方法中去,現在應該猜到為什么Account中有兩個方法被聲明為protected了吧,因為他們是在debitcredit方法被調用時,由這兩個方法調用的,對于AccountingServiceImpl來說,由于產生交易明細(createTransactionDetails)和更新賬戶信用指數(updateCreditIndex)都不屬于其職責范圍,它不需要也無權使用這些邏輯。

     

    我們可以看到,使用領域驅動設計至少會帶來下述優點:

    • 業務邏輯被合理的分散到不同的領域對象中,代碼結構更加清晰,可讀性,可維護性更高。
    • 對象職責更加單一,內聚度更高。
    • 復雜的業務模型可以通過領域建模(UML是一種主要方式)清晰的表達,開發人員甚至可以在不讀源碼的情況下就能了解業務和系統結構,這有利于對現存的系統進行維護和迭代開發。

     

    再看看如果這時需要加入網上商城的一個新的模塊,開發人員需要怎么去做,還記得上面提過的第三種方案嗎?就是把賬戶貸記和借記的相關業務抽取到成一個公共服務,同時供銀行在線支付系統和網上商城系統服務,其實這個公共的服務,本質上就是這些具有領域邏輯的領域對象:Account、AccountCreditDegree……,由此我們又可以發現領域驅動設計的一大優點:

    • 系統高度模塊化,代碼重用度高,不會出現太多的重復邏輯。

     

    筆者經驗尚淺,而且文筆拙劣,希望通過這樣的一個場景的分析比較,能讓讀者初步認識到基于面向對象的領域驅動設計的威力,并在實際項目中嘗試應用。本篇是領取驅動設計系列博文的第一篇,在系列文章的第二篇博文中,筆者將會淺析VODTO、DOPO的概念、用處和區別,敬請各位對本系列博文感興趣的讀者關注并給予指導修正。

     

          

    posted @ 2010-05-15 21:58 Johnny.Liang 閱讀(13352) | 評論 (20)編輯 收藏

    筆者從事開發多年,有這樣一種感覺,查看一些開源項目,如Spring、Apache Common等源碼是一件賞心悅目的事情,究其原因,無外兩點:1)代碼質量非常高;2)命名特別規范(這可能跟老外的英語水平有關)。
    要寫高質量的代碼,不是一件容易的事,需要長年累月的鍛煉,是一個量變到質變的過程,但要寫好命名,只需要有比較好的英語語法基礎和一種自我意識即可輕松達到。本博文將會結合本人的開發經驗,總結出若干命名規則,這些命名規則純屬個人的使用習慣,不代表是一種理想的規則,在這里列舉出來,供大家交流討論。

    1.切忌使用沒有任何意義的英語字母進行命名:
        for(int i=0; i<10; i++) {
            ...
        }
    這是在很多教Java基本語法的書上常見的代碼片斷,作為教學材料,這樣寫無可厚非,但作為真正的代碼編寫,程序員必須要養成良好的習慣,不要使用這種沒有任何含義的命名方式,這里可以使用“index”。

    2.切忌使用拼音,甚至是拼音首字母組合:
        cishu =5; // 循環的次數
        zzje = 1000.00 // 轉賬金額
    筆者在做代碼檢查的時候,無數次遇到過這樣的命名,使人哭笑不得

    3.要使用英文,而且要使用準確的英語,無論是拼寫還是語法:
    • 名詞單數,必須使用單數英文,如Account、Customer。
    • 對于數組,列表等對象集合的命名,必須使用復數,而且最好按照英文的語法基礎知識使用準確的復數形式,如 List<Account> accounts、Set<Strategy> strategies。
    • 對于boolean值的屬性,很多開發人員習慣使用isXXX,如isClose(是否關閉),但這里有兩點建議:1)最好不要帶“is”,因為JavaBean的規范,為屬性生成get/set方法的時候,會用“get/set/is”,上面的例子,生成get/set方法就會變成“getIsClose/isIsClose/getIsClose”,非常別扭;2)由于boolean值通常反映“是否”,所以準確的用法,應該是是用“形容詞”,上面的例子,最終應該被改為 closed,那么get/set方法就是“getClosed/isColsed/setClosed”,非常符合英語閱讀習慣。

    4.方法名的命名,需要使用“動賓結構短語”或“是動詞+表語結構短語”,筆者曾看到過千奇百怪的方法命名,有些使用名詞,有些甚至是“名詞+動詞”,而且,如果賓語是一個對象集合,還是最好使用復數:
        createOrder(Order order) good   
        orderCreate(Order order) bad
        removeOrders(List<Order> orders) good
        removeOrder(List<Order> order) bad

    5.對于常見的“增刪改查”方法,命名最好要謹慎:
    • 增加:最常見使用create和add,但最好根據英語的語義進行區分,這有助于理解,create代表創建,add代表增加。比如,要創建一個Student,用createStudent要比用addStudent好,為什么?想想如果有個類叫Clazz(班級,避開Java關鍵字),現在要把一個Student加入到一個Clazz,Clazz很容易就定義了一個 addStudent(Student student)的方法,那么就比較容易混淆。
    • 修改:常見的有alter、update、modify,個人覺得modify最準確。
    • 查詢:對于獲取單個對象,可以用get或load,但個人建議用get,解釋請見第7點的說明,對于不分條件列舉,用list,對于有條件查詢,用search(最好不要用find,find在英文了強調結果,是“找到”的意思,你提供一個“查詢”方法,不保證輸入的條件總能“找到”結果)。
    • 刪除:常見的有delete和remove,但刪除建議用delete,因為remove有“移除”的意思,參考Clazz的例子就可以理解,從班級移除一個學生,會用removeStudent。

    6.寧愿方法名冗長,也不要使用讓人費解的簡寫,筆者曾經遇到一個方法,判斷“支付賬戶是否與收款賬戶相同”,結果我看到一個這樣的命名:
        checkIsOrderingAccCollAccSame(...) 很難理解,我馬上把它改為:
        isOrderingAccountSameAsCollectionAccount(...),雖然有點長,但非常容易閱讀,而且這種情況總是出現得比較少。

    7.如果你在設計業務系統,最好不要使用技術化的術語去命名。筆者曾經工作的公司曾經制訂這樣的命名規則,接口必須要以“I”開頭,數據傳輸對象必須以“DTO”作為后綴,數據訪問對象必須以“DAO”作為后綴,領域對象必須以“DO”作為后綴,我之所以不建議這種做法,是希望設計人員從一開始就引導開發人員,要從“業務”出發考慮問題,而不要從“技術”出發。所以,接口不需要非得以“I”開頭,只要其實現類以“Impl”結尾即可(注:筆者認為接口是與細節無關的,與技術無關,但實現類是實現相關的,用技術化術語無可口非),而數據傳輸對象,其實無非就是保存一個對象的信息,因此可以用“**Info”,如CustomerInfo,領域對象本身就是業務的核心,所以還是以其真實名稱出現,比如Account、Customer,至于“DAO”,這一個詞來源于J2ee的設計模式,筆者在之前的項目使用“***Repository”命名,意味“***的倉庫”,如AccountRepository,關于“Repository”這個詞的命名,是來源于Eric Evans的《Domain-Driven Design》一書的倉庫概念,Eric Evans對Repository的概念定義是:領域對象的概念性集合,個人認為這個命名非常的貼切,它讓程序員完全從技術的思維中擺脫出來,站在業務的角度思考問題。說到這里,可能有人會反駁:像Spring、Hibernate這些優秀的框架,不是都在用“I”作為接口開頭,用“DAO”來命名數據訪問對象嗎?沒錯!但千萬別忽略了語義的上下文,Spring、Hibernate框架都是純技術框架,我這里所說的場景是設計業務系統。

    8.成員變量不要重復類的名稱,例如,很多人喜歡在Account對象的成員變量中使用accountId,accountNumber等命名,其實沒有必要,想想成員變量不會鼓孤立的存在,你引用accountId,必須是account.accountId,用account.id已經足夠清晰了。

    “勿以善小而不為,勿以惡小而為之”、“細節決定成敗”,有太多的名言告訴我們,要注重細節。一個優秀的程序員,必須要有堅實的基礎,而對于命名規則這樣容易掌握的基礎,我們何不現行?
    posted @ 2010-04-29 22:54 Johnny.Liang 閱讀(6604) | 評論 (17)編輯 收藏

    今天一個曾經共事的同行問我:“要從編碼轉為設計,大概需要多長時間?”
    我的回答是:“編碼本身就是一種設計,你可以設計你的代碼。”

    其實正如概要設計與詳細設計,系統設計與架構設計一樣,編碼與設計也是沒有明顯的邊界,每個正確成長的程序員,都必須從編碼開始,慢慢鍛煉抽象思維、邏輯思維、面向對象思維,然后慢慢的過渡到系統設計,再隨著經驗和知識的積累,慢慢過渡到架構設計。下面我將會以最近的一個手頭的編碼任務,簡單介紹一下如何“設計”你的代碼。

    任務是這樣的,某銀行支付系統的客戶端接收銀行用戶錄入的轉賬數據,當轉賬數據被審批通過后,狀態轉變為“transfer”,同時,該客戶端需要通過JMS以異步的方式向支付系統后臺發送一條帶有轉賬記錄(Instruction)的消息,后端在接收到信息之后,需要根據Instruction的一些相關信息,首先確定這筆轉賬數據是直接發送給真正進行轉賬的清算(Clearing)銀行系統,還是停留在后端系統,等待后端系統中需要執行的工作流程(work flow)。而后端系統需要對Instruction執行的工作流程有兩個,同時需要根據Instruction的一些相關信息進行選擇。
    為了簡化復雜度,我這里假設系統有一個InstructionHandleMsgDrivenBean,該bean有一個onMessage()方法,所有業務邏輯需要在該方法中實現。

    同時解釋一下詳細的業務細節:
    • 判斷Instruction是否需要停留在后端等待執行指定的工作流程有三個條件:xx、yy、zz,當三個條件都為true時,停留。
    • 判斷Instruction需要走A流程還是B流程,由4個因素的組合確定,如果用“Y”代表true,“N”代表false,那么由這個四個因素組成的“XXXX”一共有16種組合,不同的組合分別走A和B流程,如:YYNN、YYNY to A,NNYY、NNNY to B,……不累贅。
    好了,對于一個純編程人員來說,拿到這樣的需求,感覺邏輯很簡單,可以直接編碼了,于是,他開始一行一行的編寫代碼(偽代碼):

    public void onMessage(InstructionInfo instructionInfo) {
        if(xx && yy && zz) { // 停留在后端等待執行指定的工作流程
            // 根據每種組合進行條件判斷,走哪個流程
            if(a==true && b==true && c==true && d==true {
                ...
            }
            else if(...) {...}
            else if(...) {...}
            ...
            else(...) {...}    
        }
    }

    這種做法是最為開發人員歡迎的,因為它簡單、直接,但這種做法也恰恰反映了開發人員的通病——使用Java編寫純面向過程的代碼。

    好了,說了一大堆,如何“設計”你的代碼呢?答案是:使用面向對象思維:

    我們拿到需求之后,可以分析,這個需求大體上分為兩部分:
    • 判斷是否需要停留在后端等待執行指定的工作流程的部分
    • 選擇走哪個工作流程的部分

    有了這個前提,我可以設計出兩個職責單一的對象了:

    public class InstructionHandleDecisionMaker {
        public static boolean isHandledByBackEnd(InstructionInfo info) {
            return (isXX(...) && isYY(...) && isZZ(...));
        }

        private booolean isXX(...) {
            //TODO Implement the logic
            return false;
        }
        private booolean isYY(...) {
            //TODO Implement the logic
            return false;
        }
        private booolean isZZ(...) {
            //TODO Implement the logic
            return false;
        }
    }

    public class InstructionWorkFlowSelector {
        private static Map mapping = new HashMap();
        static {
            mapping.input("YYNN",WorkFlow.A);
            mapping.input("NNYY",WorkFlow.B);
            ...
        }

        public static WorkFlow getWorkFlow(Instruction info) {
            StringBuilder result = new StringBuilder();
            result.append(isA(...)).append(isB(...));
            result.append(isC(...)).append(isD(...));
            return mapping.get(result.toString());
        }
        private static String isA(...) {
            //TODO Implment the logic
            return "N";
        }
        private static String isB(...) {
            //TODO Implment the logic
            return "N";
        }
        private static String isC(...) {
            //TODO Implment the logic
            return "N";
        }
        private static String isD(...) {
            //TODO Implment the logic
            return "N";
        }
    }

    可以看到,我先按職責劃分了類,再按職責抽取了私有方法,“框架”設計好 ,為了讓編譯通過,我上面完整的填寫了代碼的,然后加上TODO標識,然后,我可以編寫我的onMessage方法了:

    public void onMessage(InstructionInfo instructionInfo) {
        if( InstructionHandleDecisionMaker.isHandledByBackEnd(...) ) {
            WorkFlow wf =InstructionWorkFlowSelector.getWorkFlow(...);
            //TODO Implment the logic
        }
    }

    到目前為止,我已經用純面向對象的思維方式“設計”好我的代碼了,這時,我思維非常清晰,因而代碼結構也非常清晰,職責單一,內聚高,耦合低,最后,我可以根據需求文檔的細節(沒有描述)慢慢的編寫我的實現了。

    復雜的事物總是由一些較簡單的事物組成,而這些較簡單的事物也是由更簡單的事物組成,如此類推。因此,在編寫代碼的時候,先用面向對象的思維把復雜的問題分解,再進一步分解,最后把簡單的問題各個擊破,這就是一種設計。開發人員只要養成這種習慣,即使你每天都只是做最底層的編碼工作,其實你已經在參與設計工作了,隨著知識和經驗的累積,慢慢的,你從設計代碼開始,上升為設計類、方法,進而是設計模塊,進而設計子系統,進而設計系統……,最終,一步一步成為一個優秀的架構師。

    最后,有一個真理奉獻給浮躁的程序員:

    優秀的架構師、設計師,必定是優秀的程序員,不要因為你的職位上升了,就放棄編碼。

    補充說明:本博文純粹是討論一種思維習慣,不要把其做法生搬硬套,不管實際情況,直接在編碼的時候這樣做,不見得是最好的選擇。在實際編碼中,有如下問題你必須考慮:
    • 你需要考慮業務邏輯的可重用性和復雜程度,是否有必要設計出新的類或抽取新的私有方法來封裝邏輯,或者直接在原方法上編碼(如果足夠簡單)。
    • 新的業務邏輯,是否在某些地方已經存在,可以復用,即使不存在,這些邏輯是應該封裝到新的類中,還是應該放置到現有的類中,這需要進行清晰的職責劃分。
    • 需要在設計和性能上作出權衡。
    • 如果在現成的系統中增加新的功能,而現成系統的編碼風格與你想要的相差很遠,但你又沒有足夠的時間成本來進行重構,那么還是應該讓你的代碼與現成系統保持一致的風格。

    posted @ 2010-04-28 00:51 Johnny.Liang 閱讀(4952) | 評論 (8)編輯 收藏

    筆者在N年前曾經參與一個第三方支付平臺的設計開發工作,這過程中研究了某個國內著名的第三方支付平臺,就是因為該平臺的設計者輕易

    的相信外部傳入的數據,不進行校驗,導致被我發現了一個致命的漏洞,從而測試性的盜取了2分錢,這個事情一直到2個月后,該平臺通過

    與網銀對賬才發現,由于不知道這個致命的漏洞是否已經修復,避免有惡意的人使用該方式去做違法的事情,我在這里只簡單描述該漏洞,

    而不公開該支付平臺的真實身份。

    該漏洞出現在充值業務上,實現邏輯請看下面的Sequence diagram:




    描述一下流程:

    1.客戶打開第三方支付平臺的充值頁面,選擇一個銀行,填入充值數據(充值虛擬賬戶、充值金額等),提交表單。
    2.充值頁面把數據(包括充值數據,第三方支付平臺在該銀行注冊的商戶標識)使用銀行提供的加密工具進行加密,傳遞給指定銀行的網銀系統前臺。
    3.網銀系統前臺產生一張轉賬訂單,并要求用戶輸入賬號、密碼,然后提交。
    4.網銀系統后端校驗賬號和密碼,然后根據用戶賬號,充值金額,第三方支付平臺商戶標識,把金額從用戶賬號轉賬到第三方支付平臺賬號。
    5.網銀系統把確認數據(充值虛擬賬戶、充值金額、充值結果、轉入商戶標識等)加密后重定向到第三方支付平臺結果頁面。
    6.頁面把確認數據提交給后臺,并向客戶顯示充值成功。
    7.第三方支付平臺后臺接收到確認數據,把虛擬金額充入虛擬賬戶,完成整個流程。

    漏洞描述:

    我們可以看到,用戶在第三方支付平臺充值頁面提交數據,和網銀向第三方支付平臺發送確認數據,都需要對數據進行非對稱加密,但在當

    時,我發現某一銀行,竟然不提供發送充值數據時的加密工具,這意味著,選擇這家銀行,用戶必須以明文的格式提交充值數據,好了,這

    下我可以嘗試進行man in the middle attack了,在我提交表單一刻,我把頁面的HTML腳本修改了,把提交時由第三方支付平臺指定的在所

    選銀行注冊的商戶標識修改為另外一個商戶的標識(假設是我自己注冊的商戶),OK,這意味著我的充值金額從我的賬號轉到我修改后的注

    冊商戶賬號,這時,該網銀把確認數據(注意,包括轉入商戶標識,這時的商戶標識是我修改后的標識)發送回第三方支付平臺,其實,如

    果第三方支付平臺如果能進一步校驗確認數據中的注冊商戶標識,就能發現注冊商戶標識被惡意篡改,從而導致充值失敗,但該平臺的設計

    人員卻假設之前的充值頁面已經自動填入了自己的注冊商戶標識,這里并不作校驗,從而導致這個致命的漏洞,錢沒有轉到自己的賬號上,

    而自己卻把等額的虛擬資金充值到了第三方虛擬賬戶中。


    特別聲明:本文內容純屬用于技術研討,任何人嘗試使用本文的信息實施非法行為而導致的任何損失和責任,本人概不負責。
    posted @ 2010-04-27 21:51 Johnny.Liang 閱讀(3404) | 評論 (0)編輯 收藏

    最近在項目中接到一個任務,我負責后臺開發,另一開發人員負責前臺開發。任務非常簡單,請看下面的類圖。



    A和B有一個單向的關聯關系,現在要為A增加一個屬性boolean resident,該屬性值有如下簡單的業務邏輯決定(偽代碼):

    if(a.x == a.b.x)
       resident = true;
    else
       resident = false;

    前臺的查看頁面需要顯示這個值,當用戶修改a.x或者a.b.x時,要觸發一個事件,實時的在前臺更新resident,后臺需要以這個resident來

    做某些業務邏輯的判斷。

    我們知道,由于resident完全可以由a.x與a.b.x決定,屬于一個冗余數據,不需要顯式的增加該屬性。很快,前臺的那位開發人員就輕松的

    告訴我,這個任務已經寫完了,原來他是在頁面寫了一個JS腳本,當a.x或a.b.x發生改變時,觸發一個onChange()事件,里面就實現了上述

    的邏輯。我一看,感覺問題來了,然后我告訴他,我已經在后臺的服務提供了一個方法,用于判斷resident的值,他應該在事件觸發的時候

    ,通過異步方式調用后臺方法來獲取reisdent的值。他一聽覺得非常奇怪,這么簡單的邏輯,為什么需要使用異步方式去調用后臺這么麻煩

    ,于是我就從設計的角度跟他解釋,resident由 a.x與a.b.x決定,這是一個業務邏輯,從職責劃分的設計原則分析,前臺只負責顯示邏輯,

    業務邏輯屬于后臺的職責。接著,他又提出了疑問,說這里其實只有一行代碼,沒有必要分得那么清,這樣調用不但麻煩,而且性能相對較

    低。

    這個例子,反映出了很多開發人員的通病,沒有真正從事物的本質考慮問題,只從代碼的數量去考慮。

    上述的做法存在兩個問題:

    1.開發人員沒有從職責劃分的角度考慮問題,而只是貪圖一時的方便,或者覺得簡單的一兩行代碼不需要進行設計,但實際上,上面的問題

    必然導致重復代碼。上面的例子,即使只有一行代碼,但在系統的前后臺卻出現了兩份相同的邏輯,假設以后某天業務邏輯發生了變化,那

    么,必須在兩個地方(甚至更多的地方)進行修改、測試,如果漏掉了一個地方,就會導致Bug的出現。作為一個開發人員,如果不注意這些

    細節,隨意的拷貝和重復代碼,長期下來,一個系統就會遍布無數的重復代碼,系統越往后期就越難維護,最終陷于崩潰。

    2.如果作為后臺開發人員,看到前臺已經實現了這個邏輯,而在前臺往后臺傳遞數據的時候,把resident也傳遞進來,從而認為后臺就不需

    要重復這段邏輯,直接拿著前臺傳過來的這個resident來進行業務判斷,那么就可能會給系統帶來致命的漏洞,因為數據在傳輸過程中,很

    可能被有意或無意的修改,不在后臺進行業務規則校驗這個錯誤是常見的,但也是致命的。


    有一個原則開發人員特別容易犯,也一定要切記:

    在Web應用中,相信客戶端提交的數據是正確的,不在業務層進行校驗,這是致命的錯誤。

    筆者會在下一篇博文中,詳細說明某國內著名的第三方支付平臺,因為犯了這個低級的錯誤,而導致系統出現致命的漏洞,敬請關注。
    posted @ 2010-04-27 21:43 Johnny.Liang 閱讀(4506) | 評論 (9)編輯 收藏

    主站蜘蛛池模板: 亚洲大尺度无码无码专区| 国产卡二卡三卡四卡免费网址| 亚洲一区二区三区国产精华液| 亚洲国产精品无码久久久| 亚洲日韩国产二区无码| 边摸边吃奶边做爽免费视频99| 久久久久久噜噜精品免费直播| 十八禁无码免费网站| 久久久久亚洲精品无码系列| 久久中文字幕免费视频| 久久亚洲精品成人AV| 亚洲成aⅴ人片久青草影院按摩| 久久久受www免费人成| 国产亚洲美日韩AV中文字幕无码成人 | 永久免费视频v片www| 亚洲人午夜射精精品日韩| 亚洲视频国产视频| 国国内清清草原免费视频99| 亚洲精品WWW久久久久久| 精品国产污污免费网站入口| 亚洲A∨无码无在线观看| 亚洲免费中文字幕| 亚洲av日韩综合一区二区三区| 亚洲国产精品无码久久九九| 好男人资源在线WWW免费| 亚洲免费在线播放| 插鸡网站在线播放免费观看| 亚洲人成电影福利在线播放| 成年免费大片黄在线观看岛国| 亚洲精品国产av成拍色拍| 亚洲精品视频在线看| 99爱在线精品视频免费观看9| 国产精品亚洲四区在线观看| 一级毛片全部免费播放| 亚洲AV成人影视在线观看| 亚洲AV网站在线观看| 91亚洲视频在线观看| 免费精品国产自产拍观看| 中文字幕无码亚洲欧洲日韩| 亚洲国产精品尤物yw在线| 99在线视频免费|