背景
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)的中心內(nèi)容是如何將業(yè)務(wù)領(lǐng)域概念映射到軟件工件中。大部分關(guān)于此主題的著作和文章都以Eric Evans的書《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》為基礎(chǔ),主要從概念和設(shè)計(jì)的角度探討領(lǐng)域建模和設(shè)計(jì)情況。這些著作討論實(shí)體、值對(duì)象、服務(wù)等DDD的主要內(nèi)容,或者談?wù)? 通用語言 、界定的上下文(Bounded Context)和防護(hù)層(Anti-Corruption Layer)這些的概念。
相關(guān) 廠商 內(nèi)容
本文旨在從實(shí)踐的角度探討領(lǐng)域建模和設(shè)計(jì),涉及如何著手處理領(lǐng)域模型并實(shí)際地實(shí)現(xiàn)它。我們將著眼于技術(shù)主管和架構(gòu)師在實(shí)現(xiàn)過程中能用到的指導(dǎo)方針、最佳實(shí)踐、框架及工具。領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)和開發(fā)也受一些架構(gòu)、設(shè)計(jì)、實(shí)現(xiàn)方面的影響,比如:
- 業(yè)務(wù)規(guī)則
- 持久化
- 緩存
- 事務(wù)管理
- 安全
- 代碼生成
- 測(cè)試驅(qū)動(dòng)開發(fā)
- 重構(gòu)
本文討論這些不同的因素在項(xiàng)目實(shí)施的整個(gè)生命周期中怎樣對(duì)其產(chǎn)生影響,還有架構(gòu)師在實(shí)現(xiàn)成功的DDD中應(yīng)該去尋求什么。我會(huì)先列出領(lǐng)域模型應(yīng)該具備的典型特征,以及何時(shí)在企業(yè)中使用領(lǐng)域模型(相對(duì)于根本不使用領(lǐng)域模型,或使用貧血的領(lǐng)域模型來說)。
文章包括一個(gè)貸款處理示例應(yīng)用,來演示如何將設(shè)計(jì)立場(chǎng)、以及這里討論的開發(fā)最佳實(shí)踐,應(yīng)用在真實(shí)的領(lǐng)域驅(qū)動(dòng)開發(fā)項(xiàng)目之中。示例應(yīng)用用了一些框架去實(shí) 現(xiàn)貸款 處理領(lǐng)域模型,比如Spring、Dozer、Spring Security、JAXB、Arid POJOs和Spring Dynamic Modules。示例代碼用Java編寫,但對(duì)大多數(shù)開發(fā)人員來說,不論語言背景如何,代碼都是很容易理解的。
引言
領(lǐng)域模型帶來了一些好處,其中有:
- 有助于團(tuán)隊(duì)創(chuàng)建一個(gè)業(yè)務(wù)部門與IT部門都能理解的通用模型,并用該模型來溝通業(yè)務(wù)需求、數(shù)據(jù)實(shí)體、過程模型。
- 模型是模塊化、可擴(kuò)展、易于維護(hù)的,同時(shí)設(shè)計(jì)還反映了業(yè)務(wù)模型。
- 提高了業(yè)務(wù)領(lǐng)域?qū)ο蟮目芍赜眯院涂蓽y(cè)性。
反過來,如果IT團(tuán)隊(duì)在開發(fā)大中型企業(yè)軟件應(yīng)用時(shí)不遵循領(lǐng)域模型方法,我們看看會(huì)發(fā)生些什么。
不投放資源去建立和開發(fā)領(lǐng)域模型,會(huì)導(dǎo)致應(yīng)用架構(gòu)出現(xiàn)“肥服務(wù)層”和“貧血的領(lǐng)域模型”,在這樣的架構(gòu)中,外觀類(通常是無狀態(tài)會(huì)話Bean)開始 積聚越 來越多的業(yè)務(wù)邏輯,而領(lǐng)域?qū)ο髣t成為只有g(shù)etter和setter方法的數(shù)據(jù)載體。這種做法還會(huì)導(dǎo)致領(lǐng)域特定業(yè)務(wù)邏輯和規(guī)則散布于多個(gè)的外觀類中(有些 情況下還會(huì)出現(xiàn)重復(fù)的邏輯)。
在大多數(shù)情況下,貧血的領(lǐng)域模型沒有成本效益;它們不會(huì)給公司帶來超越其它公司的競(jìng)爭(zhēng)優(yōu)勢(shì),因?yàn)樵谶@種架構(gòu)里要實(shí)現(xiàn)業(yè)務(wù)需求變更,開發(fā)并部署到生產(chǎn)環(huán)境中去要花費(fèi)太長(zhǎng)的時(shí)間。
在考慮DDD實(shí)現(xiàn)的項(xiàng)目中各種架構(gòu)和設(shè)計(jì)因素之前,讓我們先看看富領(lǐng)域模型的特性:
- 領(lǐng)域模型應(yīng)該側(cè)重于具體的業(yè)務(wù)操作領(lǐng)域。它應(yīng)該結(jié)合業(yè)務(wù)模型、策略和業(yè)務(wù)流程。
- 它應(yīng)該與業(yè)務(wù)中的其它領(lǐng)域,還有應(yīng)用架構(gòu)中的其它層隔離開來。
- 它應(yīng)該可重用,以避免相同的核心業(yè)務(wù)領(lǐng)域元素有任何重復(fù)的模型和實(shí)現(xiàn)。
- 模型應(yīng)該設(shè)計(jì)得與應(yīng)用中的其它層松耦合,這意味著領(lǐng)域?qū)优c上下兩層(即數(shù)據(jù)庫和外觀類)都沒有依賴關(guān)系。
- 它應(yīng)當(dāng)是一個(gè)抽象的、清晰劃分的層次,以使維護(hù)、測(cè)試、版本處理更容易??稍谌萜魍猓◤腎DE中)對(duì)領(lǐng)域類進(jìn)行單元測(cè)試。
- 它應(yīng)該用POJO編程模型來設(shè)計(jì),沒有任何技術(shù)或框架依賴性(我總是告訴公司里我工作的項(xiàng)目團(tuán)隊(duì),我們軟件開發(fā)用的技術(shù)是Java)。
- 領(lǐng)域模型應(yīng)該獨(dú)立于持久化實(shí)現(xiàn)的細(xì)節(jié)(盡管技術(shù)確實(shí)會(huì)對(duì)模型有一些限制)。
- 它應(yīng)該最小程度地依賴于任何基礎(chǔ)設(shè)施框架,因?yàn)樗鼘⒈冗@些框架更經(jīng)久,我們也不希望與任何外部框架緊耦合。
為了實(shí)現(xiàn)軟件開發(fā)中更高的投資回報(bào)率(ROI),業(yè)務(wù)單位和IT的高級(jí)管理人員必須在業(yè)務(wù)領(lǐng)域建模及其實(shí)現(xiàn)的投資上(時(shí)間、金錢和資源)全力以赴。讓我們來看看實(shí)現(xiàn)領(lǐng)域模型需要的其它因素。
- 團(tuán)隊(duì)?wèi)?yīng)該經(jīng)常接近業(yè)務(wù)領(lǐng)域主題專家。
- IT團(tuán)隊(duì)(建模者、架構(gòu)師和開發(fā)人員)應(yīng)具備良好的建模、設(shè)計(jì)技能。
- 分析師應(yīng)該具有良好的業(yè)務(wù)流程建模技能。
- 架構(gòu)師和開發(fā)人員應(yīng)該有豐富的面向?qū)ο笤O(shè)計(jì)(OOD)和編程(OOP)經(jīng)驗(yàn)。
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)在企業(yè)架構(gòu)中的作用
領(lǐng)域建模和DDD在企業(yè)架構(gòu)(EA)中發(fā)揮著重要的作用。因?yàn)镋A的目標(biāo)之一就是結(jié)合IT和業(yè)務(wù)部門,業(yè)務(wù)實(shí)體的代表——領(lǐng)域模型就是EA的核心部分。這就是為什么大多數(shù)EA組件(業(yè)務(wù)或基礎(chǔ)設(shè)施)應(yīng)該圍繞領(lǐng)域模型設(shè)計(jì)和實(shí)現(xiàn)的原因。
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)和SOA
面向服務(wù)的體系架構(gòu)(SOA)最近幫助團(tuán)隊(duì)構(gòu)建基于業(yè)務(wù)流程的軟件構(gòu)件和服務(wù)、加速新產(chǎn)品上市時(shí)間的勢(shì)頭越來越強(qiáng)勁。領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)是SOA的一個(gè)關(guān)鍵因素,因?yàn)樗兄诜庋b領(lǐng)域?qū)ο笾械臉I(yè)務(wù)邏輯和規(guī)則。領(lǐng)域模型也提供了定義服務(wù)契約使用的語言和上下文。
如果還沒有領(lǐng)域模型,SOA的實(shí)行就應(yīng)該包括領(lǐng)域模型的設(shè)計(jì)和實(shí)現(xiàn)。如果我們太過強(qiáng)調(diào)SOA服務(wù)、忽略了領(lǐng)域模型的重要性,那我們?cè)趹?yīng)用架構(gòu)中最終得到的就是一個(gè)貧血的領(lǐng)域模型和臃腫的服務(wù)。
理想的情況是,在開發(fā)應(yīng)用層和SOA組件的同時(shí),迭代地實(shí)現(xiàn)DDD,因?yàn)閼?yīng)用層和SOA組件都是領(lǐng)域模型要素的直接消費(fèi)者。使用豐富的領(lǐng)域?qū)崿F(xiàn),通 過給領(lǐng) 域?qū)ο筇峁┮粋€(gè)殼(代理),SOA設(shè)計(jì)將變得相對(duì)簡(jiǎn)單。但如果我們太過于關(guān)注SOA層,在后端卻沒有一個(gè)像樣的領(lǐng)域模型,業(yè)務(wù)服務(wù)就會(huì)調(diào)用不完整的領(lǐng)域模 型,這可能會(huì)導(dǎo)致出現(xiàn)一個(gè)脆弱的SOA架構(gòu)。
項(xiàng)目管理
領(lǐng)域建模項(xiàng)目通常包括以下步驟:
- 首先為業(yè)務(wù)流程建模并文檔化。
- 選擇一個(gè)候選的業(yè)務(wù)流程,與業(yè)務(wù)領(lǐng)域?qū)<乙黄鹗褂猛ㄓ谜Z言來文檔化業(yè)務(wù)流程。
- 識(shí)別候選業(yè)務(wù)流程需要的所有服務(wù)。這些服務(wù)本質(zhì)上可以是原子的(單步的)或組合好的(多步的,有無工作流皆可)。它們也可以是業(yè)務(wù)(比如承?;蛸Y金)或基礎(chǔ)設(shè)施(比如電子郵件或工作調(diào)度)。
- 對(duì)上一步識(shí)別的服務(wù)所使用的對(duì)象,確定并文檔化其狀態(tài)和行為。
一開始關(guān)注業(yè)務(wù)領(lǐng)域核心元素的時(shí)候,就將模型保持在高水平是非常重要的。
從項(xiàng)目管理的觀點(diǎn)來看,真實(shí)的DDD實(shí)現(xiàn)項(xiàng)目和其它軟件開發(fā)項(xiàng)目所包含的階段是一樣的。這些階段包括:
- 對(duì)領(lǐng)域進(jìn)行建模
- 設(shè)計(jì)
- 開發(fā)
- 單元測(cè)試和集成測(cè)試
- 基于設(shè)計(jì)和開發(fā)來完善、重構(gòu)領(lǐng)域模型(模型概念的持續(xù)集成(CI))。
- 使用更新的領(lǐng)域模型重復(fù)上述步驟(領(lǐng)域?qū)崿F(xiàn)的CI)。
非常適合在這里使用敏捷軟件開發(fā)方法學(xué),因?yàn)槊艚莘椒ㄗ⒅赜诮桓渡虡I(yè)價(jià)值,恰好DDD側(cè)重于結(jié)合軟件系統(tǒng)和業(yè)務(wù)模型。此外,就DDD迭代的特性來 說,SCRUM或DSDM這樣的敏捷方法對(duì)項(xiàng)目管理來說也是更好的框架。結(jié)合使用SCRUM(適用于項(xiàng)目管理)和XP(適用于軟件開發(fā)目標(biāo))方法對(duì)處理 DDD實(shí)現(xiàn)項(xiàng)目來說非常好。
DDD迭代周期的項(xiàng)目管理模型如圖1所示。
圖1. DDD迭代周期圖(點(diǎn)擊查看大圖)
領(lǐng)域建模結(jié)束時(shí)可以開始領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)。關(guān)于如何開始實(shí)現(xiàn)領(lǐng)域?qū)ο竽P停琑amnivas Laddad 推薦如下的步驟 。他強(qiáng)調(diào)要更側(cè)重于領(lǐng)域模型中的領(lǐng)域?qū)ο螅皇欠?wù)。
- 從領(lǐng)域?qū)嶓w和領(lǐng)域邏輯開始。
- 不要一開始就從服務(wù)層開始,只添加那些邏輯不屬于任何領(lǐng)域?qū)嶓w或值對(duì)象的服務(wù)。
- 利用通用語言、 契約式設(shè)計(jì) (DbC)、自動(dòng)化測(cè)試、CI和重構(gòu),使實(shí)現(xiàn)盡可能地與領(lǐng)域模型緊密結(jié)合。
從設(shè)計(jì)和實(shí)現(xiàn)的角度來看,典型的DDD框架應(yīng)該支持以下特征。
- 應(yīng)該是一個(gè)以POJO(如果你的公司以.Net為主營(yíng),就是POCO)為基礎(chǔ)的架構(gòu)。
- 應(yīng)該支持使用DDD概念的業(yè)務(wù)領(lǐng)域模型的設(shè)計(jì)和實(shí)現(xiàn)。
- 應(yīng)該支持像依賴注入(DI)和面向方向編程(AOP)這些概念的開箱即用。(注:稍后將在文章中詳細(xì)解釋這些概念)。
- 與單元測(cè)試框架整合,比如 JUnit 、 TestNG 、 Unitils 等。
- 與其它Java/Java EE框架進(jìn)行良好的集成,比如JPA、Hibernate、TopLink等。
示例應(yīng)用
本文中使用的示例應(yīng)用是一個(gè)住房貸款處理系統(tǒng),業(yè)務(wù)用例是批準(zhǔn)住房貸款(抵押)的資金申請(qǐng)。將貸款申請(qǐng)?zhí)峤唤o抵押放貸公司的 時(shí)候,首先要通過承保過程,承保人在這一過程中根據(jù)客戶的收入詳情、信用歷史記錄和其它因素來決定批準(zhǔn)還是拒絕貸款請(qǐng)求。如果貸款申請(qǐng)獲得承保組的批準(zhǔn), 就進(jìn)入貸款審批程序的結(jié)清和融資步驟。
貸款處理系統(tǒng)中的融資模塊自動(dòng)給貸款人支付資金。通常,融資過程從抵押放貸公司(通常是銀行)將貸款包遞交給產(chǎn)權(quán)公司開始。接著產(chǎn)權(quán)公司評(píng)估貸款包,并與房產(chǎn)買賣雙方一起確定結(jié)清貸款的時(shí)間。貸款人和賣方與結(jié)算中介在產(chǎn)權(quán)公司會(huì)面、簽署書面協(xié)議,來轉(zhuǎn)移房產(chǎn)產(chǎn)權(quán)。
架構(gòu)
典型的企業(yè)應(yīng)用架構(gòu)由下面四個(gè)概念上的層組成:
- 用戶界面 (表現(xiàn)層):負(fù)責(zé)給用戶展示信息,并解釋用戶命令。
- 應(yīng)用層: 該層協(xié)調(diào)應(yīng)用程序的活動(dòng)。不包括任何業(yè)務(wù)邏輯,不保存業(yè)務(wù)對(duì)象的狀態(tài),但能保存應(yīng)用程序任務(wù)過程的狀態(tài)。
- 領(lǐng)域?qū)樱? 這一層包括業(yè)務(wù)領(lǐng)域的信息。業(yè)務(wù)對(duì)象的狀態(tài)在這里保存。業(yè)務(wù)對(duì)象的持久化和它們的狀態(tài)可能會(huì)委托給基礎(chǔ)設(shè)施層。
- 基礎(chǔ)設(shè)施層: 對(duì)其它層來說,這一層是一個(gè)支持性的庫。它提供層之間的信息傳遞,實(shí)現(xiàn)業(yè)務(wù)對(duì)象的持久化,包含對(duì)用戶界面層的支持性庫等。
讓我們更詳細(xì)地看一下應(yīng)用層和領(lǐng)域?qū)?。?yīng)用層:
- 負(fù)責(zé)應(yīng)用中UI屏幕之間的導(dǎo)航,以及與其它系統(tǒng)應(yīng)用層之間的交互。
- 還能對(duì)用戶輸入的數(shù)據(jù)進(jìn)行基本(非業(yè)務(wù)相關(guān))的驗(yàn)證,然后再把數(shù)據(jù)傳到應(yīng)用的其它層(更底層)。
- 不包含任何業(yè)務(wù)、領(lǐng)域相關(guān)的邏輯、或數(shù)據(jù)訪問邏輯。
- 沒有任何反映商業(yè)用例的狀態(tài),但卻能處理用戶會(huì)話或任務(wù)進(jìn)展的狀態(tài)。
領(lǐng)域?qū)樱?
- 負(fù)責(zé)業(yè)務(wù)領(lǐng)域的概念,業(yè)務(wù)用例和業(yè)務(wù)規(guī)則的相關(guān)信息。領(lǐng)域?qū)ο蠓庋b了業(yè)務(wù)實(shí)體的狀態(tài)和行為。貸款處理應(yīng)用中的業(yè)務(wù)實(shí)體例子有抵押(Mortgage)、房產(chǎn)(Property)和貸款人(Borrower)。
- 如果用例跨越多個(gè)用戶請(qǐng)求(比如貸款登記過程包含多個(gè)步驟:用戶輸入貸款詳細(xì)信息,系統(tǒng)基于貸款特性返回產(chǎn)品和利率,用戶選擇特定的產(chǎn)品/利率組合,最后系統(tǒng)會(huì)用這個(gè)利率鎖定貸款),還可以管理業(yè)務(wù)用例的狀態(tài)(會(huì)話)。
- 包含服務(wù)對(duì)象,這些服務(wù)對(duì)象只包含一個(gè)定義好的、不屬于任何領(lǐng)域?qū)ο蟮目刹僮餍袨?。服?wù)封裝了業(yè)務(wù)領(lǐng)域的狀態(tài),而業(yè)務(wù)領(lǐng)域并不適用于領(lǐng)域?qū)ο蟊旧怼?
- 是商業(yè)應(yīng)用的核心,應(yīng)該與應(yīng)用的其它層隔離開來。而且,它不應(yīng)該依賴于其它層使用的應(yīng)用框架(JSP/JSF、 Struts 、EJB、 Hibernate 、 XMLBeans 等)。
下面的圖2顯示了應(yīng)用中使用的不同架構(gòu)層次,以及它們與DDD有怎樣的關(guān)系。
圖2. 多層應(yīng)用架構(gòu)圖 (點(diǎn)擊查看大圖)
下面的設(shè)計(jì)觀點(diǎn)被認(rèn)為是目前DDD實(shí)現(xiàn)訣竅的主要部分:
OOP是領(lǐng)域?qū)崿F(xiàn)中最重要的基本原則。應(yīng)該利用像繼承、封裝和多態(tài)這樣的OOP概念,使用Plain Java類和接口來設(shè)計(jì)領(lǐng)域?qū)ο?。大部分領(lǐng)域元素是既有狀態(tài)(屬性)又有行為(操作狀態(tài)的方法或操作)的真正對(duì)象。它們同時(shí)對(duì)應(yīng)于真實(shí)世界的概念,能很合 適地適用于OOP概念。DDD中的實(shí)體和值對(duì)象都是OOP概念的典型例子,因?yàn)樗鼈兺瑫r(shí)有狀態(tài)和行為。
在典型的工作單元(UOW)中,領(lǐng)域?qū)ο笮枰c其它的對(duì)象協(xié)作,無論這些對(duì)象是服務(wù)、資源庫、還是工廠。領(lǐng)域?qū)ο筮€需要處理其它那些本身就橫切的關(guān) 注點(diǎn), 比如領(lǐng)域狀態(tài)變化跟蹤、審計(jì)、緩存、事務(wù)管理(包括事務(wù)重試)。這些都是可重用、非領(lǐng)域相關(guān)的關(guān)注點(diǎn),通常很容易在包括領(lǐng)域?qū)拥恼麄€(gè)代碼中散布和重復(fù)。在 領(lǐng)域?qū)ο笾星度朐撨壿嫊?huì)導(dǎo)致領(lǐng)域?qū)雍头穷I(lǐng)域相關(guān)的代碼互相糾纏、產(chǎn)生混亂。
說到處理對(duì)象間之沒有緊耦合的代碼依賴關(guān)系和隔離橫切關(guān)注點(diǎn)的時(shí)候,OOP并不能獨(dú)自為領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)和開發(fā)提供極好的設(shè)計(jì)解決方案。在這是可以利用DI和AOP這樣的設(shè)計(jì)概念對(duì)OOP進(jìn)行補(bǔ)充,以盡量減少緊耦合、提高模塊化、更好地處理橫切關(guān)注點(diǎn)。
依賴注入
DI能很有效地將配置和依賴代碼從領(lǐng)域?qū)ο笾幸瞥?。此外,領(lǐng)域類對(duì)數(shù)據(jù)訪問對(duì)象(DAO)類、服務(wù)類對(duì)領(lǐng)域類的設(shè)計(jì)依賴性使得DI成為DDD實(shí)現(xiàn)中“必須有”的內(nèi)容。通過將資源庫和服務(wù)之類的其它對(duì)象注入到領(lǐng)域?qū)ο?,DI有助于創(chuàng)建一個(gè)更清晰、松耦合的設(shè)計(jì)。
在示例應(yīng)用中,服務(wù)對(duì)象(FundingServiceImpl)利用DI注入實(shí)體對(duì)象(Loan、Borrower和FundingRequest)。實(shí)體也通過DI引用資源庫。同樣的,像 數(shù)據(jù)源 、Hibernate 會(huì)話工廠 和 事務(wù)管理器 這些其它的Java EE資源也被注入到服務(wù)和資源庫對(duì)象中。
面向方面編程
通過從領(lǐng)域?qū)ο笾幸瞥龣M切關(guān)注點(diǎn)代碼,比如檢查、領(lǐng)域狀態(tài)變化跟蹤等,AOP有助于實(shí)現(xiàn)一個(gè)更好的設(shè)計(jì)(即在領(lǐng)域模型中少一些亂七八糟的內(nèi)容)。可 利用 AOP把協(xié)同對(duì)象和服務(wù)注入領(lǐng)域?qū)ο?,特別是那些容器沒有實(shí)例化的對(duì)象(比如持久化對(duì)象)。在可以利用AOP的領(lǐng)域?qū)又校渌姆矫嬗芯彺?、事?wù)管理和基 于角色的安全(授權(quán))。
貸款處理應(yīng)用利用自定義方面將數(shù)據(jù)緩存引入服務(wù)對(duì)象。貸款產(chǎn)品和利率信息從數(shù)據(jù)庫表中加載一次(客戶端第一次請(qǐng)求這些信息時(shí)),然后存儲(chǔ)到適用于后面產(chǎn)品和利率查找的對(duì)象緩存( JBossCache )中。產(chǎn)品和利率會(huì)被頻繁訪問,但不會(huì)定期更新,所以緩存數(shù)據(jù)是一個(gè)很好的候選方案,而不是每次都從后端的數(shù)據(jù)庫獲取。
在近期的討論貼子里,DDD中DI和AOP概念的作用是主要的話題。討論以Ramnivas Laddad的演講為基礎(chǔ),Ramnivas在其演講中主張, 沒有AOP和DI的幫助,DDD無法實(shí)現(xiàn) 。 Ramnivas在這個(gè)演講中討論了“細(xì)粒度DI”的概念,這一概念利用AOP使領(lǐng)域?qū)ο蠡謴?fù)機(jī)敏性。他說領(lǐng)域?qū)ο笮枰L問其它細(xì)粒度的對(duì)象來提供豐富的 行為,該問題的解決方案是在領(lǐng)域?qū)ο笾凶⑷敕?wù)、工廠或資源庫(通過在調(diào)用構(gòu)造或setter方法時(shí)期使用方面來注入依賴)。
Chris Richardson也討論了有關(guān) 利用DI、對(duì)象和方面 ,通過減少耦合、提高模塊化來改進(jìn)應(yīng)用設(shè)計(jì)。Chris談到了“超級(jí)大服務(wù)”反模式,這是應(yīng)用代碼耦合、混亂、分散的結(jié)果,他還談了如何利用DI和AOP的概念來避免這一反模式。
注解
最近定義、處理方面和DI的趨勢(shì)是使用注解。對(duì)實(shí)現(xiàn)遠(yuǎn)程服務(wù)(比如EJB或Web Services)來說,注解有助于減少所需的工件。它們還簡(jiǎn)化了配置管理任務(wù)。 Spring 2.5 、 Hibernate 3 ,以及其它框架都充分利用注解在Java企業(yè)應(yīng)用的不同層中配置組件。
我們應(yīng)該利用注解生成模板代碼,模板代碼能在靈活性上增加價(jià)值。但同時(shí)應(yīng)該謹(jǐn)慎使用注解。注解應(yīng)該用于不會(huì)引起混淆或誤解實(shí)際代碼的地方。使用注解 的一個(gè) 很好的例子是Hibernate ORM映射,注解能直接用類或?qū)傩悦o指定的SQL表或列名添加值。另一方面,像JDBC驅(qū)動(dòng)配置(驅(qū)動(dòng)類名、JDBC URL、用戶名和密碼)這樣的詳細(xì)信息則更適合于存放在XML文件中,而不是使用注解。這基于數(shù)據(jù)庫在同一個(gè)上下文中這一假設(shè)。如果領(lǐng)域模型和數(shù)據(jù)庫表之 間需要相當(dāng)多的轉(zhuǎn)換,那就應(yīng)該好好思考一下設(shè)計(jì)了。
Java EE 5 提供JPA注解,比如 @Entity 、 @PersistenceUnit 、 @PersistenceContext 等,以此給簡(jiǎn)單的Java類添加持久化細(xì)節(jié)。在領(lǐng)域建模上下文中,實(shí)體、資源庫和服務(wù)都是使用注解的好地方。
@Configurable 是 Spring將資源庫和服務(wù)注入領(lǐng)域?qū)ο蟮姆绞?。Spring框架在@Configurable注解之上擴(kuò)展了“領(lǐng)域?qū)ο笠蕾囎⑷搿彼枷搿?Ramnivas最近在博客中談?wù)摿思磳l(fā)布的Spring 2.5.2版本(從項(xiàng)目的Snapshot Build 379開始可用)的 最新改進(jìn) 。 有三個(gè)新的方面(AnnotationBeanConfigurerAspect、 AbstractInterfaceDrivenDependencyInjectionAspect和 AbstractDependencyInjectionAspect)為領(lǐng)域?qū)ο笠蕾囎⑷胩峁┝撕?jiǎn)單、更靈活的選擇。Ramnivas說,引入中間的方 面(AbstractInterfaceDrivenDependencyInjectionAspect),其主要原因是要讓領(lǐng)域特定的注解和接口發(fā)揮 作用。Spring還提供了其它注解來幫助設(shè)計(jì)領(lǐng)域?qū)ο?,比? @Repository 、 @Service 和 @Transactional 。
示例應(yīng)用中使用了部分注解。實(shí)體對(duì)象(Loan、Borrower和FundingRequest)使用了@Entity注解;這些對(duì)象還使用@Configurable注解綁定資源庫對(duì)象;服務(wù)類也使用@Transactional注解來用事務(wù)行為裝飾服務(wù)方法。
領(lǐng)域模型和安全
領(lǐng)域?qū)拥膽?yīng)用安全確保只有授權(quán)的客戶端(人類用戶或其它應(yīng)用)能調(diào)用領(lǐng)域操作,訪問領(lǐng)域狀態(tài)。
Spring安全 (Spring Portfolio的一個(gè)子項(xiàng)目)同時(shí)為應(yīng)用的表現(xiàn)層(以URL為基礎(chǔ))和領(lǐng)域?qū)樱ǚ椒?jí))提供了細(xì)粒度的訪問控制。該框架使用Spring的Bean Proxy來攔截方法調(diào)用,運(yùn)用安全約束。它為使用 MethodSecurityInterceptor 類的Java對(duì)象提供了基于角色的聲明式安全。它也有針對(duì)領(lǐng)域?qū)ο蟮脑L問控制列表(ACL's)形式的實(shí)例級(jí)別安全,以控制實(shí)例級(jí)別的用戶訪問。
在領(lǐng)域模型中使用Spring安全來處理授權(quán)需求的主要好處是,框架有一個(gè)非侵入式的架構(gòu),我們可以完全隔離領(lǐng)域和安全方面。此外,業(yè)務(wù)對(duì)象也不會(huì)和安全實(shí)現(xiàn)細(xì)節(jié)混成一團(tuán)。我們可以只在一個(gè)地方編寫通用的安全規(guī)則,(使用AOP技術(shù))在任何需要實(shí)現(xiàn)它們的地方運(yùn)用它們。
在領(lǐng)域和服務(wù)類中,授權(quán)在類方法調(diào)用級(jí)別進(jìn)行處理。舉例來說,對(duì)于高達(dá)一百萬美元的貸款,承保領(lǐng)域?qū)ο笾械摹百J款審批”方法可以由任何具有“承保人 ”角色 的用戶調(diào)用;而對(duì)于超過一百萬美元的貸款申請(qǐng)來說,同一領(lǐng)域?qū)ο笾械膶徟椒▌t只能由具有“核保主管”角色的用戶調(diào)用。
下表簡(jiǎn)要說明了應(yīng)用架構(gòu)每一層中應(yīng)用的各種安全關(guān)注點(diǎn)。
表1. 各個(gè)應(yīng)用層中的安全關(guān)注點(diǎn)
層 安全關(guān)注點(diǎn)客戶端/控制器 | 認(rèn)證、Web頁面(URL)界別授權(quán) |
外觀 | 基于角色的授權(quán) |
領(lǐng)域 | 領(lǐng)域?qū)嵗?jí)別授權(quán)、ACL |
數(shù)據(jù)庫 | DB對(duì)象級(jí)別授權(quán)(存儲(chǔ)過程、存儲(chǔ)函數(shù)、觸發(fā)器) |
業(yè)務(wù)規(guī)則
業(yè)務(wù)規(guī)則是業(yè)務(wù)領(lǐng)域中的重要部分。它們定義了數(shù)據(jù)驗(yàn)證和其它的約束規(guī)則,這些規(guī)則需要應(yīng)用于特定業(yè)務(wù)流程場(chǎng)景中的領(lǐng)域?qū)ο蟆I(yè)務(wù)規(guī)則通常分為下面幾類:
- 數(shù)據(jù)驗(yàn)證
- 數(shù)據(jù)轉(zhuǎn)換
- 商業(yè)決策
- 流程流向(工作流邏輯)
上下文在DDD世界中非常重要。上下文的特性決定了領(lǐng)域?qū)ο髤f(xié)作及其它運(yùn)行時(shí)因素,比如運(yùn)用什么業(yè)務(wù)規(guī)則等。驗(yàn)證以及其它業(yè)務(wù)規(guī)則往往都是在一個(gè)特 定的業(yè) 務(wù)上下文中處理的。這意味著,相同的領(lǐng)域?qū)ο笤诓煌臉I(yè)務(wù)上下文中將不得不處理不同的一組業(yè)務(wù)規(guī)則。比如說,通過了貸款審批流程中的承保步驟后,貸款領(lǐng)域 對(duì)象的一些屬性(像貸款數(shù)額和利率)就不能再改變了。但在貸款剛剛登記并與特定利率關(guān)聯(lián)的時(shí)候,同樣的屬性是可以改變的。
盡管所有的領(lǐng)域特 定業(yè)務(wù)規(guī)則都應(yīng)該封裝在領(lǐng)域?qū)?,但一些?yīng)用設(shè)計(jì)將規(guī)則放在了外觀類中,這導(dǎo)致了領(lǐng)域類在業(yè)務(wù)規(guī)則邏輯方面變成了“貧血的”。在小型應(yīng)用中這可能是可接受的 解決方案,但不推薦將其用于包含復(fù)雜業(yè)務(wù)規(guī)則的中大型企業(yè)應(yīng)用。更好的設(shè)計(jì)方案是把規(guī)則放在它們應(yīng)該在的地方——領(lǐng)域?qū)ο笾小H绻粋€(gè)業(yè)務(wù)規(guī)則 跨越兩個(gè)或兩個(gè)以上的實(shí)體對(duì)象,那么該規(guī)則應(yīng)該做為服務(wù)類的一部分。
此外,如果我們不在應(yīng)用中下苦功,往往把業(yè)務(wù)規(guī)則變成代碼里的一串switch語句。隨著規(guī)則變得越來越復(fù)雜,開發(fā)人員不會(huì)愿意花費(fèi)時(shí)間去重構(gòu)代 碼,將 switch語句移到更易于管理的設(shè)計(jì)中。在類中硬編碼復(fù)雜的流向或決策規(guī)則邏輯會(huì)導(dǎo)致類中出現(xiàn)更長(zhǎng)的方法、代碼重復(fù)、最終僵化的應(yīng)用設(shè)計(jì),長(zhǎng)遠(yuǎn)來看,這 將成為維護(hù)的噩夢(mèng)。一個(gè)良好的設(shè)計(jì)是把所有的規(guī)則(特別是隨著業(yè)務(wù)策略的變化而頻繁改變的復(fù)雜規(guī)則)放到規(guī)則引擎(利用規(guī)則框架,比如 JBoss Rules 、 OpenRules 或 Mandarax )中去,并從領(lǐng)域類中進(jìn)行調(diào)用。
驗(yàn)證規(guī)則通常會(huì)用不同的語言實(shí)現(xiàn),比如Javascript、XML、Java代碼,還有其它腳本語言。但由于業(yè)務(wù)規(guī)則的動(dòng)態(tài)特性, Ruby 、 Groovy 、 領(lǐng)域特定語言 (DSL) 這些腳本語言是定義、管理這些規(guī)則更好的選擇。Struts(應(yīng)用層)、Spring(服務(wù)層)和Hibernate(ORM)都有其自己的驗(yàn)證模塊,我 們可以在這些驗(yàn)證模塊中對(duì)傳入或傳出的數(shù)據(jù)對(duì)象運(yùn)用驗(yàn)證規(guī)則。在一些情況下,驗(yàn)證規(guī)則還能被處理為方面,它們可以組合到應(yīng)用的不同層次中去(比如服務(wù)和控 制器)。
在編寫領(lǐng)域類處理業(yè)務(wù)規(guī)則時(shí),緊記單元測(cè)試方面是非常重要的。規(guī)則邏輯中的任何變化都應(yīng)該很容易、獨(dú)立地單元可測(cè)。
示例應(yīng)用包括一個(gè)業(yè)務(wù)規(guī)則集來驗(yàn)證貸款特性是否都在允許的產(chǎn)品和利率規(guī)格內(nèi)。規(guī)則在腳本語言中(Groovy)進(jìn)行定義,并用于傳遞給FundingService對(duì)象的貸款數(shù)據(jù)。
設(shè)計(jì)
從設(shè)計(jì)的角度出發(fā),領(lǐng)域?qū)討?yīng)該有一個(gè)定義清晰的邊界,以避免來自非核心領(lǐng)域?qū)雨P(guān)注點(diǎn)的層的損壞,比如特定供應(yīng)商的說明、數(shù)據(jù)過濾、轉(zhuǎn)換等。領(lǐng)域元素 應(yīng)該設(shè) 計(jì)為正確地保存領(lǐng)域狀態(tài)和行為。不同的領(lǐng)域元素會(huì)基于狀態(tài)和行為進(jìn)行不同的結(jié)構(gòu)化。下面的表2展示了領(lǐng)域元素及其包含的內(nèi)容。
表2. 領(lǐng)域元素及其狀態(tài)和行為
領(lǐng)域元素 狀態(tài)/行為實(shí)體、值對(duì)象、聚合 | 狀態(tài)和行為都有 |
數(shù)據(jù)傳輸對(duì)象 | 只有狀態(tài) |
服務(wù)、資源庫 | 只有行為 |
同時(shí)包含狀態(tài)(數(shù)據(jù))和行為(操作)的實(shí)體、值對(duì)象、聚合應(yīng)該有定義清晰的狀態(tài)和行為。同時(shí),該行為不應(yīng)該超出對(duì)象邊界的范圍。實(shí)體應(yīng)該在作用于本地狀態(tài)的用例中完成大部分工作。但它們不應(yīng)該知道太多無關(guān)的概念。
對(duì)那些封裝領(lǐng)域?qū)ο鬆顟B(tài)所需要的屬性來說,好的設(shè)計(jì)實(shí)踐是只包括這些屬性的getter/setter方法。設(shè)計(jì)領(lǐng)域?qū)ο髸r(shí),只為那些能改變的屬性提供setter方法。此外,公有的構(gòu)造函數(shù)應(yīng)該只含有必需的屬性,而不是包含領(lǐng)域類中所有的屬性。
在大部分用例中,我們并不是真的要去直接改變對(duì)象的狀態(tài)。所以,代替改變內(nèi)部狀態(tài)的做法是,創(chuàng)建一個(gè)帶有已改變狀態(tài)的新對(duì)象并返回該新對(duì)象。這種方法在這些用例中就足夠了,還能降低設(shè)計(jì)的復(fù)雜性。
聚合類對(duì)調(diào)用者隱藏了協(xié)作類的用法。聚合類可用來封裝領(lǐng)域類中復(fù)雜的、有侵入性的、狀態(tài)依賴的需求。
支持DDD的設(shè)計(jì)模式
有幾種有助于領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)和開發(fā)的設(shè)計(jì)模式。下面是這些設(shè)計(jì)模式的列表:
- 領(lǐng)域?qū)ο螅―O)
- 數(shù)據(jù)傳輸對(duì)象(DTO)
- DTO組裝器
- 資源庫:資源庫包含領(lǐng)域?yàn)橹行牡姆椒?,并使用DAO與數(shù)據(jù)庫交互。
- 泛型DAO
- 時(shí)態(tài)模式( Temporal Patterns ):這些模式給豐富的領(lǐng)域模型添加了時(shí)間維。 Bitemporal框架 基于Martin Fowler的 時(shí)態(tài)模式 ,為處理領(lǐng)域模型中的雙時(shí)態(tài)問題提供了設(shè)計(jì)方法。核心的領(lǐng)域?qū)ο蠹捌潆p時(shí)態(tài)屬性能用ORM產(chǎn)品持久化,比如Hibernate。
在DDD中應(yīng)用的其它設(shè)計(jì)模式還包括策略模式、外觀模式和工廠模式。Jimmy Nilsson在他的 書 里討論了工廠模式,認(rèn)為它是一種領(lǐng)域模式。
DDD反模式
在最佳實(shí)踐和設(shè)計(jì)模式的反面,架構(gòu)師和開發(fā)人員在實(shí)現(xiàn)領(lǐng)域模型時(shí)還應(yīng)該提防一些DDD的壞氣味。由于這些反模式,領(lǐng)域?qū)釉趹?yīng)用架構(gòu)中成為最不重要的部分,外觀類反而在模型中承擔(dān)了更重要的責(zé)任。下面是一些反模式:
- 貧血的領(lǐng)域?qū)ο?
- 重復(fù)的DAO
- 肥服務(wù)層:服務(wù)類在這里最終會(huì)包含所有的業(yè)務(wù)邏輯。
- 依戀情結(jié)( Feature Envy ):這是Martin Fowler在他關(guān)于重構(gòu)的 書 中提到的典型的壞氣味,在該反模式中,一個(gè)類的方法對(duì)屬于其它類的數(shù)據(jù)太過念念不忘。
數(shù)據(jù)訪問對(duì)象
DAO和資源庫在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)中都很重要。DAO是關(guān)系型數(shù)據(jù)庫和應(yīng)用之間的契約。它封裝了Web應(yīng)用中的數(shù)據(jù)庫CRUD操作細(xì)節(jié)。另一方面,資源庫是一個(gè)獨(dú)立的抽象,它與DAO進(jìn)行交互,并提供到領(lǐng)域模型的“業(yè)務(wù)接口”。
資源庫使用領(lǐng)域的通用語言,處理所有必要的DAO,并使用領(lǐng)域理解的語言提供對(duì)領(lǐng)域模型的數(shù)據(jù)訪問服務(wù)。
DAO方法是細(xì)粒度的,更接近數(shù)據(jù)庫,而資源庫方法的粒度粗一些,而且更接近領(lǐng)域。此外,一個(gè)資源庫類中能注入多個(gè)DAO。資源庫和DAO能防止解耦的領(lǐng)域模型去處理數(shù)據(jù)訪問和持久化細(xì)節(jié)。
領(lǐng)域?qū)ο髴?yīng)該只依賴于資源庫接口。這就是為什么是注入資源庫、而不是DAO會(huì)產(chǎn)生一個(gè)更規(guī)則的領(lǐng)域模型的原因。DAO類不能由客戶端(服務(wù)和其它的消費(fèi)者類)直接調(diào)用??蛻舳藨?yīng)該始終調(diào)用領(lǐng)域?qū)ο?,領(lǐng)域?qū)ο笤僬{(diào)用DAO將數(shù)據(jù)持久化到數(shù)據(jù)存儲(chǔ)中。
處理領(lǐng)域?qū)ο笾g的依賴關(guān)系(比如實(shí)體及其資源庫之間的依賴關(guān)系)是開發(fā)人員經(jīng)常遇到的典型問題。解決這個(gè)問題通常的設(shè)計(jì)方案是讓服務(wù)類或外觀類直 接調(diào)用 資源庫,在調(diào)用資源庫的時(shí)候返回實(shí)體對(duì)象給客戶端。該設(shè)計(jì)最終導(dǎo)致前面提到的貧血領(lǐng)域模型,其中外觀類會(huì)開始堆積更多的業(yè)務(wù)邏輯,而領(lǐng)域?qū)ο髣t成為單純的 數(shù)據(jù)載體。好的設(shè)計(jì)是利用DI和AOP技術(shù)將資源庫和服務(wù)注入到領(lǐng)域?qū)ο笾腥ァ?
示例應(yīng)用在實(shí)現(xiàn)貸款處理領(lǐng)域模型時(shí)遵循了這些設(shè)計(jì)原則。
持久化
持久化是一個(gè)基礎(chǔ)設(shè)施方面,領(lǐng)域?qū)討?yīng)該與其解耦。JPA通過對(duì)類隱藏持久化實(shí)現(xiàn)的細(xì)節(jié),提供了這一抽象。它由注解推動(dòng),所以不需要XML映射文件。但同時(shí),表名和列名嵌在代碼中,在某些情況下可能并不是一個(gè)靈活的解決辦法。
使用提供數(shù)據(jù)網(wǎng)格解決方案的網(wǎng)格計(jì)算產(chǎn)品,比如Oracle的 Coherence 、WebSphere的Object Grid、 GigaSpaces ,開發(fā)人員在建模和設(shè)計(jì)業(yè)務(wù)領(lǐng)域時(shí),完全不需要考慮RDBMS。數(shù)據(jù)庫層用內(nèi)存對(duì)象/數(shù)據(jù)網(wǎng)格的形式從領(lǐng)域?qū)映橄蟪鰜怼?
緩存
在我們討論領(lǐng)域?qū)拥臓顟B(tài)(數(shù)據(jù))時(shí),我們不得不談到緩存問題。經(jīng)常訪問的領(lǐng)域數(shù)據(jù)(比如抵押貸款處理應(yīng)用中的產(chǎn)品和利率)很值得緩存起來。緩存能提高性能,減少數(shù)據(jù)庫服務(wù)器的負(fù)載。服務(wù)層很適合緩存領(lǐng)域狀態(tài)。 TopLink 和 Hibernate 這些ORM框架也提供數(shù)據(jù)緩存。
貸款處理示例應(yīng)用使用JBossCache框架來緩存產(chǎn)品和利率詳情,以減少數(shù)據(jù)庫調(diào)用、提高應(yīng)用性能。
事務(wù)管理
對(duì)保持?jǐn)?shù)據(jù)完整性、整體提交或回滾UOW(工作單元模式)來說,事務(wù)管理是很重要的。應(yīng)該在應(yīng)用架構(gòu)層的哪里處理事務(wù)一直存在爭(zhēng)議。交叉實(shí)體的事務(wù)(在同一UOW中跨越多個(gè)領(lǐng)域?qū)ο螅┮灿绊懺谀睦锾幚硎聞?wù)這一設(shè)計(jì)決策。
一些開發(fā)人員傾向于在DAO類中管理事務(wù),這是一個(gè)欠佳的設(shè)計(jì)。該設(shè)計(jì)導(dǎo)致過細(xì)粒度的事務(wù)控制,對(duì)那些事務(wù)跨越多個(gè)領(lǐng)域?qū)ο蟮挠美齺碚f,這種事務(wù)控 制沒有 靈活性。服務(wù)類應(yīng)該處理事務(wù);即使事務(wù)跨越多個(gè)領(lǐng)域?qū)ο螅?wù)類也能處理事務(wù),因?yàn)樵诖蠖鄶?shù)用例中,是服務(wù)類在處理控制流。
示例應(yīng)用中的FundingServiceImpl類處理資金申請(qǐng)的事務(wù),通過調(diào)用資源庫執(zhí)行多個(gè)數(shù)據(jù)庫操作,并在單一事務(wù)中提交或回滾所有的數(shù)據(jù)庫變化。
數(shù)據(jù)傳輸對(duì)象
領(lǐng)域?qū)ο竽P驮诮Y(jié)構(gòu)上與從業(yè)務(wù)服務(wù)接收或發(fā)送的消息不兼容,在這樣一種SOA環(huán)境中,DTO就是設(shè)計(jì)中很重要的一部分。消息通常都在XML模式定義 文檔 (XSD)中定義和維護(hù),從XSD編寫(或代碼生成)DTO對(duì)象,并在領(lǐng)域和SOA服務(wù)層之間使用它們來傳輸數(shù)據(jù)(消息)是一種普遍的做法。在分布式應(yīng)用 中,將來自于一個(gè)或多個(gè)領(lǐng)域?qū)ο笾械臄?shù)據(jù)映射到DTO中會(huì)成為必然的弊端,因?yàn)閺男阅芎桶踩嵌瘸霭l(fā),跨越網(wǎng)絡(luò)發(fā)送領(lǐng)域?qū)ο笫遣粚?shí)際的。
從DDD的角度來看,DTO還有利于維護(hù)服務(wù)層和UI層之間的縫隙,其中DO用于領(lǐng)域?qū)雍头?wù)層,DTO用于表現(xiàn)層。
Dozer 框架用于將一或多個(gè)領(lǐng)域?qū)ο蠼M裝為一個(gè)DTO對(duì)象。它是雙向的,將領(lǐng)域?qū)ο筠D(zhuǎn)換為DTO的時(shí)候,它會(huì)保存大量備用的代碼和時(shí)限,反之亦然。DO和DTO之間的雙向映射有利于消除“DO到DTO”和“DTO到DO”各自的轉(zhuǎn)換邏輯。該框架還能正確處理類型和數(shù)組的轉(zhuǎn)換。
示例應(yīng)用在資金處理申請(qǐng)到來時(shí),利用Dozer映射文件(XML)將FundingRequestDTO對(duì)象劃分成為L(zhǎng)oan、Borrower、 FundingRequest實(shí)體對(duì)象。在返回給客戶端時(shí),映射同樣負(fù)責(zé)將來自實(shí)體的資金響應(yīng)數(shù)據(jù)聚合到單一的DTO對(duì)象中。
DDD實(shí)現(xiàn)框架
像Spring、Real Object Oriented( ROO )、Hibernate和Dozer這些框架都有助于設(shè)計(jì)并實(shí)現(xiàn)領(lǐng)域模型。支持DDD實(shí)現(xiàn)的其它框架有 Naked Objects 、 Ruby On Rails 、 Grails ,以及Spring Modules XT Framework 。
Spring負(fù)責(zé)實(shí)例化,并將服務(wù)、工廠和資源庫這些領(lǐng)域類聯(lián)接在一起。它還使用@Configurable注解將服務(wù)注入實(shí)體。該注解是Spring特有的,所以完成這一注入的其它選擇是使用諸如Hibernate攔截器的東西。
ROO是建立在觀點(diǎn)“領(lǐng)域第一,基礎(chǔ)設(shè)施第二”之上的DDD實(shí)現(xiàn)框架。開發(fā)該框架是為了減少Web應(yīng)用開發(fā)中模式的模板編碼。利用ROO時(shí),我們定義領(lǐng)域模型,接著框架(基于 Maven ? Archetypes )為模型-視圖-控制器(MVC)、DTO、業(yè)務(wù)層外觀和DAO層生成代碼。它也能為單元測(cè)試和集成測(cè)試生成stubs。?
ROO有幾個(gè)非常實(shí)用的實(shí)現(xiàn)模式。比如說,它區(qū)分處理屬性的狀態(tài)、使用屬性級(jí)訪問的持久層、只反映必需屬性的公有構(gòu)造函數(shù)。
開發(fā)
沒有實(shí)際的實(shí)現(xiàn),模型就沒有用處。實(shí)現(xiàn)階段應(yīng)該盡可能多地自動(dòng)化完成開發(fā)任務(wù)。為了看看什么任務(wù)能自動(dòng)完成,讓我們看看涉及領(lǐng)域模型的一個(gè)典型用例。下面是用例的步驟列表:
輸入請(qǐng)求:
- 客戶端調(diào)用外觀類,以XML文檔(XSD兼容的)的方式發(fā)送數(shù)據(jù);外觀類為UOW初始化一個(gè)新的事務(wù)。
- 驗(yàn)證輸入的數(shù)據(jù)。驗(yàn)證包括基本驗(yàn)證(基本的/數(shù)據(jù)類型/屬性級(jí)檢查)和業(yè)務(wù)驗(yàn)證。如果有任何的驗(yàn)證錯(cuò)誤,拋出適當(dāng)?shù)漠惓!?
- 將描述轉(zhuǎn)換為代碼(以成為簡(jiǎn)單的領(lǐng)域)。
- 改變數(shù)據(jù)格式,以成為簡(jiǎn)單的領(lǐng)域模型。
- 進(jìn)行所有的屬性分割(比如,在客戶實(shí)體對(duì)象中,將客戶姓名分成名字和姓)。
- 把DTO拆分為一或多個(gè)領(lǐng)域?qū)ο蟆?
- 持久化領(lǐng)域?qū)ο蟮臓顟B(tài)。
輸出響應(yīng):
- 從數(shù)據(jù)存儲(chǔ)中獲取領(lǐng)域?qū)ο蟮臓顟B(tài)。
- 如果必要,緩存狀態(tài)。
- 將領(lǐng)域?qū)ο蠼M裝為對(duì)應(yīng)用有利的數(shù)據(jù)對(duì)象(DTO)。
- 進(jìn)行所有的數(shù)據(jù)元素合并或分離(比如結(jié)合名字和姓,組成單一的客戶姓名屬性)。
- 將代碼轉(zhuǎn)換為描述。
- 必要時(shí)改變數(shù)據(jù)格式,以處理客戶端數(shù)據(jù)使用的要求。
- 如果有必要,緩存DTO的狀態(tài)。
- 事務(wù)提交(如果有錯(cuò)誤則回滾),退出控制流。
下表顯示了應(yīng)用中不同的對(duì)象,這些對(duì)象將一個(gè)層的數(shù)據(jù)傳到另一個(gè)層。
表3. 應(yīng)用層間的數(shù)據(jù)流向
層 起點(diǎn)對(duì)象 終點(diǎn)對(duì)象 框架DAO | 數(shù)據(jù)庫表 | DO | Hibernate |
領(lǐng)域委托 ?? ?? | DO | DTO | Dozer |
數(shù)據(jù)傳輸 | DTO | XML | JAXB |
正如你所看到的,相同的數(shù)據(jù)以不同形式(DO、DTO、XML等)在應(yīng)用架構(gòu)中傳遞的層并不多。大部分持有數(shù)據(jù)的這些對(duì)象(Java或XML),還 有像 DAO、DAOImpl、DAOTest這些類實(shí)際上都是基礎(chǔ)設(shè)施。這些有模板代碼和結(jié)構(gòu)的類、XML文件都很適合代碼生成。
代碼生成
ROO這樣的框架還為新項(xiàng)目創(chuàng)建了一個(gè)標(biāo)準(zhǔn)、一致的項(xiàng)目模板(使用Maven插件)。使用預(yù)先生成的項(xiàng)目模板,我們可以實(shí)現(xiàn)目錄結(jié)構(gòu)的一致性,其中存放源碼、測(cè)試類、配置文件,以及對(duì)內(nèi)部和外部(第三方)組件庫的依賴關(guān)系。
典型的企業(yè)軟件應(yīng)用所需的種種類和配置文件時(shí),其數(shù)量之多令人望而生畏。代碼生成是解決該問題的最好辦法。代碼生成工具通常使用某類模板框架來定義模板,或是代碼生成器能從中生成代碼的映射。 Eclipse建模框架 (EMF)的幾個(gè)子項(xiàng)目有助于Web應(yīng)用項(xiàng)目需要的各種工件的代碼生成。模型驅(qū)動(dòng)架構(gòu)(MDA)工具,比如AndroMDA,都利用EMF在架構(gòu)模型的基礎(chǔ)上生成代碼。
說到在領(lǐng)域?qū)泳帉懳蓄悾铱吹介_發(fā)人員手動(dòng)編寫這些類(大多是從無到有地寫完第一個(gè),接著用“復(fù)制并粘貼”的模式來為其它的領(lǐng)域?qū)ο髣?chuàng)建所需的委 托 類)。由于這些類大部分都是領(lǐng)域類的外觀,它們很適合代碼生成。代碼生成是長(zhǎng)遠(yuǎn)的解決辦法,盡管建立并測(cè)試代碼生成器(引擎)增加了初期的投入(代碼量和 時(shí)間)。
對(duì)生成的測(cè)試類來說,一個(gè)好的選擇就是在需要進(jìn)行單元測(cè)試的主類中,為帶有復(fù)雜業(yè)務(wù)邏輯的方法創(chuàng)建抽象方法。這樣,開發(fā)人員能繼承生成的測(cè)試基類,然后實(shí)現(xiàn)不能自動(dòng)生成的自定義業(yè)務(wù)邏輯。同樣,這個(gè)方法也適用于任何有不能自動(dòng)創(chuàng)建測(cè)試邏輯的測(cè)試方法。
對(duì)編寫代碼生成器來說,腳本語言是一個(gè)更好的選擇,因?yàn)樗鼈冮_銷少,還支持模板創(chuàng)建和自定義選項(xiàng)。如果我們?cè)贒DD項(xiàng)目中充分利用代碼生成,我們只需要從無到有地編寫少量的代碼。必須從無到有進(jìn)行創(chuàng)建的工件有:
- XSD
- 領(lǐng)域?qū)ο?
- 服務(wù)
一旦我們定義了XSD和Java類,我們可以生成下列全部或大部分的類和配置文件:
- DAO接口和實(shí)現(xiàn)類
- 工廠
- 資源庫
- 領(lǐng)域代理(如果有必要)
- 外觀(包括EJB和WebService類)
- DTO
- 上述類的單元測(cè)試(包括測(cè)試類和測(cè)試數(shù)據(jù))
- Spring配置文件
表4列出了Web應(yīng)用架構(gòu)中不同的層,以及那些層中能生成什么工件(Java類或XML文件)。
表4. DDD實(shí)現(xiàn)項(xiàng)目中的代碼生成
層/功能 模式 你寫的代碼 生成的代碼 框架數(shù)據(jù)訪問 | DAO/ 資源庫 | ?? |
DAO接口,
DAO實(shí)現(xiàn)類, DAOTest, 測(cè)試種子數(shù)據(jù) |
Unitils,
DBUnit |
領(lǐng)域 | DO | 領(lǐng)域類 | DomainTest | ?? |
持久化 | ORM | 領(lǐng)域類 |
ORM映射,
ORM映射測(cè)試 |
Hibernate,
ORMUnit |
數(shù)據(jù)傳輸 | DTO | XSD | DTO | JAXB |
DTO組裝 | 組裝 | 映射 | DO-DTO映射文件 | Dozer |
委托 | 業(yè)務(wù)委托 ?? ?? | DO到DTO的 轉(zhuǎn)換 代碼 | ?? | ?? |
外觀 | ?? | 外觀 | ?? |
遠(yuǎn)程服務(wù),
EJB, Web Service |
控制器 | MVC | 控制器映射文件 | Struts/Spring MVC | ?? |
表示層 | MVC | 視圖配置文件 | Spring MVC |
委托層是唯一同時(shí)理解領(lǐng)域?qū)ο蠛虳TO的層。其它層,例如持久層,不應(yīng)該察覺到DTO。
重構(gòu)
重構(gòu)就是改變或調(diào)整應(yīng)用代碼,但不修改應(yīng)用的功能或行為。重構(gòu)可以是設(shè)計(jì)相關(guān)的,也可以是代碼相關(guān)的。設(shè)計(jì)重構(gòu)是為了不斷完善模型、重構(gòu)代碼來提升領(lǐng)域模型。
由于重構(gòu)的迭代性和領(lǐng)域建模不斷演進(jìn)的性質(zhì),重構(gòu)在DDD項(xiàng)目中發(fā)揮著重要作用。將重構(gòu)任務(wù)集成到項(xiàng)目中的方法之一是在項(xiàng)目的每次迭代中添加重構(gòu)環(huán)節(jié),重構(gòu)結(jié)束之后才算完成迭代。理想情況下,每項(xiàng)開發(fā)任務(wù)之前和之后都應(yīng)該進(jìn)行重構(gòu)。
進(jìn)行重構(gòu)應(yīng)該有嚴(yán)格的規(guī)定。結(jié)合使用重構(gòu)、CI和單元測(cè)試,以確保代碼變化不會(huì)破壞任何功能,同時(shí),代碼的變化要有助于以后的代碼和性能改進(jìn)。
自動(dòng)化測(cè)試在重構(gòu)應(yīng)用代碼中發(fā)揮著至關(guān)重要的作用。沒有良好的自動(dòng)化測(cè)試和 測(cè)試驅(qū)動(dòng)開發(fā) (TDD)實(shí)踐,重構(gòu)可能會(huì)產(chǎn)生反面的效果,因?yàn)闆]有自動(dòng)化的方式去驗(yàn)證作為重構(gòu)一部分的設(shè)計(jì)和代碼并變化沒有改變行為、或破壞功能。
像 Eclipse 這 樣的工具有助于用迭代的方式和作為開發(fā)一部分的重構(gòu)來實(shí)現(xiàn)領(lǐng)域模型。Eclipse有一些功能,比如把一個(gè)方法提取或移動(dòng)到不同的類中,或?qū)⒁粋€(gè)方法下推 到子類中。也有幾個(gè)Eclipse代碼分析插件有助于處理代碼依賴關(guān)系、識(shí)別DDD反模式。我做項(xiàng)目的設(shè)計(jì)和代碼審查時(shí),都是依靠插件 JDepend 、 Classycle 和 Metrics 來評(píng)估應(yīng)用中領(lǐng)域和其它模塊的質(zhì)量。
Chris Richardson談到 運(yùn)用代碼重構(gòu) ,以使用Eclipse提供的重構(gòu)功能將過程設(shè)計(jì)轉(zhuǎn)變?yōu)橐粋€(gè)OO設(shè)計(jì)。
單元測(cè)試/持續(xù)集成
我們剛才談到的目標(biāo)之一是領(lǐng)域類應(yīng)該(在最初的開發(fā)階段,以及隨后重構(gòu)已有代碼時(shí))單元可測(cè),而不過多依賴于容器或其它基礎(chǔ)設(shè)施代碼。TDD方法有 助于團(tuán) 隊(duì)盡早地找出任何設(shè)計(jì)問題,并有助于驗(yàn)證代碼與領(lǐng)域模型在保持一致。DDD對(duì)測(cè)試先行開發(fā)來說是很理想的,因?yàn)闋顟B(tài)和行為都包含在領(lǐng)域類中,而且單獨(dú)測(cè)試 它們應(yīng)該是容易的。測(cè)試領(lǐng)域模型的狀態(tài)和行為,又不太過關(guān)注于數(shù)據(jù)訪問或持久化的實(shí)現(xiàn)細(xì)節(jié)是很重要的。
單元測(cè)試框架,比如JUnit或TestNG,都是實(shí)現(xiàn)和處理領(lǐng)域模型很棒的工具。其它測(cè)試框架,像 DBUnit 和Unitils,也可用來測(cè)試領(lǐng)域?qū)?,尤其是把測(cè)試數(shù)據(jù)注入到DAO類中。對(duì)在單元測(cè)試類中增加測(cè)試數(shù)據(jù)來說,這將大大減少編寫額外的代碼。
模擬對(duì)象(Mock objects)同樣有利于單獨(dú)測(cè)試領(lǐng)域?qū)ο?。但是在領(lǐng)域?qū)硬灰獮E用模擬對(duì)象是很重要的。如果有其他測(cè)試領(lǐng)域類的簡(jiǎn)單方法,你應(yīng)該使用這些方法來代替使用 模擬對(duì)象。比如說,如果你能使用真實(shí)的后端DAO類(而不是模擬的DAO實(shí)現(xiàn))和內(nèi)存HSQL數(shù)據(jù)庫(而不是真實(shí)的數(shù)據(jù)庫)測(cè)試一個(gè)實(shí)體類,能使領(lǐng)域?qū)訂?元測(cè)試運(yùn)行得更快,而運(yùn)行得更快正好是使用模擬對(duì)象潛在的主要想法。這樣,你將能測(cè)試領(lǐng)域?qū)ο笾g的協(xié)作(交互),以及它們之間交換的狀態(tài)(數(shù)據(jù))。使用 模擬對(duì)象,我們則只能測(cè)試領(lǐng)域?qū)ο笾g的交互。
一旦開發(fā)任務(wù)完成,所有在開發(fā)階段創(chuàng)建的單元測(cè)試和集成測(cè)試(不管有沒有使用TDD做法)都將成為自動(dòng)化測(cè)試套件的一部分。這些測(cè)試用應(yīng)該經(jīng)常進(jìn)行維護(hù),并經(jīng)常在本地或更高一級(jí)的開發(fā)環(huán)境中執(zhí)行,以便找出新的代碼變化是否在領(lǐng)域類中引入了Bug。
Eric Evans在他的 書 中提到了CI,他說CI應(yīng)該始終運(yùn)用在界定的上下文中,應(yīng)該包括人和代碼的同步。像 CruiseControl 和 Hudson 這些CI工具可用來建立一個(gè)自動(dòng)化構(gòu)建和測(cè)試的環(huán)境,來運(yùn)行應(yīng)用構(gòu)建腳本(使用 Ant 或Maven這些構(gòu)建工具創(chuàng)建)從SCM倉庫中(像 CVS 、 Subversion 等)檢出代碼,編譯領(lǐng)域類(以及應(yīng)用中的其它類),并在沒有構(gòu)建錯(cuò)誤的情況下自動(dòng)運(yùn)行所有的測(cè)試(單元測(cè)試和集成測(cè)試)。CI工具還可以設(shè)置在有任何構(gòu)建或測(cè)試錯(cuò)誤時(shí)(通過E-mail或RSS Feeds)通知項(xiàng)目團(tuán)隊(duì)。
部署
領(lǐng)域模型絕對(duì)不會(huì)是靜態(tài)的;在項(xiàng)目生命周期中,它們會(huì)隨著業(yè)務(wù)需求的演變、新項(xiàng)目中新需求的提出而發(fā)生變化。此外,隨著你開發(fā)和實(shí)現(xiàn)領(lǐng)域模型,你能不斷學(xué)習(xí)和提高,而且你也想在已有的模型中運(yùn)用新的知識(shí)。
打包、部署領(lǐng)域類的時(shí)候,隔離很關(guān)鍵。因?yàn)轭I(lǐng)域?qū)右蕾囉贒AO層的一面,而服務(wù)外觀層又依賴于DAO層的另一面(參見圖2-應(yīng)用架構(gòu)圖),所以這些領(lǐng)域類打包、部署為一或多個(gè)模塊來處理依賴關(guān)系很有意義。
DI、AOP和工廠這些設(shè)計(jì)模式在設(shè)計(jì)階段減少了對(duì)象之間的耦合,并使應(yīng)用模塊化; OSGi (以前被稱為開放服務(wù)網(wǎng)關(guān)規(guī)范)則在運(yùn)行時(shí)處理模塊化。OSGi正在成為打包、發(fā)布企業(yè)應(yīng)用的標(biāo)準(zhǔn)機(jī)制。它能很好地處理模塊之間的依賴關(guān)系。我們還能用OSGi來進(jìn)行領(lǐng)域模型的版本處理。
我們可以把DAO類打包到一個(gè)OSGi的Bundle(DAO Bundle)中,把服務(wù)外觀類打包到另一個(gè)Bundle(服務(wù)Bundle)中,所以DAO或服務(wù)實(shí)現(xiàn)進(jìn)行了修改,或是部署了應(yīng)用的不同版本,由于 OSGi,應(yīng)用都不需要重啟。如果我們?yōu)榱讼蚝蠹嫒?,必須支持某些領(lǐng)域?qū)ο笠延械陌姹竞托碌陌姹荆俏覀円部梢圆渴鹣嗤I(lǐng)域類的兩個(gè)不同版本。
為了利用OSGi的能力,應(yīng)用對(duì)象在消費(fèi)之前(即在客戶端能查找到它們之前),應(yīng)該在OSGi平臺(tái)中進(jìn)行注冊(cè)。這意味著我們必須使用OSGi的API進(jìn)行注冊(cè),我們還必須處理使用OSGi容器啟動(dòng)和通知服務(wù)時(shí)的失敗場(chǎng)景。 Spring Dynamic Modules 框架對(duì)該領(lǐng)域很有利,它允許在應(yīng)用中導(dǎo)出或?qū)肴魏螌?duì)象類型,而不改變?nèi)魏未a。
Spring DM還提供測(cè)試類,以在容器外運(yùn)行OSGi集成測(cè)試。比如說,能從IDE中直接用 AbstractOsgiTests 運(yùn)行集成測(cè)試。設(shè)置由測(cè)試基礎(chǔ)設(shè)施來處理,所以我們不需要為測(cè)試編寫MANIFEST.MF文件,或者進(jìn)行任何的打包或部署。該框架支持大部分目前可用的OSGi實(shí)現(xiàn)( Equinox 、 Knopflerfish 和 Apache Felix )。
貸款處理應(yīng)用使用OSGi、Spring DM、Equinox容器來處理模塊級(jí)別的依賴關(guān)系,以及領(lǐng)域和其它模塊的部署。LoanAppDeploymentTests說明了Spring DM測(cè)試模塊的用法。
示例應(yīng)用設(shè)計(jì)
在貸款處理示例應(yīng)用中用到的領(lǐng)域類列舉如下:
實(shí)體:
- Loan
- Borrower
- UnderwritingDecision
- FundingRequest
值對(duì)象:
- ProductRate
- State
服務(wù):
- FundingService
資源庫:
- LoanRepository
- BorrowerRepository
- FundingRepository
圖3展示了示例應(yīng)用的領(lǐng)域模型圖。
圖3. 分層應(yīng)用領(lǐng)域模型 (點(diǎn)擊查看大圖)
在本文中討論的大部分DDD設(shè)計(jì)概念和技術(shù)都在示例應(yīng)用中進(jìn)行了運(yùn)用。像DI、AOP、注解、領(lǐng)域級(jí)別安全、持久化這些概念都用到了。另外,我還使用了幾個(gè)開源框架來助力DDD開發(fā)和實(shí)現(xiàn)任務(wù)。這些框架列舉如下:
- Spring
- Dozer
- Spring安全
- JAXB(用于封送處理和取消封送處理數(shù)據(jù)的Spring-WS)
- Spring Testing(用于單元測(cè)試和集成測(cè)試)
- DBUnit
- Spring Dynamic Modules
示例應(yīng)用中的領(lǐng)域類利用Equinox和Spring DM框架部署為OSGi模塊。下表顯示了示例應(yīng)用的模塊打包細(xì)節(jié)。
表5. 打包、部署細(xì)節(jié)
層 部署工件名稱 模塊內(nèi)容 Spring配置文件客戶端/控制器 | loanapp-controller.jar | 控制器,客戶端代理類 | LoanAppContext-Controller.xml |
外觀 | loanapp-service.jar | 外觀(遠(yuǎn)程)服務(wù),服務(wù)代理類,XSD | LoanAppContext-RemoteServices.xml |
領(lǐng)域 | loanapp-domain.jar | 領(lǐng)域類、DAO,通用的DTO | LoanAppContext-Domain.xml, LoanAppContext-Persistence.xml |
框架 | loanapp-framework.jar | 框架,實(shí)用工具,監(jiān)視(JMX)類,方面 | LoanAppContext-Framework.xml, LoanAppContext-Monitoring.xml, LoanApp-Aspects.xml |
結(jié)論
DDD是一個(gè)功能強(qiáng)大的概念,只要團(tuán)隊(duì)接受了DDD的培訓(xùn),并開始運(yùn)用“領(lǐng)域第一,基礎(chǔ)設(shè)施第二”的觀點(diǎn),它就會(huì)改變建模者、架構(gòu)師、開發(fā)人員和測(cè) 試人員 思考軟件的方式。由于領(lǐng)域建模、設(shè)計(jì)和實(shí)現(xiàn)中會(huì)涉及具有不同背景和專長(zhǎng)領(lǐng)域的不同利益相關(guān)方(來自IT和業(yè)務(wù)單位),引用Eric Evans的說法,“不要弄混設(shè)計(jì)觀點(diǎn)(DDD)和有助于我們完成它的技術(shù)工具箱(OOP、DI、AOP)之間的界限”。
前進(jìn)中的新領(lǐng)域
本節(jié)涵蓋了一些新出現(xiàn)的、影響DDD設(shè)計(jì)和開發(fā)的方法。這些概念中的一些仍在不斷發(fā)展,觀察它們將如何影響DDD也很有意思。
在領(lǐng)域模型標(biāo)準(zhǔn)的治理、策略實(shí)施,以及實(shí)現(xiàn)的最佳實(shí)踐中,實(shí)施Architecture Rules和契約式設(shè)計(jì)起到了重要作用。Ramnivas 談到了 利用Aspects來強(qiáng)制僅通過工廠創(chuàng)建資源庫對(duì)象;這是在設(shè)計(jì)領(lǐng)域?qū)訒r(shí)經(jīng)常被違背的規(guī)則。
領(lǐng)域特定語言(DSL)和業(yè)務(wù)自然語言(BNL)近幾年來正得到越來越多的關(guān)注。人們可以在領(lǐng)域類中使用這些語言表達(dá)業(yè)務(wù)邏輯。BNL可以用來保存 業(yè)務(wù)規(guī) 范,記錄業(yè)務(wù)規(guī)則,還能作為可執(zhí)行代碼,從這種意義上來說,BNL是非常強(qiáng)大的。還能用它們創(chuàng)建測(cè)試用例,來驗(yàn)證系統(tǒng)是否如預(yù)期的那樣運(yùn)轉(zhuǎn)。
行為驅(qū)動(dòng)開發(fā) (BDD) 是最近被討論的另一個(gè)有趣概念。通過提供跨越業(yè)務(wù)和技術(shù)之間鴻溝的通用詞匯(通用語言),BDD有利于將開發(fā)集中在有優(yōu)先次序、可驗(yàn)證的商業(yè)價(jià)值的發(fā)布 上。通過利用側(cè)重于系統(tǒng)行為方面的術(shù)語,而不是單單著眼于測(cè)試,BDD引導(dǎo)開發(fā)人員將TDD背后的真正價(jià)值最大程度地發(fā)揮出來。如果正確實(shí)踐的話,BDD 可以成為DDD很好的補(bǔ)充,BDD概念會(huì)對(duì)領(lǐng)域?qū)ο蟮拈_發(fā)產(chǎn)生積極的影響;畢竟領(lǐng)域?qū)ο缶褪菍?duì)狀態(tài)和行為的封裝。
事件驅(qū)動(dòng)的體系架構(gòu) (EDA) 是能在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)中發(fā)揮作用的另一個(gè)領(lǐng)域。比如說,在領(lǐng)域?qū)ο髮?shí)例中通知任何狀態(tài)變化的事件模型將有助于處理后事件(post-event)處理任務(wù), 在領(lǐng)域?qū)ο蟮臓顟B(tài)改變時(shí),后事件處理任務(wù)就需要被觸發(fā)。EDA有利于封裝基于事件的邏輯,將之嵌進(jìn)領(lǐng)域邏輯的核心。Martin Fowler評(píng)述了 領(lǐng)域事件 設(shè)計(jì)模式。
?
示例應(yīng)用的代碼可以 在這里 下載
?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
