邏輯管理:解決方案(一) – 關於前端邏輯管理的設計和實現
- 2019 年 10 月 3 日
- 筆記
切入思考點
組件化,解決了一組可以復用的功能,我們可使用一般的開源的公共組件,也可以針對我們特殊業務場景,沉澱出符合自己業務的業務組件;
工程化,解決了可控和規範性的功能,我們可使用開源的一些腳手架比如vue-cli、create-react-app等,或者公司內部自己沉澱的內部腳手架解決方案;
但是誰來解決散落在各個模組和工程中的邏輯?怎樣去避免硬程式碼編程,減少邏輯的後期維護和成本等等,也是一個需要考慮的點。
觀察程式碼
首先可以從一個客觀角度去分析這份程式碼,review這份程式碼,可以看出很多問題,比如:
-
- 開頭的配置參數和類型檢查的配置,程式碼佔了很大篇幅,是否可以抽離到配置文件管理里去維護?
- tools工具類是否可以進行重構,一個tools聚合了很多不同類型的輔助方法,後期增長是否會持續臃腫,是否可以通過分類歸納,tools管理更清晰明了
- tools的內部工具,是否可以拆分成只做一件事和多件事共同完成一件事方式?
- 太長的函數,是否有拆分的可能,增強可讀性要求?
- 很多方法依賴自身對象的其他方法,整個鏈路的流轉複雜多變,牽一髮動全身。
- 程式碼能力劃分不明確,通用和非通用沒有明確界定
- 對外暴露能力的程式碼重複度比較高
- ……
當時最初寫這份程式碼還做過簡單的分類,有點邏輯管理的淺顯意識。但是我們可以看看我們自己真實用於生產的公司的項目,多人維護,協同開發、業務增長等,到最後已經完全不可控,邏輯動都不敢動,只敢打修補程式,越來越臃腫。下面就是我之前針對我們內部項目一小塊做的一塊分析,這些都真實存在幾乎所有人的程式碼里,是我們存在的痛點。
-
- 單獨時間處理函數,是否可以抽離到公用邏輯中,基於原型鏈的屬性,是否會污染和覆蓋原型鏈屬性等
- 業務交互設計功能,是否可以封裝到獨立函數中?
- 枚舉統一抽離管理?
- 請求抽離統一管理?
- 數據的轉換賦值處理?
- 複雜文案拼裝,抽象到函數中,提高可讀性?減輕複雜度?
- 多重邏輯判斷是否可簡化表達式?分解複雜條件,合併行為一致?
- ….
前端對業務做了什麼?
基於之前對程式碼的分析,堆積了很多問題,說明這塊確實是我們的痛點。那麼這些痛點歸根究底是我們做了什麼導致?前端對業務到底做了哪些方面的東西?
-
- 獲取業務數據(業務規則下的數據獲取)
- 數據處理(可細分轉換,格式化,校驗等等)
- 業務判斷(針對業務場景,每個場景下需要做什麼)
- 業務數據提交(業務規則產出的數據的記錄)
- 業務交互功能(在業務規則下,需要怎麼做,做怎樣的功能)
- 業務展示(在業務場景下,合理的show出業務的形態)
- ……(暫時只想到這些領域,如有遺漏歡迎補充)
以上,幾乎囊括了前端在業務領域,所需要做的所有事情,也是我們的所有的邏輯。
對邏輯的深入思考
我們需要這些邏輯的堆砌去完成我們需要的東西,其實觀察每一小塊業務程式碼,都是由一條條最簡單的邏輯規則,一步步流轉到最後我們所需要的結果的,就跟我們做的思維腦圖一樣,一個流程節點都是一個小邏輯。一個業務的開始,到一個業務的結束,都是由每個最小的邏輯點組成的。
so,我們能不能站在一個全局的角度去看整個業務,能不能把每個流程節點打碎成一個最小的原子,所有的業務邏輯,都是從最小的原子一個一個組裝起來的,這樣,我們就能更專註於最小的邏輯。我們所做的任何業務都是由原子拼起來。這樣就可以從基礎去hold住任何邏輯,不管複雜和簡單。
我們也可以參考,在Java或者其他後端語言里,設計最初是最理想。它們都希望,我的世界就和現實世界一樣,都是由最小的顆粒去組裝我想要的設計的世界。所以一個class代表了一類事情,一個function代表了一件事。無論你們上面怎麼玩,我都能支援你們去組裝你們要的世界,你們要做的任何複雜的事。所以,邏輯處理其實也是這樣的,把任何邏輯打成最小顆粒,通過拼接,組裝,去支撐上層的任何業務邏輯。
如此之後,設想如下場景:
-
- 只關心原子邏輯,去豐富原子邏輯
- 業務邏輯,在原子提供的邏輯上適應任何業務規則,通過組裝去產出任何業務程式碼
- 業務規則變化下,小變化,直接替換一個邏輯節點,替換插槽。大變化,重新組裝另一條業務線。
- 整個鏈路數據流轉清晰可追蹤
- …
理想設計架構圖
簡單摸索設計思路
原子邏輯:對象的基類,管理所有注入原子
組合邏輯:繼承原子,組合,輸出
對外介面:解析配置,調用原子和組合類管理、拋出生產結果
思路圖如下:
基類設計程式碼
// 原子管理類,管理所有原子邏輯 class Atom { /* * 注入原子邏輯,以屬性的方式管理 * objArr: 原子邏輯數組 * */ setBasics(objArr) { objArr.forEach(x => { this[x.name] = x.assembly }) } /* * 生產組裝類所需要的原子 * param * useBasics:組裝類,所需要繼承的原子 * 支援type: String - 指定一個、Array - 指定多個、無(undefined)- 所有 * * return * output:生產出的原子邏輯 * */ machiningBasics(useBasics) { let output = {} if (useBasics) { if (Array.isArray(useBasics)) { useBasics.forEach(x => { Object.assign(output, this[x]) }) } else { Object.assign(output, this[useBasics]) } } else { Object.keys(this).forEach(x => { Object.assign(output, this[x]) }) } return output } } export default Atom
基類,作為最底層的基礎模組,管理所有原子,供上層業務邏輯繼承和調用,去組裝自己的業務邏輯。該類內部拋出2個方法如下:
setBasics
作為對原子邏輯的注入。可以持續去豐富底層的原子邏輯(後期是否支援動態注入,再考慮);
machiningBasics
提供給組裝類繼承原子的邏輯,輸出所需要的底層基礎,供上游拼裝
組裝類設計程式碼
// 因ES6不支援私有屬性,所以將私有屬性放到外層 /* * 生產組裝對象,並注入指定作用域 * param - * * return * Temporary:組裝對象 * * */ function makeObject() { function Temporary(assembly) { for (let key in assembly) { this[key] = assembly[key].bind(this) } } return Temporary } /* * 組裝中是否透傳原子方法 * param * Temporary:組裝對象 * config: 組裝的配置 * * return * output:輸出最終邏輯 * */ function isThrough(Temporary, config) { // 根據配置,實例化對象 let temp = new Temporary(config.assembly) let output = {} for (let key in temp) { // 是否開啟配置 if (config.through === false) { // 是否是自身屬性 if (temp.hasOwnProperty(key)) { output[key] = temp[key] } } else { output[key] = temp[key] } } return output } // 組裝類,管理組裝和輸出。 class Package { /* * 注入組裝配置 * param * config:組裝配置 * prototype:組裝所依賴的原子屬性 * * return 生產完成的對象 * */ setPackage(config, prototype) { let temp = makeObject(config) temp.prototype = prototype return isThrough(temp, config) } } export default Package
組裝類,通過一系列的原子邏輯組裝成一條條所需要的業務邏輯。整體步驟為:生產出組裝的對象,通過原型繼承裝配原子,對外暴露組裝結果。就跟工廠一樣,生產目標,生產原料,生產產物。組裝類對內部拋出一個方法:
setPackage
根據提供的配置文件以及所需繼承的原子,組裝出一類業務邏輯。
index入口設計
import Atom from './atom/index' import Package from './package/index' // 實例化原子和組裝類 const _atom = new Atom() const _package = new Package() // 生產原子快取 let _globalCache = {} /* * 對外暴露,注入配置依賴,生產組裝 * param * param: 配置參數 * */ export const injection = function (param) { _atom.setBasics(param.atom) param.package.forEach(x => { let prototype = _atom.machiningBasics(x.extends) // 快取組裝 _globalCache[x.name] = _package.setPackage(x, prototype) }) } /* * 對外暴露,獲取生產完成的組裝對象 * param * param:獲取的目標 * type:String - 指定一個、Array - 指定多個、 無(undefined) - 全部 * * return * output:生產結束的對象 * */ export const getMateriel = function (param) { let output = {} if (param) { if (Array.isArray(param)) { return param.forEach(x => { output[x] = _globalCache[x] }) } else { output = _globalCache[param] } } else { output = _globalCache } return output }
對外的入口,主要功能為解析配置,組裝配置,輸出組裝結果供使用3大功能。
injection
標準對外入口,進行邏輯管理的初始化,該方法將所有的原子邏輯注入到原子類里,再通過組裝配置,從原子類獲取到每個組裝對象所需要繼承的原子供組裝使用,最後將組裝好的邏輯全局存到一個全局的快取里。
getMateriel
對外輸出生產完成的組裝邏輯,暴露出組裝結束的結果,可獲取所有組裝結果,也可單獨或者批量獲取結果
使用格式規定
默認注入配置(injection方法)
/* * injection方法注入對象的格式 * atom: 所有的原子邏輯 * package: 組裝原子的邏輯 */ { atom: ['原子邏輯1', '原子邏輯2'], package: ['組裝邏輯1', '組裝邏輯2'] }
原子邏輯文件格式
/* * 該格式為原子邏輯的標準格式 * name: 原子類的名稱 * assembly: 原子的方法存放的對象 */ export default { name: '原子的名稱', assembly: { // 原子邏輯所對外提供的方法 sendRequest() { // do something } } }
組裝邏輯文件格式
/* * 該格式為組裝邏輯的標準格式 * name: 組裝類的名稱 * extends: 組裝類需要繼承的原子 * through: 是否透傳原子類內部的資訊 * assembly: 原子的方法存放的對象 */ export default { name: '組裝類名稱', extends: '繼承原子', // 支援字元串(單原子)、無(默認繼承所有原子)、數組(指定多個原子) assembly: { // 組裝邏輯對外產出的方法,可直接this.來調用繼承原子的方法 getAtom1Promise() { // do something... } } }
DEMO展示
目錄格式
-src
|-atom // 存放原子邏輯的地方
|-package // 存放組裝邏輯的地方
|-index.js // 入口文件
原子邏輯(atom)
export default { name: 'atom1', assembly: { sendRequest() { return new Promise((res, rej) => { setTimeout(function () { res([1, 2, 3]) }, 3000) }) } } }
export default { name: 'atom2', assembly: { judgeArray(data) { return Array.isArray(data) } } }
組裝邏輯(package)
export default { name: 'package1', extends: 'atom1', assembly: { getAtom1Promise() { this.sendRequest() .then(x => { console.warn('使用成功', x) }) } } }
export default { name: 'package2', through: false, assembly: { packageLogin() { this.sendRequest() .then(x => { console.warn('判斷是否是數組:', this.judgeArray(x)) }) } } }
入口注入(index)
import {injection, getMateriel} from '@fines/factory-js' import atom1 from './atom/atom1' import atom2 from './atom/atom2' import package1 from './package/package1' import package2 from './package/package2' injection({ atom: [atom1, atom2], package: [package1, package2] }) console.warn('組裝成功:', getMateriel()) // 測試package1方法 getMateriel('package1').getAtom1Promise() // 測試package2方法 getMateriel('package2').packageLogin()
測試結果
npm發布
包名
@fines/factory-js
安裝
npm i @fines/factory-js
註明
fines作為一個新的註冊的組織,這裡將寫一些更美好的東西,以後所有能變得更美好的程式碼都將發布到這個包下面(更重要一些包名已經無法使用,但是組織可以無限制)
github託管
地址
https://github.com/GerryIsWarrior/factory-js 感覺有參考意義可以點個star,內部正在使用踩坑中
Issues
https://github.com/GerryIsWarrior/factory-js/issues
demo地址
https://github.com/GerryIsWarrior/factory-js/tree/master/demo
PS:可直接clone下來 npm run start 直接跑起來測試
後記
以前在邏輯管理領域做過相關的摸索和思考,如下:
在之前的摸索基礎上,更深入的思考,才最終產出這個邏輯的解決方案,僅供大家參考,後面仍將持續完善該方案。
社區有人說,這不是你前端做的事,不是你的活,做這個幹啥?聽完這句話,總感覺有點彆扭。
在我看來,我們每個人都是一個架構師,不斷地在架構自己的程式碼。不停的去認知世界的樣子,認知自我。我們都不是最完美的,有好也有壞。去發現自身痛點,對痛點進行分析,進行思考,找出最終的根源,然後再去思考如何去解決這個痛點,嘗試,摸索,失敗,階段性勝利,再繼續。就這樣一路走來,堅信終有收穫。共勉!
下期方向
組裝原子如何和原子共存,共建上層輸出邏輯?
因為有些通過原子邏輯組成的通用方法,也可以作為基礎原子繼續使用的,如何注入管理作為下一期課題研究。