領域驅動設計在前端中的應用

2021-02-23 前端迷

(文章末尾有抽獎送書活動)垃圾桶現象

在開始本篇文章前,我給讀者們分享一個很考驗人性的有趣現象,在公司洗手間的洗漱臺旁邊,放置了一個垃圾桶,每次我洗完手,用紙巾擦乾手後,將其扔進垃圾桶,但是偶爾扔不準會扔到垃圾桶外面。

一般情況下,我會將其撿起,再放入垃圾桶,心裡想著:「不能破壞這麼幹淨的環境呀」。

但是,當垃圾桶周邊有很多別人沒扔進去的餐巾紙時,我就不會那麼願意將自己沒扔進去的餐巾紙再撿起來扔進去,想著:「反正都這麼邋遢了,多了一個也不會怎樣」。

萬惡的人心呀!

過了很久,我接手了一個老的項目,這個項目經過近十個人手迭代,傳到我這裡時,已經是非常混亂的狀態了,閱讀代碼時,發現了很多不合理的寫法與隱藏式BUG,當我在寫新的需求時,很自然地,我不會那麼精益求精地編寫業務邏輯,甚至也會留下一些隱藏的坑給後人。

恰恰相反,前段時間有幸接手一個大佬的項目,閱讀其代碼仿佛如沐春風,整個結構堪稱完美,邏輯條理清晰,看代碼就像看需求文檔一樣,堪稱一絕。這個時候,當我要在其寫新的需求,我會模仿其設計,小心翼翼地將自己代碼插入其中,就像不忍心破壞這件藝術品一樣。

以上故事純屬我一個理想主義程式設計師虛構。

但是回到現實當中,我們維護一個混亂項目和一個優雅項目的心情肯定是不一樣的,就像上面講的那個垃圾桶現象,混亂的項目就像周圍遍布很多垃圾的垃圾桶,當你在混亂項目裡再添加一些混亂代碼後會良心也不會很痛,而優雅的項目你就會注意自己的行為,不能一顆老鼠屎壞了一鍋粥。

前端開發面臨的困難

這裡我們講到的困難並不是指技術細節實現層面上的困難,而是從整個軟體開發過程中,遇到對高複雜度業務的開發困難,比如說很難從代碼中直觀地看出業務邏輯,項目經歷不同人手迭代導致的邏輯書寫規範不一致而進一步導致的後續人員理解成本高昂,說簡單點,就是在高複雜度的業務之上,開發人員沒有很強的意識去簡化邏輯,將業務知識直接體現在代碼中,我們具體從以下幾個點來講解這個問題。

業務邏輯本身錯綜複雜

這一點作為開發者是很難避免的,在一個項目中,必然會存在一些邏輯複雜的業務,初始開發者是最能夠理解該業務的每個細節的,將業務映射成實際的代碼過程中,複雜的業務轉換成的代碼肯定是也是複雜的,如何將其直面地轉換成更易理解的代碼,讓後續維護者閱讀代碼就能大致理解其業務邏輯概貌,而進一步提升維護者開發的信心。

對全局業務理解不夠透徹

一個項目在開發過程中人員變動是很正常的,可能是前人離職後人接手,也可能是新增人手,新人對業務的理解往往是不夠透徹的,可能一來就直接評審接著就進入開發,比如新增了一個接口需要將數據展示在頁面上,該需求前因後果並不知曉,這就形成了一種「面向頁面」開發模式,對業務不熟悉,自然無法合適地將新的需求代碼融入整個項目體系中。

知識在團隊傳播中的丟失

複雜的業務邏輯知識在團隊中是很難傳播的,在人員的變動後,更是支離破碎,業務知識丟失後,開發者就會陷入「不知在哪改、不敢改、不願改」的泥淖中,最終導致業務開發不下去,推倒重來,嚴重影響整個項目的進展,我們在這裡能做的,就是儘量將代碼寫成既能運行又能展示業務邏輯知識的形態,讓後續的維護者更有信心的面對「知識丟失」這一困境。

團隊無法形成統一邏輯代碼書寫規範

這裡我指的書寫規範並不是指 eslint 之類的 style 規範,而是書寫業務邏輯的位置、方式、分層、復用等,比如 A 為了將應用隔離而習慣將接口寫在 UI 層直接處理數據,B 習慣將接口寫在 common 模塊供自己或者別人在 UI 層調用,A 習慣將 util 類工具函數直接和 api 接口混在一起寫,而 B 更願意將 util 類函數寫得更通用放在 common 模塊,假如新來了開發者 C , C 看到各式各樣的風格就會很疑惑,不知應該按照 A 還是 B 或者按照自己的習慣書寫,隨著開發人員越來越多,直接會導致了整個項目邏輯書寫規範的崩潰,維護者的維護信心會大打折扣。

真實業務案例

為了讓讀者能夠更直觀的理解領域驅動設計的思想,我們用一個多頁面應用來舉一些例子,同時為了體現出普通設計與領域驅動設計的區別,我們會用兩種設計方式來實現同一需求,並且每個需求都由團隊中的 A B C D 成員完成,成員的技術水平與代碼風格各不相同,我們會分析在普通設計下,會出現哪些使得代碼複雜度失控的行為。之後我們使用領域驅動設計的思維去重構該項目,再分析其設計方式如何讓項目業務邏輯更清晰與更易維護。

案例需求分析

該需求為一個大型零售業務的 demo 版,請讀者儘可能地將其想像為更為複雜的業務場景,該項目分為商品主頁、個人中心頁、積分中心頁、抽獎活動頁面,具體需求為:

A 成員:主頁為商品展示頁面,這個頁面展示推薦的商品列表,同時在頁面的右上角,展示用戶的頭像與用戶名。

B 成員:實現用戶的積分中心頁面,該頁面展示用戶的剩餘積分、積分記錄列表、積分兌換禮品。

C 成員:實現個人中心頁面,個人中心展示了用戶的詳細信息,除此之外還需要展示用戶積分中心的積分。

D 成員:實現抽獎活動頁面,用戶在該頁面能夠使用積分中心中的積分進行抽獎,每次抽獎將會消耗100積分,中獎的獎品分虛擬獎品(優惠券、會員、積分)、實物獎品(需要用戶填寫收貨地址)。

接口與原型圖

具體業務需求原型圖以及頁面對接口的調用如圖所示:

在上文中已經假設該項目會越來越龐大,所以為了更高效地開發我們將其設置成多頁面應用,我們看到在寫少量接口與頁面的情況下,視圖與不同領域的接口調用已經是非常混亂的,在實際代碼中,這種混亂程度會因為上述講到「前端開發面臨的困難」中的問題而進一步放大,下一節我們將使用非常不規範的團隊協作來實現整個項目。

不規範的代碼設計

我們假設該團隊中成員的規範意識不強烈,各有各的代碼風格與分層習慣,這樣的代碼會寫成怎樣呢?

該項目我已經傳到 Github 中,讀者可訪問:ddd-fe-demo clone 代碼後執行以下操作啟動項目:

// 切換到不規範寫法的分支下

git checkout feature/normal

// 啟動 mock 數據

npm run server

// 啟動頁面

npm run start

多頁面應用,各頁面url:

商城主頁:http://localhost:3000/index.html

個人中心:http://localhost:3000/user.html

權益中心:http://localhost:3000/interest.html

抽獎活動頁面:http://localhost:3000/lottery.html

mock數據

這裡我們用 mock 數據模仿後端的請求,下面是所以的數據接口,返回的數據請在 /ddd-fe-demo/server/*.js中查看:

// goods API

'GET /goods/list' // 獲取商品列表

// user API

'GET /user/detail' // 獲取用戶信息詳情

// ponit API

'GET /interest/point' // 獲取用戶剩餘積分

'GET /interest/pointRecord' // 獲取用戶積分記錄數據

'GET /interest/gift' // 獲取積分兌換獎品

// lottery API

'GET /lottery/detail' // 獲取該抽獎活動的詳情

'GET /lottery/prizeList' // 獲取獎品列表

'POST /lottery/play' // 觸發抽獎

'POST /lottery/address' // 填寫獎品收貨地址

文件結構分析

├── common

│ └── util

│ └── http.js // 統一axios庫

└── page

├── index // 商城主頁目錄

│ ├── App.js

│ ├── apis // 商城頁面用到的api

│ │ ├── goods.js

│ │ └── user.js

│ ├── components

│ │ ├── GoodsItem.js

│ │ ├── GoodsItem.scss

│ │ ├── Nav.js

│ │ └── Nav.scss

│ └── index.js

├── interest // 積分權益頁面目錄

│ ├── App.js

│ ├── App.scss

│ ├── apis // 商城頁面用到的api

│ │ ├── interest.js

│ │ └── user.js

│ ├── components

│ │ ├── GiftItem.js

│ │ ├── GiftItem.scss

│ │ ├── PointRecordItem.js

│ │ └── PointRecordItem.scss

│ └── index.js

├── ....

細數存在的問題

這裡我們通過貼出項目中的問題代碼,來分析出一些存在問題,以及討論其會導致的後果與優化的方案。

視圖層過厚

問題代碼的位置: /src/page/index/components/GoodsItem.js

return(

<div className="goods-item">

<div className="main-info">

<img className="goods-img" src={mainPic} alt=""/>

<div className="goods-name">{goodsName}</div>

{/* 當 status 為2時,表示無貨 */}

{status === 2

? <span className="out-stock">已無貨</span>

: null}

</div>

<div className="detail-info">

{/* 當 activityType 為 3 表示該商品正在參與活動,為特價商品 */}

{activityType === 3

? <span className="price discount">特價:{price / 100} 元</span>

: <span className="price">價格:{price / 100} 元</span>}

<div className="tag-wrap">

{filterTag.map(v=>{

return (

<span className="tag">{v.title}</span>

)

})}

</div>

</div>

</div>

)

存在的問題: 視圖層原本只需要展示 DOM 的結構,但這裡卻承擔了各種邏輯判斷、數據篩選、數據轉換等「雜活」,視圖代碼與邏輯代碼比例已經接近 1 : 1。

導致的後果: 難以直觀地理解視圖結構,並且在視圖層寫大段的注釋顯然是很不優雅。

優化思路: 視圖層最好單一,數據展示到視圖層之前,做好數據的篩選、轉換,判斷邏輯抽象層公用函數放入 util 中。

判斷邏輯重複

問題代碼位置: src/page/index/components/Nav.js & src/page/user/App.js

<div className="user">

<img className={`${userInfo.vip ? 'vip' : ''}`} src={userInfo.avatar} alt=""/>

<span>{userInfo.userType === 2 ? '尊敬的籤約客戶:' : null}{userInfo.userName}</span>

</div>

<div>{userType === 2 ? '尊敬的籤約客戶:' : null}{userName}</div>

存在的問題: 同樣的邏輯在兩個視圖層中重複出現,這是團隊協作經常會遇到的問題,假設例子中的邏輯較假設非常複雜的,各成員實現方式不一致,在後期維護將會造成許多問題。

導致的後果:

優化思路: 試圖將某個實體抽象成一個類,比如將用戶抽象成 User 類,類中有一個方法為 isContractUser 用來判斷用戶是否為籤約客戶,之後視圖層只需要調用 User.isContractUser() 便能夠復用這塊邏輯,並且容易理解其含義。

接口調用不統一

問題代碼位置: src/page/index/apis/user.js & src/page/lottery/apis/user.js & src/page/user/apis/user.js

import axios from '@common/util/http';

export function getUserInfo() {

return axios('/user/detail');

}

存在問題: 多塊業務頁面用到了同一個接口,並且在各自的根目錄下都有一份相同的請求代碼。作為成員,可能有這樣的辨詞:「我怕他改動了這個接口的參數配置會導致我的頁面出問題,為了相互隔離而將其複製一份」,雖然有道理,但是這不是最優解。

導致的後果: 非常直觀地後果就是代碼重複不優雅,修改一塊業務卻找到了很多相同的請求邏輯,容易搞混,並且接口發生變化後,統一維護的成本較大。

優化思路: 將整個項目中所有的請求函數統一放在 commom 中管理,根據領域劃分,比如說用戶領域下,存放用戶相關的接口,接口函數儘量可配置、可拓展,供多個業務使用。

接口欄位不可控性

問題代碼: src/page/user/App.js

getUserInfo().then(data => {

this.setState({

userInfo: data

})

})

}

getUserPonitCount = () => {

getUserPointCount().then(count => {

this.setState({

pointCount: count

})

})

}

render() {

const { userInfo, pointCount } = this.state;

// vip 單從字面上難以辨別出是一個bool類型,更規範的命名應該是 isVip

// avatar 是一個 url 類型的欄位,更規範的寫法應該是 avatarUrl 會更直觀

const { avatar, userName, userType, tel, vip, email, vipValidityDate } = userInfo;

存在的問題: 定義欄位在理想的情況下是前端主導,且前後端有共同的認知,但是不排除特殊情況下接口欄位定義混亂且不直觀。

導致的後果: 閱讀代碼時,接口欄位不規範,在視圖層展示時,會導致誤解或者難以理解的代碼邏輯。

優化思路: 將接口層抽離出來,在接口返回時,逐一將欄位列舉出來,將不符合規範的欄位進行糾正,轉換成更易理解的詞語,甚至在這一層中就能將很多欄位內容進行轉換,比如後端返回的金額為分單位*100的整數,在這一層中即將其轉換為浮點數,在視圖層中即可直接使用。

忽略業務整體

問題代碼: 上述整個項目例子。

存在的問題: 在一個龐大、多人協作的項目,作為其中一員很可能出現對整個系統理解不夠,只知道自己負責的那幾個頁面,逐步惡化成「面向頁面編程」。

導致的後果:

這對整個項目的「成長」是不利的,會導致像上述舉例代碼中出現的「重複性」問題。假如開發者對整個項目有全局的了解,在編碼時,會考慮更多的「可拓展性」與「預判未來性」,或者在接手其他成員負責的領域也會減少很多上手成本。

從業務的角度看,在需求評審的過程中,熟悉整體業務,會對其新的需求進行更深的思考,判斷其對整個項目是否會有明顯的「驅動」作用,而進一步考慮是否應該拒絕該需求或者提出更好的需求建議,避免成為產品經理說什麼就做什麼的「面向頁面編程」工程師。

優化思路: 將每一塊業務劃分成不同的領域,各領域下包含哪些服務,每個頁面調用的並不是 API 接口,而是各自領域的服務。

領域驅動設計

首先提出領域的角色是需求方,每一個需求都必將會映射到某個領域,比如「搜索商品」這個動作對應著商品中心域,「用戶登錄」對應著用戶信息&鑑權域。從產品-後端-前端對其領域的劃分認知都是一致的,這是各角色對其整個項目進行合作的基礎,在一起討論問題時,都知道對方講述的信息是處在哪個域上。

在對領域具有統一認知的情況下,需求方也會更謹慎、清晰地提出新的需求或是更改業務邏輯,各方人員對其業務的熟悉後,也能從自己負責的職能角度上表達出自己對新業務新迭代的看法或建議,而不是「機械」地根據需求文檔完成自己的職責。

假設各方角色對整體業務領域不熟悉,大家對其業務的認知不統一,項目很快就會成為一個鬆散的結構,需求方、開發方、設計方的產出模型無法大致匹配,最後成為開發/維護代價極高的「危樓」項目。

領域驅動設計不是萬能的,它只是解決了軟體開發中的部分問題,也不是可適用於任何場景的,但是其核心思想是可以借鑑到軟體設計與開發過程中的,本文主要講解領域驅動設計在前端中解決的問題以及核心思想。

業務領域

在龐大的項目中,領域是非常繁多的,在對領域進行劃分時,我們需要與產品、後端進行統一。我理解的先後流程是:產品產出需求,需求被劃分到已有或新的領域,後端接到需求根據其領域的劃分產出接口文檔,前端根據產品&後端的領域劃分設計出前端的領域,三方大致統一領域劃分後,各自對需求概念、名詞認知統一,最後才進行代碼的編寫。

我們根據上述項目業務進行領域劃分,得到以下領域模塊圖,每個具體的功能都明確對應唯一一塊領域,產品、後端、前端對其都應該有一致的認知。

領域模塊圖是需要各方人員進行持續維護演進的,其存在的意義是加強了成員對業務的理解,讓團隊成員力量進行聚焦,共同思考業務,這樣才能讓項目走的更遠、更穩。

前端領域設計與結構分層

回到前端開發的設計上,我們理解了上述講解的業務領域的概念後,接著將其落實到前端開發中,我們重點需要理解的概念是 職責分明,合理分層,根據上述提出的「問題代碼」,我們希望在前端結構設計中能做到:

視圖層儘可能薄:獲得的數據能夠直接使用到視圖層中,禁止在視圖層中對數據進行轉換、篩選、計算等邏輯操作。

不寫重複邏輯:遇到相同的邏輯儘可能復用而不是重寫,邏輯函數儘可能寫成可拓展可維護,暴露給團隊其他成員。

不同職責的代碼進行分層:將不同職責代碼合理分層,每層儘可能純淨,互不影響。

前端欄位不受後端影響:返回欄位進行糾正,欄位含義儘可能直觀,在視圖層使用時,能夠更清晰地描述視圖結構。

可縱觀全局領域:前端進行領域模塊結構設計時,能夠縱覽整個項目下所有的領域,以及每個領域下具有的邏輯功能。

帶著以上五個目的,我們以第一步的業務領域為基礎,進行前端結構的分層,並且開始改造上文出現問題的項目。

改造後的項目在 ddd-fe-demo 中,請讀者 clone 下來後,進行以下操作啟動項目:

git checkout master

npm run server // 啟動 mock 接口

npm run start // 啟動前端服務

同樣是訪問以下 url

商城主頁:http://localhost:3000/index.html

個人中心:http://localhost:3000/user.html

權益中心:http://localhost:3000/interest.html

抽獎活動頁面:http://localhost:3000/lottery.html

建議讀者在開始下面內容閱讀前,將改造後的源碼大致閱讀一遍,帶著問題進入接下來的內容,會有更深的理解。

項目結構圖

為了讓各層職責分明,視圖層儘可能純粹,我們將各功能塊代碼進行分層,得到以下層級:

分層之後明顯地降低了項目的複雜度,將前端的業務邏輯代碼與視圖邏輯進行解耦。

文件結構

我們根據上述結構圖的分層思想,在實際項目中定義了以下的文件目錄:

├── common

│ ├── components // 公用組件

│ ├── constants // 全局變量

│ │ ├── goods

│ │ │ └── index.js

│ │ ├── ...

│ ├── data-source // 數據接口層

│ │ ├── goods

│ │ │ ├── requestApis.js

│ │ │ └── translators.js

│ │ ├── ...

│ ├── domains // 領域層

│ │ ├── goods-domain

│ │ │ ├── entities // 實體

│ │ │ │ └── goods.js

│ │ │ └── goodsService.js // 領域Service服務

│ │ ├── ...

│ └── util // 公用函數

│ └── http.js

└── page // 頁面視圖層

├── index

│ ├── App.js

│ ├── components

│ │ ├── GoodsItem.js

│ │ ├── GoodsItem.scss

│ │ ├── Nav.js

│ │ └── Nav.scss

│ ├── index.js

│ └── services // 該頁面需要用到的Service

│ └── index.js

├── ...

數據接口層 data-source

代碼位置:src/common/data-source/interest/requestApis.js

import axios from '@common/util/http';

src/common/data-source/interest/requestApis.js

import { pointRecordTranslator, pointGiftTranslator } from './translators'

export function getUserPointRecordList() {

return axios('/interest/pointRecord').then(data => {

return data.map(item => pointRecordTranslator(item));

})

}

export function getInterestGiftList() {

return axios('/interest/gift').then(data => {

return data.map(item => pointGiftTranslator(item))

})

}

分層作用: 在這一層中集結了 interest 領域下所有的接口函數,避免了數據接口分散到各個頁面,統一存放更易管理,這裡我們解決了上文提出的 接口調用不統一問題。

代碼位置:src/common/data-source/goods/translators.js

export function goodsTranslator({

id,

goodsName,

price,

status,

activityType,

desc,

brand,

relatedModelId,

mainPic,

tag,

relatedModelImg

}) {

return {

id,

name: goodsName,

price: (price / 100).toFixed(2),

status,

activityType,

description: desc,

brandName: brand,

mainPicUrl: mainPic,

tags: tag

}

}

分層作用: 在這一層對接口欄位、內容經過二次加工,避免了後端定義欄位不規範、混亂對前端的影響,含義清晰、規範的欄位在視圖層使用時更具有表現力,這裡我們解決了上文提出的 接口欄位不可控性問題。

數據接口層是整個項目的根基,提供了結構清晰、定義規範、可直接使用的數據。

領域層 -> domain

領域層是整個項目的核心層,它掌管了所有領域下的行為與定義,它是整個項目中最能體現業務知識的一層。

代碼位置: src/common/domains/lottery-domain/entities/lottery.js

/**

* 抽獎活動實體

*/

import dayjs from 'dayjs'

import { lotteryTypeMap } from '@constants/lottery'

class Lottery {

constructor(lottery={}) {

this.id = lottery.id

this.name = lottery.name

this.type = lottery.type

this.startDate = lottery.startDate

this.endDate = lottery.endDate

}

// 獲取活動時間範圍

getLotteryTimeScope() {

return `${dayjs(this.startDate).format("M月D日")} - ${dayjs(this.endDate).format("M月D日")}`

}

// 獲取活動類型描述

getLotteryType() {

return this.type && lotteryTypeMap[this.type].title

}

}

export default Lottery

在前端中,我們把它定義為一個 class 類,構造函數中初始化實體的屬性,在類中定義了實體的方法,屬性和方法的返回值主要是用於視圖層中的直接展示,同一個實體的邏輯確保只在實體類中編寫,在不同視圖下可復用,這裡我們解決了上文提出的 判斷邏輯重複的問題。

代碼位置: src/common/domains/lottery-domain/lotteryService.js

import {

getLotteryDetail,

getPrizeList,

playLottery,

savePrizeAddress

} from '@data-source/lottery/requestApis';

import Prize from './entities/prize';

import Lottery from './entities/lottery';

class LotteryService {

/**

* 獲取本次抽獎活動詳情

* @param {string} id 活動id

*/

static getLotteryDetail(id) {

return getLotteryDetail(id).then(lottery => new Lottery(lottery))

}

/**

* 獲取本次抽獎活動的獎品列表

* @param {string} id 抽獎活動id

*/

static getPrizeList(id) {

return getPrizeList(id).then(list => {

return list.map(item => new Prize(item));

})

}

/**

* 進行抽獎

* @param {string} id 抽獎活動id

*/

static playLottery(id) {

return playLottery(id).then(result => {

const { recordId, prize } = result;

return {

recordId,

prize: new Prize(prize)

}

})

}

/**

* 填寫中獎的收貨地址信息

* @param {Object} param0 中獎記錄id以及地址信息

*/

static savePrizeAddress({ recordId, name, phoneNumber, address }) {

const data = {

recordId,

name,

phoneNumber,

address

}

return savePrizeAddress(data)

}

}

export default LotteryService

分層作用: 我們可以看到,Service 層連接了 entity 層與 data-source 層,接收後端返回的數據將其轉換成具有屬性與方法的 entity 實體類,供視圖層直接進行展示。不僅如此,Service 層還定義了該領域下的所有行為,比如填寫收貨地址。領域服務層涵蓋了整個業務領域的行為,直觀地體現了業務需求。這裡我們解決了上文提出的 忽略業務整體問題。

View 視圖層 -> view

視圖層也就是我們書寫交互邏輯、樣式的一層,可以使用純 HTML 或者框架(React、Vue),這一層只需要調用了領域的服務,將返回值直接體現在視圖層中,無需編寫條件判斷、數據篩選、數據轉換等與視圖展示無關的邏輯代碼,這些「糙活」都在其他層中以已經完成,所以視圖層是非常「薄」的一層,只需關注視圖的展示與交互,整個 HTML 結構非常直觀清晰。

代碼位置: src/page/user/App.js

import React from 'react';

import { UserService, InterestService } from './services';

import User from '@domain/user-domain/entities/user';

import { SIGN_USER_TYPE } from '@constants/user';

import "./App.scss"

class App extends React.Component {

state = {

pointCount: null,

user: new User()

}

componentDidMount() {

this.getUserInfo();

this.getUserPonitCount();

}

// 獲取用戶信息

getUserInfo = () => {

UserService.getUserDetail().then(user => {

this.setState({

user

})

});

}

// 獲取用戶積分

getUserPonitCount = () => {

InterestService.getUserPointCount().then(count => {

this.setState({

pointCount: count

})

})

}

render() {

const { pointCount, user } = this.state;

return (

<div className="user-page">

<h3>個人中心</h3>

<div className="user">

<div className="info">

<div>{user.type === SIGN_USER_TYPE ? `尊敬的${user.getUserTypeTitle()}:` : null}{user.name}</div>

<div>綁定手機號: {user.phoneNumber}</div>

<div>綁定email: {user.email}</div>

</div>

<div className="avatar">

<img className={`${user.isVip ? 'vip' : ''}`} src={user.avatarUrl} alt=""/>

{ user.isNeedRemindUserVipLack() && user.isVip

? <div>會員還有{user.getVipRemainDays()}天</div>

: ''

}

</div>

</div>

<div className="lottery-tips">

<div>剩餘積分:{pointCount} 分</div>

<a href="/interest.html">前往積分權益中心 ></a>

</div>

</div>

);

}

}

export default App;

我們可以對比之前寫的「問題代碼」:

render() {

const { userInfo, pointCount } = this.state;

const { avatar, userName, userType, tel, vip, email, vipValidityDate } = userInfo;

// console.log()

const remainDay = dayjs(vipValidityDate).diff(new Date(), 'day');

return (

<div className="user-page">

<h3>個人中心</h3>

<div className="user">

<div className="info">

<div>{userType === 2 ? '尊敬的籤約客戶:' : null}{userName}</div>

<div>綁定手機號: {tel}</div>

<div>綁定email: {email}</div>

</div>

<div className="avatar">

<img className={`${vip ? 'vip' : ''}`} src={avatar} alt=""/>

{ remainDay < 6 && vip

? <div>會員還有{remainDay}天</div>

: ''

}

</div>

</div>

<div className="lottery-tips">

<div>剩餘積分:{pointCount} 分</div>

<a href="/interest.html">前往積分權益中心 ></a>

</div>

</div>

);

}

分層作用: 將 Service 中返回的數據直接使用,視圖層中只編寫交互與樣式,不管是 HTML 純粹的結構還是代碼可讀性,新的設計都更討人喜歡。除了視圖層與前端框架有關,其他層可獨立應用於任何框架的,分層的結構解決了上文提出的 視圖層過厚問題。

實踐過程中的建議堅定信仰

領域驅動設計的初衷是將項目進行合理地結構分層,降低複雜項目的維護難度,有效地減少團隊成員之間的協作成本,將業務直觀地映射成代碼,讓開發者更關注業務整體的本身,不局限於自己的職責,共同提出更好的業務建議,只有業務真正有價值了,你寫的優秀代碼才能保證被傳承下去。

而一個複雜項目的生命周期是非常久的,可能長達幾年,維護舊代碼時間肯定會比編寫新需求更長,為了後期能夠爽快地維護,前期多付出寫時間去改善代碼結構與質量,從長遠的角度來看,是非常值得的。就算中途離職,留給後人的代碼也能做到問心無愧,更不會留下臭名昭著的名聲。

所以,這裡我想說的是,做一個更理想主義的程式設計師,堅定自己的信念,在實行領域驅動設計的初期或許會出現各種不適,甚至會受到冷嘲熱諷,挺過了這段適應期後,就能體會到自己設計的結構與代碼很絲滑很優雅。

團隊成員實時同步

團隊成員之間都需要熟悉全局的領域模型,特別是當需要修改他人負責領域下的代碼,更是要熟悉其領域下的細節。當團隊中加入了新的成員後,先向他介紹我們項目下的領域模型,再分享我們的項目架構與分層。

在另一方面,團隊協作開發最不利的因素是閉門造車,大家都不知道對方做了什麼、怎麼做的,很常見的問題就是重複開發,建議團隊指定間隔多久進行一次討論,大家分享自己最近做了什麼或者遇到了什麼困難,或許自己的困惑其他人之前也遇到過並且有很好的解決方案。大家也可以一起吐槽需求方不合理的需求,聽聽大家的觀點,說不定還能提高自己的業務思考能力。

既然選擇了領域驅動設計,那麼自然地要把自己融入到整個業務、整個項目中,把自己認定為項目中不可缺少的一部分,肩負了業務前進的重任。

嚴格的 Review

因為團隊中各成員的能力水平、對領域驅動設計的領悟程度不一致,在初期可能會寫出不規範的代碼或者結構,甚至出現錯誤的領域劃分,在合入分支前進行嚴格的 Code Review 是非常有必要的,領域驅動設計是非常不抗「腐蝕」的,不能接受不規範的代碼或結構,在初期的 Review 成本或許有些大,等成員之間認知統一後,後續便能愉快地一起寫代碼了~

總結

本篇文章我們以一個很有趣的現象開頭,結合有問題的代碼分析出前端開發過程中遇到的困難,接著提出了領域驅動設計,結合其實踐,逐一解決了之前遇到的困難,注意,上文實踐的領域驅動結構並不是完全按照 Evans 在《領域驅動設計》書中提出的結構,因為該書中的結構更適合後端的實踐,而在前端中,我們提取了書中部分優良的設計,與實際的前端開發場景進行結合而總結出上述結構,當然讀者可以對其結構進一步的改造、優化,也期待讀者與我進行交流,文中出現的錯誤歡迎指正。

作者簡介

華勁博,酷家樂前端工程師,花名景川,負責在團隊內推廣「領域驅動設計」在前端應用,聯繫email:artistcoder@163.com

福利抽獎送書

本書以基礎知識、示例、實戰案例相結合的方式詳盡講述了HTML&CSS&JavaScript及目前新的前端技術。主要包括HTML5的結構、文本、圖像、連結、表單、音頻、視頻、拖放、本地存儲、圖形,CSS3的文本設計、背景設計、DIV CS布局、盒布局、多列布局、自適應布局、動畫、漸變,還有JavaScript的語法、對象、BOM、DOM、事件響應等;*後兩個兩個完整案例綜合前面所學,讓讀者對網站設計與網頁開發有個整體的認識。本書運用大量示例,讓讀者在實戰中體會編程的快樂。建議讀者邊學邊練,有難以理解的概念或知識一定要弄清楚,不能迷迷糊糊。要培養自己單獨開發項目的能力。本書適合想從事網頁和前端開發的入門人員、網站建設自學者和網絡管理技術人員閱讀。

可以掃碼查看圖書詳情

相關焦點

  • 「乾貨貼」Nuxt Apps中的領域驅動設計實踐
    我在Vue Storefront和Vue Storefront Next中實驗 Vue apps 的領域驅動方法有一段時間了。使用這種方法能顯著改善你的代碼庫的可維護性和複雜度。在這個系列,我想分享一些在 Vue Storefront 中對我們有效,且容易應用到任何 Vue 應用程式中的模式。
  • Vaughn Vernon 談微服務和領域驅動設計
    雖然單體應用程式也可以實現相當好地建模,但它們常常會演變成一團亂麻。究其原因是單體應用程式內的多個領域模型錯綜複雜地交織在一起。
  • [幻燈]剔除「偽創新」和「無領域」的領域驅動設計-2月
    很多關於「領域驅動設計」的文章、書籍和課程,包括國外和國內,存在兩個問題:(1)偽創新(2)無領域。
  • 女生在面對學習前端開發、UI設計和測試三個領域時該如何選擇
    首先,前端開發、UI設計和軟體測試都是女生通常比較感興趣的崗位,這些崗位本身雖然具有一定的聯繫,但是區別也是比較明顯的,需要組織的知識結構也完全不同,初學者可以根據自身的興趣愛好和能力特點來進行選擇。從當前的行業發展趨勢和人才需求量兩方面來看,當前可以重點考慮一下前端開發崗位,在學習前端開發知識的過程中,除了傳統的Web前端知識(HTML、CSS、JavaScript等)之外,還需要注重以下幾方面知識的學習:第一:移動端開發知識。
  • 業務變化不息 架構演進不止 第四屆領域驅動設計峰會線上開啟
    同時,民航信息技術總監張逸、IBM資深應用架構師於靜、《中臺架構與實現:基於DDD和微服務》作者歐創新等國內持續實踐領域驅動設計(DDD)的代表和思想領袖也分享了他們在各個不同場景下對於使用領域驅動設計的感悟和總結,展現了在後疫情時代領域驅動設計將會出現的新變化。
  • 業務變化不息,架構演進不止 第四屆領域驅動設計峰會線上開啟
    領域驅動設計峰會(DDD Conference)是由國內領域驅動設計(DDD)思想和實踐的領軍者——ThoughtWorks的架構諮詢師們組織發起,希望為國內的領域驅動設計(DDD) 實踐者們提供一個互相交流、分享自己團隊的成功經驗的平臺,使得領域驅動設計(DDD)的架構思想能夠在國內被更多人所認知,從而形成更大的規模效應
  • 物聯網技術在生態環境領域中的應用有哪些
    打開APP 物聯網技術在生態環境領域中的應用有哪些 發表於 2019-05-29 15:15:03 隨著5G逐漸商用,其所具備的高帶寬、低時延和大連接的特點,將進一步促進生態環境領域各類傳感器技術進步與擴大應用範圍,更好支撐雲端智能化應用,從而進一步驅動『智能+』產業的發展與應用。」柳絮說。 目前,物聯網技術在生態環境領域應用最廣泛、最深入,主要應用於環境監控,包括汙染源自動監控、環境質量在線監測和環境衛星遙感3方面。
  • 開課吧開啟雙十二教育節:以實戰驅動的Web前端課程
    隨著網際網路的快速發展,Web前端人員的需求量越來越大。前端開發也由此逐漸成為了一個不可缺少的專業角色。作為數位化人才在線教育平臺,開課吧帶領名師團隊研發了豐富的課程體系。
  • 新能源商用車驅動方案及電驅動橋的應用
    本文介紹了電動汽車的驅動方案,闡述了電驅動系統的布置方式,列舉了幾種典型的電驅動橋在商用車領域的應用實例。目前新能源汽車常用的電驅動橋主要有:集成電驅動橋、同軸電驅動橋和輪邊電驅動橋等。本文主要從電動汽車的驅動方案、電驅動橋的技術優勢等方面來介紹,並分享幾種典型的電驅動橋在商用車領域的應用實例。
  • 明微電子:專注集成電路設計 力爭成為全球LED驅動IC領域領軍企業
    公司圍繞集成電路設計領域持續不斷加大投入,擁有了一個省級工程技術中心、一個省部級產學研基地和一個市級工程實驗室。我們的主要產品廣泛應用於LED顯示、LED照明和家用電器等領域。  明微電子團隊將繼續深耕集成電路設計領域,鞏固並充分發揮在驅動IC領域較強的研發及技術優勢和豐富的智慧財產權積累,推動公司高質量、高效率發展,在封裝測試環節實現自我提升和品質把控,在技術前沿和尖端領域取得突破性成果,力爭將公司打造為全球驅動IC領域的領軍企業,用較高的產品附加值,以及穩定的盈利能力來回饋投資者的信賴與支持。
  • 《設計》專訪|戴端:設計與科技在融合再造中驅動教育新格局
    將課堂教學、創新實踐等內容有機結合,引導學生更加開放的了解和應用科技手段解決實際問題,在教學過程、設計實踐中有效的嘗試了設計與科技融合的創新探索。科技創新驅動戰略背景下,設計走向了一個更加開放、系統、智能的文化環境中,設計教育尤其強調其跨學科、跨領域、跨文化的課程管理特色,並在整體運作的鏈式驅動中進行設計與科技的深度融合與再造。
  • 使電機驅動設計簡單
    設計人員面臨不斷縮短的開發周期,同時也面臨著創新的挑戰,這使得選擇合適的電機控制系統至關重要。據Grand View Research 1,到2025年,全球電機市場將達到1,550億美元。有幾個領域正在推動這一擴張,包括製造業內的自動化。電機、執行器及其控制器是這些系統的關鍵器件。所有大車廠都專注於一個不斷擴大的電機市場領域,推進電動汽車(EV)的發展。
  • 網頁前端設計快速入門技巧
    今天我就跟大家說說前端,如何快速入門?網頁前端設計一千個人眼中就有一千個哈姆雷特,每個人對網頁前端的理解也是不一樣的。我認為網頁前端開發就像是網際網路的美容師,不僅給訪客帶來視覺上的美感,而且隨著網際網路的發展,Html5、CSS3的應用,前端工程師結合技術與藝術能把網站最好的界面呈現給用戶,這就是網頁前端!
  • 高亮度LED線性驅動晶片設計及典型應用方案分析
    高亮度LED具有發光強度大、發光效率高、節能環保、壽命長等優點而被廣泛應用於汽車照明、顯示器、相機閃光燈、面板背光源、景光照明和室內裝飾等領域。由於LED的發光亮度與其電流大小相關,而導通電壓、工作電壓以及環境溫度的微小變化又對LED電流有較大的影響,因此需要設計專門的驅動晶片維持LED陣列亮度的一致性。
  • 《HTML5網頁前端設計》的教與學
    拼圖遊戲的設計與實現第8章 HTML5 媒體APIHTML5音頻的應用HTML5視頻的應用HTML5媒體API的其他通用功能42上機實驗(任選其一):《HTML5網頁前端設計實戰》第6章:1. 音樂播放器的設計與實現2.
  • 寫給想成為前端工程師的同學們―前端工程師是做什麼的?
    隨著網際網路的發展,大約從2005年開始,正式的前端工程師角色被行業認可,到了2010年,網際網路開始全面進入移動時代,前端工程師的地位越來越重要,前端領域的技術發展也越來越快,各種新的思想、設計模式、工具和平臺都快速發展,對前端工程師的技能要求也越來越高。有一些數據可以說明前端行業的發展迅速。
  • 前端程式設計師必須掌握之三角函數在前端動畫中的應用
    ,提升前端技能)作者:HelKylehttps://juejin.im/post/5d99b706e51d4577f9285c33開發過程中經常有意無意地刻意避開數學相關的知識,你也知道解數學題非常枯燥無趣。
  • Windows CE.NET下ADC驅動開發設計
    多線性、多任務、全優先的作業系統環境是專門針對資源有限而設計的,它的模塊化設計使嵌入式系統開發者和應用者能夠將其應用於各種產品,例如家用電器、專門的工業控制和嵌入式通信設備等。Windows CE 支持各種硬體外圍設備及網絡系統,應用領域極為廣闊,是微軟專門為信息設備、移動通訊、電子產品、嵌入式應用等非 PC 領域而專門設計的一種戰略性作業系統產品。
  • 網站前端ICONFONT圖標助力網站設計之WordPress應用
    說到網站建設就不得不提到前端UI與後端程序談到UI就不得不提及美工與設計在當前這個網際網路開放的時代,提到前端美工設計不提及ICONFONT圖標的廣泛應用,那完全是設計界的盲人;什麼是ICONFONT?;【內容來源於:https://www.diebaosoft.com/】,未經授權,謝絕轉載【Font Awesome官網】:https://fontawesome.com/因裡面的庫文件太多,沒法一一列表,所以需要哪個自行查找;比如上方圖中,
  • 觸景無限斬獲最佳前端人臉抓拍方案多項AI大獎
    日前,觸景無限科技(北京)有限公司再次獲得媒體與業界肯定,斬獲雷鋒網2018 AI最佳掘金案例年度榜單中「AI+安防領域」最佳前端人臉抓拍方案獎。  連續舉辦兩屆的雷鋒網「AI 最佳掘金案例年度評選」,從商業維度出發,尋找人工智慧在汽車、金融、醫療、教育、安防等10個行業的58個最佳前沿應用。276家參選企業經過49天的篩選與評審,最終58個最具商業價值的公司入選榜單。  觸景無限憑藉在前端感知賽道的深耕榮膺大獎,實至名歸。