在開始本篇文章前,我給讀者們分享一個很考驗人性的有趣現象,在公司洗手間的洗漱臺旁邊,放置了一個垃圾桶,每次我洗完手,用紙巾擦乾手後,將其扔進垃圾桶,但是偶爾扔不準會扔到垃圾桶外面。
一般情況下,我會將其撿起,再放入垃圾桶,心裡想著:「不能破壞這麼幹淨的環境呀」。
但是,當垃圾桶周邊有很多別人沒扔進去的餐巾紙時,我就不會那麼願意將自己沒扔進去的餐巾紙再撿起來扔進去,想著:「反正都這麼邋遢了,多了一個也不會怎樣」。
萬惡的人心呀!
過了很久,我接手了一個老的項目,這個項目經過近十個人手迭代,傳到我這裡時,已經是非常混亂的狀態了,閱讀代碼時,發現了很多不合理的寫法與隱藏式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、事件響應等;*後兩個兩個完整案例綜合前面所學,讓讀者對網站設計與網頁開發有個整體的認識。本書運用大量示例,讓讀者在實戰中體會編程的快樂。建議讀者邊學邊練,有難以理解的概念或知識一定要弄清楚,不能迷迷糊糊。要培養自己單獨開發項目的能力。本書適合想從事網頁和前端開發的入門人員、網站建設自學者和網絡管理技術人員閱讀。
可以掃碼查看圖書詳情