領域驅動設計(DDD)能給前端帶來什么
感謝大家還沒取關我,畢竟這么久沒更新
最近在建設投資體系,這跟知識體系一樣是個系統(tǒng)工程,大家感興趣的話后續(xù)可以聊下這個話題。然后讀書系列會繼續(xù)進行下去~
為什么需要DDD
在回答這個問題之前,我們先看下大部分軟件都會經歷的發(fā)展過程:頻繁的變更帶來軟件質量的下降

而這又是軟件發(fā)展的規(guī)律導致的:
軟件是對真實世界的模擬,真實世界往往十分復雜
人在認識真實世界的時候總有一個從簡單到復雜的過程
因此需求的變更是一種必然,并且總是由簡單到復雜演變
軟件初期的業(yè)務邏輯非常清晰明了,慢慢變得越來越復雜
可以看到需求的不斷變更和迭代導致了項目變得越來越復雜,那么問題來了,項目復雜性提高的根本原因是需求變更引起的嗎?
根本原因其實是因為在需求變更過程中沒有及時的進行解耦和擴展。

那么在需求變更的過程中如何進行解耦和擴展呢?DDD發(fā)揮作用的時候來了。
什么是DDD
DDD(領域驅動設計)的概念見維基百科:
https://zh.wikipedia.org/wiki/%E9%A0%98%E5%9F%9F%E9%A9%85%E5%8B%95%E8%A8%AD%E8%A8%88
可以看到領域驅動設計(domin-driven design)不同于傳統(tǒng)的針對數(shù)據(jù)庫表結構的設計,領域模型驅動設計自然是以提煉和轉換業(yè)務需求中的領域知識為設計的起點。在提煉領域知識時,沒有數(shù)據(jù)庫的概念,亦沒有服務的概念,一切圍繞著業(yè)務需求而來,即:
現(xiàn)實世界有什么事物 -> 模型中就有什么對象
現(xiàn)實世界有什么行為 -> 模型中就有什么方法
現(xiàn)實世界有什么關系 -> 模型中就有什么關聯(lián)
在DDD中按照什么樣的原則進行領域建模呢?
單一職責原則(Single responsibility principle)即SRP:軟件系統(tǒng)中每個元素只完成自己職責內的事,將其他的事交給別人去做。
上面這句話有沒有什么哪里不清晰的?有,那就是“職責”兩個字。職責該怎么理解?如何限定該元素的職責范圍呢?這就引出了“限界上下文”的概念。
Eric Evans 用細胞來形容限界上下文,因為“細胞之所以能夠存在,是因為細胞膜限定了什么在細胞內,什么在細胞外,并且確定了什么物質可以通過細胞膜。”這里,細胞代表上下文,而細胞膜代表了包裹上下文的邊界。
我們需要根據(jù)業(yè)務相關性、耦合的強弱程度、分離的關注點對這些活動進行歸類,找到不同類別之間存在的邊界,這就是限界上下文的含義。上下文(Context)是業(yè)務目標,限界(Bounded)則是保護和隔離上下文的邊界,避免業(yè)務目標的不單一而帶來的混亂與概念的不一致。
如何DDD
DDD的大體流程如下:
建立統(tǒng)一語言
統(tǒng)一語言是提煉領域知識的產出物,獲得統(tǒng)一語言就是需求分析的過程,也是團隊中各個角色就系統(tǒng)目標、范圍與具體功能達成一致的過程。
使用統(tǒng)一語言可以幫助我們將參與討論的客戶、領域專家與開發(fā)團隊拉到同一個維度空間進行討論,若沒有達成這種一致性,那就是雞同鴨講,毫無溝通效率,相反還可能造成誤解。因此,在溝通需求時,團隊中的每個人都應使用統(tǒng)一語言進行交流。
一旦確定了統(tǒng)一語言,無論是與領域專家的討論,還是最終的實現(xiàn)代碼,都可以通過使用相同的術語,清晰準確地定義領域知識。重要的是,當我們建立了符合整個團隊皆認同的一套統(tǒng)一語言后,就可以在此基礎上尋找正確的領域概念,為建立領域模型提供重要參考。
舉個例子,不同玩家對于英雄聯(lián)盟(league of legends)的稱呼不盡相同;國外玩家一般叫“League”,國內玩家有的稱呼“擼啊擼”,有的稱呼“LOL”等等。那么如果要開發(fā)相關產品,開發(fā)人員和客戶首先需要統(tǒng)一對“英雄聯(lián)盟”的語言模型。
事件風暴(Event Storming)
事件風暴會議是一種基于工作坊的實踐方法,它可以快速發(fā)現(xiàn)業(yè)務領域中正在發(fā)生的事件,指導領域建模及程序開發(fā)。它是Alberto Brandolini發(fā)明的一種領域驅動設計實踐方法,被廣泛應用于業(yè)務流程建模和需求工程,基本思想是將軟件開發(fā)人員和領域專家聚集在一起,相互學習,類似頭腦風暴。
會議一般以探討領域事件開始,從前向后梳理,以確保所有的領域事件都能被覆蓋。
什么是領域事件呢?
領域事件是領域模型中非常重要的一部分,用來表示領域中發(fā)生的事件。一個領域事件將導致進一步的業(yè)務操作,在實現(xiàn)業(yè)務解耦的同時,還有助于形成完整的業(yè)務閉環(huán)。
領域事件可以是業(yè)務流程的一個步驟,比如投保業(yè)務繳費完成后,觸發(fā)投保單轉保單的動作;也可能是定時批處理過程中發(fā)生的事件,比如批處理生成季繳保費通知單,觸發(fā)發(fā)送繳費郵件通知操作;或者一個事件發(fā)生后觸發(fā)的后續(xù)動作,比如密碼連續(xù)輸錯三次,觸發(fā)鎖定賬戶的動作。
進行領域建模,將各個模型分配到各個限界上下文中,構建上下文地圖。
領域建模時,我們會根據(jù)場景分析過程中產生的領域對象,比如命令、事件等之間關系,找出產生命令的實體,分析實體之間的依賴關系組成聚合,為聚合劃定限界上下文,建立領域模型以及模型之間的依賴。
上面我們大體了解了DDD的作用,概念和一般的流程,雖然前端和后端的DDD不盡相同,但是我們仍然可以將這種思想應用于我們的項目中。
DDD能給前端項目帶來什么
通過領域模型 (feature)組織項目結構,降低耦合度
很多通過react腳手架生成的項目組織結構是這樣的:
-componentscomponent1component2-actions.ts...allActions-reducers.ts...allReducers
首先從功能角度對項目進行拆分。將業(yè)務邏輯拆分成高內聚松耦合的模塊。從而對 feature 進行新增,重構,刪除,重命名等變得簡單 ,不會影響到其他的feature,使項目可擴展和可維護。 
再從技術角度進行拆分,可以看到componet, routing,reducer 都來自等多個功能模塊
技術上的代碼按照功能的方式組織在feature下面,而不是單純通過技術角度進行區(qū)分。 通常是由一個文件來管理所有的路由,隨著項目的迭代,這個路由文件也會變得復雜。那么可以把路由分散在feature中,由每個feature 來管理自己的路由。

如何組織 componet,action,reducer
按feature組織組件,action 和 reducer 組件和樣式文件在同一級 Redux放在單獨的文件
每個feature下面分為 redux文件夾 和 組件文件

redux文件夾下面的 action.js 只是充當loader的作用,負責將各個action引入,而沒有具體的邏輯。reducer 同理

項目的根節(jié)點還需要一個 root loader 來加載 feature 下的資源

如何組織 router
每個feature都有自己專屬的路由配置 頂層路由(頁面級別的路由)通過JSON配置1,然后解析JSON到React Router


import { App } from '../features/home';import { PageNotFound } from '../features/common';import homeRoute from '../features/home/route';import commonRoute from '../features/common/route';import examplesRoute from '../features/examples/route';const childRoutes = [homeRoute,commonRoute,examplesRoute,];const routes = [{path: '/',componet: App,childRoutes: [... childRoutes,{ path:'*', name: 'Page not found', component: PageNotFound },].filter( r => r.componet || (r.childRoutes && r.childRoutes.length > 0))}]export default routes
import React from 'react';import { Switch, Route } from 'react-router-dom';import { ConnectedRouter } from 'connected-react-router';import routeConfig from './common/routeConfig';function renderRouteConfig(routes, path) {const children = [] // children component listconst renderRoute = (item, routeContextPath) => {let newContextPath;if (/^\//.test(item.path)) {newContextPath = item.path;} else {newContextPath = `${routeContextPath}/${item.path}`;}newContextPath = newContextPath.replace(/\/+/g, '/');if (item.component && item.childRoutes) {const childRoutes = renderRouteConfigV3(item.childRoutes, newContextPath);children.push(<Routekey={newContextPath}render={props => <item.component {...props}>{childRoutes}</item.component>}path={newContextPath}/>,);} else if (item.component) {children.push(<Route key={newContextPath} component={item.component} path={newContextPath} exact />,);} else if (item.childRoutes) {item.childRoutes.forEach(r => renderRoute(r, newContextPath));}};routes.forEach(item => renderRoute(item,path))return <Switch>children</Switch>}function Root() {const children = renderRouteConfig(routeConfig, '/');return (<ConnectedRouter>{children}</ConnectedRouter>);}
