<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端領(lǐng)域的 “干凈架構(gòu)”

          共 19484字,需瀏覽 39分鐘

           ·

          2022-01-20 03:21

          大家好,我是 ConardLi,前端有架構(gòu)嗎?這可能是很多人心里的疑惑,因?yàn)樵趯?shí)際業(yè)務(wù)開發(fā)里我們很少為前端去設(shè)計(jì)標(biāo)準(zhǔn)規(guī)范的代碼架構(gòu),可能更多的去關(guān)注的是工程化、目錄層級(jí)、以及業(yè)務(wù)代碼的實(shí)現(xiàn)。

          今天我們來看一種前端架構(gòu)的模式,原作者稱它為“干凈架構(gòu)(Clean Architecture)”,文章很長,講的也很詳細(xì),我花了很長時(shí)間去讀完了它,看完很有收獲,翻譯給大家,文中也融入了很多我自己的思考,推薦大家看完。

          • https://dev.to/bespoyasov/clean-architecture-on-frontend-4311
          • 本文中示例的源碼:https://github.com/bespoyasov/frontend-clean-architecture/

          首先,我們會(huì)簡(jiǎn)單介紹一下什么是干凈架構(gòu)(Clean architecture),比如領(lǐng)域、用例和應(yīng)用層這些概念。然后就是怎么把干凈架構(gòu)應(yīng)用于前端,以及值不值得這么做。

          接下來,我們會(huì)用干凈架構(gòu)的原則來設(shè)計(jì)一個(gè)商店應(yīng)用,并從頭實(shí)現(xiàn)一下,看看它能不能運(yùn)行起來。

          這個(gè)應(yīng)用將使用 React 作為它的 UI 框架,這只是為了表明這種開發(fā)方式是可以和 React 一起使用的。你也可以選擇其他任何一種 UI 庫去實(shí)現(xiàn)它。

          代碼中會(huì)用到一些 TypeScript,這只是為了展示怎么使用類型和接口來描述實(shí)體。其實(shí)所有的代碼都可以不用 TypeScript 實(shí)現(xiàn),只是代碼不會(huì)看起來那么富有表現(xiàn)力。

          架構(gòu)和設(shè)計(jì)

          設(shè)計(jì)本質(zhì)上就是以一種可以將它們重新組合在一起的方式將事物拆開…… 將事物拆分成可以重新組合的事物,這就是設(shè)計(jì)。— Rich Hickey《設(shè)計(jì)、重構(gòu)和性能》

          系統(tǒng)設(shè)計(jì)其實(shí)就是系統(tǒng)的拆分,最重要的是我們可以在不耗費(fèi)太多時(shí)間的情況下重新把它們組起來。

          我同意上面這個(gè)觀點(diǎn),但我認(rèn)為系統(tǒng)架構(gòu)的另一個(gè)主要目標(biāo)是系統(tǒng)的可擴(kuò)展性。我們應(yīng)用的需求是不斷變化的。我們希望我們的程序可以非常易于更新和修改以滿足持續(xù)變化的新需求。干凈的架構(gòu)就可以幫助我們實(shí)現(xiàn)這一目標(biāo)。

          什么是干凈的架構(gòu)?

          干凈架構(gòu)是一種根據(jù)應(yīng)用程序的領(lǐng)域(domain)的相似程度來拆分職責(zé)和功能的方法。

          領(lǐng)域(domain)是由真實(shí)世界抽象而來的程序模型。可以反映現(xiàn)實(shí)世界和程序中數(shù)據(jù)的映射。比如,如果我們更新了一個(gè)產(chǎn)品的名稱,用新名稱來替換舊名稱就是領(lǐng)域轉(zhuǎn)換。

          干凈架構(gòu)的功能通常被分為三層,我們可以看下面這張圖:

          領(lǐng)域?qū)?/span>

          在在中心的是領(lǐng)域?qū)訉樱@里會(huì)描述應(yīng)用程序主題區(qū)域的實(shí)體和數(shù)據(jù),以及轉(zhuǎn)換該數(shù)據(jù)的代碼。領(lǐng)域是區(qū)分不同程序的核心。

          你可以把領(lǐng)域理解為當(dāng)我們從 React 遷移到 Angular,或者改變某些用例的時(shí)候不會(huì)變的那一部分。在商店這個(gè)應(yīng)用中,領(lǐng)域就是產(chǎn)品、訂單、用戶、購物車以及更新這些數(shù)據(jù)的方法。

          數(shù)據(jù)結(jié)構(gòu)和他們之間的轉(zhuǎn)化與外部世界是相互隔離的。外部的事件調(diào)用會(huì)觸發(fā)領(lǐng)域的轉(zhuǎn)換,但是并不會(huì)決定他們?nèi)绾芜\(yùn)行。

          比如:將商品添加到購物車的功能并不關(guān)心商品添加到購物車的方式:

          • 用戶自己通過點(diǎn)擊“購買”按鈕添加
          • 用戶使用了優(yōu)惠券自動(dòng)添加。

          在這兩種情況下,都會(huì)返回一個(gè)更新之后的購物車對(duì)象。

          應(yīng)用層

          圍在領(lǐng)域外面的是應(yīng)用層,這一層描述了用例。

          例如,“添加到購物車”這個(gè)場(chǎng)景就是一個(gè)用例。它描述了單擊按鈕后應(yīng)執(zhí)行的具體操作,像是一種“協(xié)調(diào)者”:

          • 向服務(wù)器發(fā)送一個(gè)請(qǐng)求;
          • 執(zhí)行領(lǐng)域轉(zhuǎn)換;
          • 使用響應(yīng)的數(shù)據(jù)更新 UI。

          此外,在應(yīng)用層中還有端口 — 它描述了應(yīng)用層如何和外部通信。通常一個(gè)端口就是一個(gè)接口(interface),一個(gè)行為契約。

          端口也可以被認(rèn)為是一個(gè)現(xiàn)實(shí)世界和應(yīng)用程序之間的“緩沖區(qū)”。輸入端口會(huì)告訴我們應(yīng)用要如何接受外部的輸入,同樣輸出端口會(huì)說明如何與外部通信做好準(zhǔn)備。

          適配器層

          最外層包含了外部服務(wù)的適配器,我們通過適配器來轉(zhuǎn)換外部服務(wù)的不兼容 API

          適配器可以降低我們的代碼和外部第三方服務(wù)的耦合,適配器一般分為:

          • 驅(qū)動(dòng)型 - 向我們的應(yīng)用發(fā)消息;
          • 被動(dòng)型 - 接受我們的應(yīng)用所發(fā)送的消息。

          一般用戶最常和驅(qū)動(dòng)型適配器進(jìn)行交互,例如,處理UI框架發(fā)送的點(diǎn)擊事件就是一個(gè)驅(qū)動(dòng)型適配器。它與瀏覽器 API 一起將事件轉(zhuǎn)換為我們的應(yīng)用程序可以理解的信號(hào)。

          驅(qū)動(dòng)型會(huì)和我們的基礎(chǔ)設(shè)施交互。在前端,大部分的基礎(chǔ)設(shè)施就是后端服務(wù)器,但有時(shí)我們也可能會(huì)直接與其他的一些服務(wù)交互,例如搜索引擎。

          注意,離中心越遠(yuǎn),代碼的功能就越 “面向服務(wù)”,離應(yīng)用的領(lǐng)域就越遠(yuǎn),這在后面我們要決定一個(gè)模塊是哪一層的時(shí)候是非常重要的。

          依賴規(guī)則

          三層架構(gòu)有一個(gè)依賴規(guī)則:只有外層可以依賴內(nèi)層。這意味著:

          • 領(lǐng)域必須獨(dú)立
          • 應(yīng)用層可以依賴領(lǐng)域
          • 最外層可以依賴任何東西

          當(dāng)然有些特殊的情況可能會(huì)違反這個(gè)規(guī)則,但最好不要濫用它。例如,在領(lǐng)域中也有可能會(huì)用到一些第三方庫,即使不應(yīng)該存在這樣的依賴關(guān)系。下面看代碼時(shí)會(huì)有這樣一個(gè)例子。

          不控制依賴方向的代碼可能會(huì)變得非常復(fù)雜和難以維護(hù)。比如:

          • 循環(huán)依賴,模塊 A 依賴于 B,B 依賴于 C,C 依賴于 A。
          • 可測(cè)試性差,即使測(cè)試一小塊功能也不得不模擬整個(gè)系統(tǒng)。
          • 耦合度太高,因此模塊之間的交互會(huì)很脆弱。

          干凈架構(gòu)的優(yōu)勢(shì)

          獨(dú)立領(lǐng)域

          所有應(yīng)用的核心功能都被拆分并統(tǒng)一維護(hù)在一個(gè)地方—領(lǐng)域

          領(lǐng)域中的功能是獨(dú)立的,這意味著它更容易測(cè)試。模塊的依賴越少,測(cè)試所需的基礎(chǔ)設(shè)施就越少。

          獨(dú)立的領(lǐng)域也更容易根據(jù)業(yè)務(wù)的期望進(jìn)行測(cè)試。這有助于讓新手理解起來更容易。此外,獨(dú)立的域也讓從需求到代碼實(shí)現(xiàn)中出現(xiàn)的錯(cuò)誤更容易排除。

          獨(dú)立用例

          應(yīng)用的使用場(chǎng)景和用例都是獨(dú)立描述的。它決定了我們所需要哪些第三方服務(wù)。我們讓外部服務(wù)更適應(yīng)我們的需求,這讓我們有更多的空間可以選擇合適的第三方服務(wù)。比如,現(xiàn)在我們調(diào)用的支付系統(tǒng)漲價(jià)了,我們可以很快的換掉它。

          用例的代碼也是扁平的,并且容易測(cè)試,擴(kuò)展性強(qiáng)。我們會(huì)在后面的示例中看到這一點(diǎn)。

          可替換的第三方服務(wù)

          適配器讓外部第三方服務(wù)更容易替換。只要我們不改接口,那么實(shí)現(xiàn)這個(gè)接口的是哪個(gè)第三方服務(wù)都沒關(guān)系。

          這樣如果其他人改動(dòng)了代碼,不會(huì)直接影響我們。適配器也會(huì)減少應(yīng)用運(yùn)行時(shí)錯(cuò)誤的傳播。

          實(shí)現(xiàn)干凈架構(gòu)的成本

          架構(gòu)首先是一種工具。像任何其他工具一樣,干凈的架構(gòu)除了好處之外還會(huì)帶來額外的成本。

          需要更多時(shí)間

          首先是時(shí)間,設(shè)計(jì)、實(shí)現(xiàn)都需要更多的時(shí)間,因?yàn)橹苯诱{(diào)用第三方服務(wù)總是比寫適配器簡(jiǎn)單。

          我們很難在一開始就把模塊所有的交互和需求都想的很明白,我們?cè)O(shè)計(jì)的時(shí)候需要時(shí)刻留意哪些地方可能發(fā)生變化,所以要考慮更多的可擴(kuò)展性。

          有時(shí)會(huì)顯得多余

          一般來說,干凈架構(gòu)并不適用于所有場(chǎng)景、甚至有的時(shí)候是有害的。如果本身就是一個(gè)很小的項(xiàng)目,你還要按照干凈架構(gòu)進(jìn)行設(shè)計(jì),這會(huì)大大增加上手門檻。

          上手更困難

          完全按照干凈架構(gòu)進(jìn)行設(shè)計(jì)和實(shí)現(xiàn)會(huì)讓新手上手更加困難,因?yàn)樗紫纫私馇宄?yīng)用是怎么運(yùn)行起來的。

          代碼量增加

          這是前端會(huì)特有的一個(gè)問題,干凈架構(gòu)會(huì)增加最終打包的產(chǎn)物體積。產(chǎn)物越大,瀏覽器下載和解釋的時(shí)間越長,所以代碼量一定要把控好,適當(dāng)刪減代碼:

          • 將用例描述的得更簡(jiǎn)單一些;
          • 直接從適配器和領(lǐng)域交互,繞過用例;
          • 進(jìn)行代碼拆分

          如何降低這些成本

          你可以通過適當(dāng)?shù)耐倒p料和犧牲架構(gòu)的“干凈度”來減少一些實(shí)現(xiàn)時(shí)間和代碼量。如果舍棄一些東西會(huì)獲得更大的收益,我會(huì)毫不猶豫的去做。

          所以,不必在所有方面走遵守干凈架構(gòu)的設(shè)計(jì)準(zhǔn)則,把核心準(zhǔn)則遵守好即可。

          抽象領(lǐng)域

          對(duì)領(lǐng)域的抽象可以幫助我們理解整體的設(shè)計(jì),以及它們是怎么工作的,同時(shí)也會(huì)讓其他開發(fā)人員更容易理解程序、實(shí)體以及它們之間的關(guān)系。

          即使我們直接跳過其他層,抽象的領(lǐng)域也更加容易重構(gòu)。因?yàn)樗鼈兊拇a是集中封裝在一個(gè)地方的,其他層需要的時(shí)候可以方便添加。

          遵守依賴規(guī)則

          第二條不應(yīng)該放棄的規(guī)則是依賴規(guī)則,或者說是它們的依賴方向。外部的服務(wù)需要適配內(nèi)部,而不是反方向的。

          如果你嘗試直接去調(diào)用一個(gè)外部 API,這就是有問題的,最好在還沒出問題之前寫個(gè)適配器。

          商店應(yīng)用的設(shè)計(jì)

          說完了理論,我們就可以開始實(shí)踐了,下面我們來實(shí)際設(shè)計(jì)一個(gè)商店應(yīng)用的。

          商店會(huì)出售不同種類的餅干,用戶可以自己選擇要購買的餅干,并通過三方支付服務(wù)進(jìn)行付款。

          用戶可以在首頁看到所有餅干,但是只有登錄后才能購買,點(diǎn)擊登錄按鈕可以跳轉(zhuǎn)到登錄頁。

          登錄成功后,用戶就可以把餅干加進(jìn)購物車了。

          把餅干加進(jìn)購物車后,用戶就可以付款了。付款后,購物車會(huì)清空,并產(chǎn)生一個(gè)新的訂單。

          首先,我們來對(duì)實(shí)體、用例和功能進(jìn)行定義,并對(duì)它們進(jìn)行分層。

          設(shè)計(jì)領(lǐng)域

          程序設(shè)計(jì)中最重要的就是領(lǐng)域設(shè)計(jì),它們表示了實(shí)體到數(shù)據(jù)的轉(zhuǎn)換。

          商店的領(lǐng)域可能包括:

          • 每個(gè)實(shí)體的數(shù)據(jù)類型:用戶、餅干、購物車和訂單;
          • 如果你是用OOP(面向?qū)ο笏枷耄?shí)現(xiàn)的,那么也要設(shè)計(jì)生成實(shí)體的工廠和類;
          • 數(shù)據(jù)轉(zhuǎn)換的函數(shù)。

          領(lǐng)域中的轉(zhuǎn)換方法應(yīng)該只依賴于領(lǐng)域的規(guī)則,而不依賴于其他任何東西。比如方法應(yīng)該是這樣的:

          • 計(jì)算總價(jià)的方法
          • 檢測(cè)用戶口味的方法
          • 檢測(cè)商品是否在購物車的方法

          設(shè)計(jì)應(yīng)用層

          應(yīng)用層包含用例,一個(gè)用包含一個(gè)參與者、一個(gè)動(dòng)作和一個(gè)結(jié)果。

          在商店應(yīng)用里,我們可以這樣區(qū)分:

          • 一個(gè)產(chǎn)品購買場(chǎng)景;
          • 支付,調(diào)用第三方支付系統(tǒng);
          • 與產(chǎn)品和訂單的交互:更新、查詢;
          • 根據(jù)角色訪問不同頁面。

          我們一般都是用主題領(lǐng)域來描述用例,比如“購買”包括下面的步驟:

          • 從購物車中查詢商品并創(chuàng)建新訂單;
          • 創(chuàng)建支付訂單;
          • 支付失敗時(shí)通知用戶;
          • 支付成功,清空購物車,顯示訂單。

          用例方法就是描述這個(gè)場(chǎng)景的代碼。

          此外,在應(yīng)用層中還有端口—用于與外界通信的接口。

          設(shè)計(jì)適配器層

          在適配器層,我們?yōu)橥獠糠?wù)聲明適配器。適配器可以為我們的系統(tǒng)兼容各種不兼容的外部服務(wù)。

          在前端,適配器一般是UI框架和對(duì)后端的API請(qǐng)求模塊。比如在我們的商店程序中會(huì)用到:

          • 用戶界面;
          • API請(qǐng)求模塊;
          • 本地存儲(chǔ)的適配器;
          • API返回到應(yīng)用層的適配器。

          對(duì)比 MVC 架構(gòu)

          有時(shí)我們很難判斷某些數(shù)據(jù)屬于哪一層,這里可以和 MVC 架構(gòu)做個(gè)小對(duì)比:

          • Model 一般都是領(lǐng)域?qū)嶓w
          • Controller 一般是與轉(zhuǎn)換或者應(yīng)用層
          • View 是驅(qū)動(dòng)適配器

          這些概念雖然在細(xì)節(jié)上不太相同,但是非常相似。

          實(shí)現(xiàn)細(xì)節(jié)—領(lǐng)域

          一旦我們確定了我們需要哪些實(shí)體,我們就可以開始定義它們的行為了,下面就是我們項(xiàng)目的目錄結(jié)構(gòu):

          src/
          |_domain/
          ??|_user.ts
          ??|_product.ts
          ??|_order.ts
          ??|_cart.ts
          |_application/
          ??|_addToCart.ts
          ??|_authenticate.ts
          ??|_orderProducts.ts
          ??|_ports.ts
          |_services/
          ??|_authAdapter.ts
          ??|_notificationAdapter.ts
          ??|_paymentAdapter.ts
          ??|_storageAdapter.ts
          ??|_api.ts
          ??|_store.tsx
          |_lib/
          |_ui/

          領(lǐng)域都定義在 domain 目錄下,應(yīng)用層定義在 application 目錄下,適配器都定義在 service 目錄下。最后我們還會(huì)討論目錄結(jié)構(gòu)是否會(huì)有其他的替代方案。

          創(chuàng)建領(lǐng)域?qū)嶓w

          我們?cè)陬I(lǐng)域中有 4 個(gè)實(shí)體:

          • product(產(chǎn)品)
          • user(用戶)
          • order(訂單)
          • cart(購物車)

          其中最重要的就是 user,在回話中,我們會(huì)把用戶信息存起來,所以我們單獨(dú)在領(lǐng)域中設(shè)計(jì)一個(gè)用戶類型,用戶類型包括以下數(shù)據(jù):

          //?domain/user.ts

          export?type?UserName?=?string;
          export?type?User?=?{
          ??id:?UniqueId;
          ??name:?UserName;
          ??email:?Email;
          ??preferences:?Ingredient[];
          ??allergies:?Ingredient[];
          };

          用戶可以把餅干放進(jìn)購物車,我們也給購物車和餅干加上類型。

          //?domain/product.ts

          export?type?ProductTitle?=?string;
          export?type?Product?=?{
          ??id:?UniqueId;
          ??title:?ProductTitle;
          ??price:?PriceCents;
          ??toppings:?Ingredient[];
          };


          //?domain/cart.ts

          import?{?Product?}?from?"./product";

          export?type?Cart?=?{
          ??products:?Product[];
          };

          付款成功后,將創(chuàng)建一個(gè)新訂單,我們?cè)賮硖砑右粋€(gè)訂單實(shí)體類型。

          //?domain/order.ts??—?ConardLi

          export?type?OrderStatus?=?"new"?|?"delivery"?|?"completed";

          export?type?Order?=?{
          ??user:?UniqueId;
          ??cart:?Cart;
          ??created:?DateTimeString;
          ??status:?OrderStatus;
          ??total:?PriceCents;
          };

          理解實(shí)體之間的關(guān)系

          以這種方式設(shè)計(jì)實(shí)體類型的好處是我們可以檢查它們的關(guān)系圖是否和符合實(shí)際情況:

          我們可以檢查以下幾點(diǎn):

          • 參與者是否是一個(gè)用戶
          • 訂單里是否有足夠的信息
          • 有些實(shí)體是否需要擴(kuò)展
          • 在未來是否有足夠的可擴(kuò)展性

          此外,在這個(gè)階段,類型可以幫助識(shí)別實(shí)體之間的兼容性和調(diào)用方向的錯(cuò)誤。

          如果一切都符合我們預(yù)期的,我們就可以開始設(shè)計(jì)領(lǐng)域轉(zhuǎn)換了。

          創(chuàng)建數(shù)據(jù)轉(zhuǎn)換

          我們剛剛設(shè)計(jì)的這些類型數(shù)據(jù)會(huì)發(fā)生各種各樣的事情。我們可以添加商品到購物車、清空購物車、更新商品和用戶名等。下面我們分別來為這些數(shù)據(jù)轉(zhuǎn)換創(chuàng)建對(duì)應(yīng)的函數(shù):

          比如,為了判斷某個(gè)用戶是喜歡還是厭惡某個(gè)口味,我們可以創(chuàng)建兩個(gè)函數(shù):

          //?domain/user.ts

          export?function?hasAllergy(user:?User,?ingredient:?Ingredient):?boolean?{
          ??return?user.allergies.includes(ingredient);
          }

          export?function?hasPreference(user:?User,?ingredient:?Ingredient):?boolean?{
          ??return?user.preferences.includes(ingredient);
          }

          將商品添加到購物車并檢查商品是否在購物車中:

          //?domain/cart.ts??—?ConardLi

          export?function?addProduct(cart:?Cart,?product:?Product):?Cart?{
          ??return?{?...cart,?products:?[...cart.products,?product]?};
          }

          export?function?contains(cart:?Cart,?product:?Product):?boolean?{
          ??return?cart.products.some(({?id?})?=>?id?===?product.id);
          }

          下面是計(jì)算總價(jià)(如果需要的話我們還可以設(shè)計(jì)更多的功能,比如配打折、優(yōu)惠券等場(chǎng)景):

          //?domain/product.ts

          export?function?totalPrice(products:?Product[]):?PriceCents?{
          ??return?products.reduce((total,?{?price?})?=>?total?+?price,?0);
          }

          創(chuàng)建新訂單,并和對(duì)應(yīng)用戶以及他的購物車建立關(guān)聯(lián)。

          //?domain/order.ts

          export?function?createOrder(user:?User,?cart:?Cart):?Order?{
          ??return?{
          ????user:?user.id,
          ????cart,
          ????created:?new?Date().toISOString(),
          ????status:?"new",
          ????total:?totalPrice(products),
          ??};
          }

          詳細(xì)設(shè)計(jì)—共享內(nèi)核

          你可能已經(jīng)注意到我們?cè)诿枋鲱I(lǐng)域類型的時(shí)候使用的一些類型。例如 EmailUniqueIdDateTimeString 。這些其實(shí)都是類型別名:

          //?shared-kernel.d.ts

          type?Email?=?string;
          type?UniqueId?=?string;
          type?DateTimeString?=?string;
          type?PriceCents?=?number;

          我用 DateTimeString 代替 string 來更清晰的表明這個(gè)字符串是用來做什么的。這些類型越貼近實(shí)際,就更容易排查問題。

          這些類型都定義在 shared-kernel.d.ts 文件里。共享內(nèi)核指的是一些代碼和數(shù)據(jù),對(duì)他們的依賴不會(huì)增加模塊之間的耦合度。

          在實(shí)踐中,共享內(nèi)核可以這樣解釋:我們用到 TypeScript,使用它的標(biāo)準(zhǔn)類型庫,但我們不會(huì)把它們看作是一個(gè)依賴項(xiàng)。這是因?yàn)槭褂盟鼈兊哪K互相不會(huì)產(chǎn)生影響并且可以保持解耦。

          并不是所有代碼都可以被看作是共享內(nèi)核,最主要的原則是這樣的代碼必須和系統(tǒng)處處都是兼容的。如果程序的一部分是用 TypeScript 編寫的,而另一部分是用另一種語言編寫的,共享核心只可以包含兩種語言都可以工作的部分。

          在我們的例子中,整個(gè)應(yīng)用程序都是用 TypeScript 編寫的,所以內(nèi)置類型的別名完全可以當(dāng)做共享內(nèi)核的一部分。這種全局都可用的類型不會(huì)增加模塊之間的耦合,并且在程序的任何部分都可以使用到。

          實(shí)現(xiàn)細(xì)節(jié)—應(yīng)用層

          我們已經(jīng)完成了領(lǐng)域的設(shè)計(jì),下面可以設(shè)計(jì)應(yīng)用層了。

          這一層會(huì)包含具體的用例設(shè)計(jì),比如一個(gè)用例是將商品添加到購物車并支付的完整過程。

          用例會(huì)涉及應(yīng)用和外部服務(wù)的交互,與外部服務(wù)的交互都是副作用。我們都知道調(diào)用或者調(diào)試沒有副作用的方法會(huì)更簡(jiǎn)單一些,所以大部分領(lǐng)域函數(shù)都實(shí)現(xiàn)為成純函數(shù)了。

          為了將無副作用的純函數(shù)和與有副作用的交互結(jié)合起來,我們可以將應(yīng)用層用作有副作用的非純上下文。

          非純上下文純數(shù)據(jù)轉(zhuǎn)換

          一個(gè)包含副作用的非純上下文和純數(shù)據(jù)轉(zhuǎn)換是這樣一種代碼組織方式:

          • 首先執(zhí)行一個(gè)副作用來獲取一些數(shù)據(jù);
          • 然后對(duì)數(shù)據(jù)執(zhí)行純函數(shù)進(jìn)行數(shù)據(jù)處理;
          • 最后再執(zhí)行一個(gè)副作用,存儲(chǔ)或傳遞這個(gè)結(jié)果。

          比如,“將商品放入購物車”這個(gè)用例:

          • 首先,從數(shù)據(jù)庫里獲取購物車的狀態(tài);
          • 然后調(diào)用購物車更新函數(shù),把要添加的商品信息傳進(jìn)去;
          • 最后將更新的購物車保存到數(shù)據(jù)庫中。

          這個(gè)過程就像一個(gè)“三明治”:副作用、純函數(shù)、副作用。所有主要的邏輯處理都在調(diào)用純函數(shù)進(jìn)行數(shù)據(jù)轉(zhuǎn)換上,所有與外部的通信都隔離在一個(gè)命令式的外殼中。

          設(shè)計(jì)用例

          我們選擇結(jié)賬這個(gè)場(chǎng)景來做用例設(shè)計(jì),它更具代表性,因?yàn)樗钱惒降模視?huì)與很多第三方服務(wù)進(jìn)行交互。

          我們可以想一想,通過整個(gè)用例我們要表達(dá)什么。用戶的購物車?yán)镉幸恍╋灨桑?dāng)用戶點(diǎn)擊購買按鈕的時(shí)候:

          • 要?jiǎng)?chuàng)建一個(gè)新訂單;
          • 在第三方支付系統(tǒng)中支付;
          • 如果支付失敗,通知用戶;
          • 如果支付成功,將訂單保存在服務(wù)器上;
          • 在本地存儲(chǔ)保存訂單數(shù)據(jù),并在頁面上顯示;

          設(shè)計(jì)函數(shù)的時(shí)候,我們會(huì)把用戶和購物車都作為參數(shù),然后讓這個(gè)方法完成整個(gè)過程。

          type?OrderProducts?=?(user:?User,?cart:?Cart)?=>?Promise<void>;

          當(dāng)然,理想情況下,用例不應(yīng)該接收兩個(gè)單獨(dú)的參數(shù),而是接收一個(gè)封裝后的對(duì)象,為了精簡(jiǎn)代碼,我們先這樣處理。

          編寫應(yīng)用層的接口

          我們?cè)賮碜屑?xì)看看用例的步驟:訂單創(chuàng)建本身就是一個(gè)領(lǐng)域函數(shù),其他一切操作我們都要調(diào)用外部服務(wù)。

          我們要牢記,外部方法永遠(yuǎn)要適配我們的需求。所以,在應(yīng)用層,我們不僅要描述用例本身,也要定義調(diào)用外部服務(wù)的通信方式—端口。

          想一想我們可能會(huì)用到的服務(wù):

          • 第三方支付服務(wù);
          • 通知用戶事件和錯(cuò)誤的服務(wù);
          • 將數(shù)據(jù)保存到本地存儲(chǔ)的服務(wù)。

          注意,我們現(xiàn)在討論的是這些服務(wù)的 interface ,而不是它們的具體實(shí)現(xiàn)。在這個(gè)階段,描述必要的行為對(duì)我們來說很重要,因?yàn)檫@是我們?cè)诿枋鰣?chǎng)景時(shí)在應(yīng)用層所依賴的行為。

          如何實(shí)現(xiàn)現(xiàn)在不是重點(diǎn),我們可以在最后再考慮調(diào)用哪些外部服務(wù),這樣代碼才能盡量保證低耦合。

          另外還要注意,我們按功能拆分接口。與支付相關(guān)的一切都在同一個(gè)模塊中,與存儲(chǔ)相關(guān)的都在另一個(gè)模塊中。這樣更容易確保不的同第三方服務(wù)的功能不會(huì)混在一起。

          支付系統(tǒng)接口

          我們這個(gè)商店應(yīng)用只是個(gè)小 Demo,所以支付系統(tǒng)會(huì)很簡(jiǎn)單。它會(huì)有一個(gè) tryPay 方法,這個(gè)方法將接受需要支付的金額,然后返回一個(gè)布爾值來表明支付的結(jié)果。

          //?application/ports.ts??—?ConardLi

          export?interface?PaymentService?{
          ??tryPay(amount:?PriceCents):?Promise<boolean>;
          }

          一般來說,付款的處理是在服務(wù)端。但我們只是簡(jiǎn)單演示一下,所以在前端就直接處理了。后面我們也會(huì)調(diào)用一些簡(jiǎn)單的API,而不是直接和支付系統(tǒng)進(jìn)行通信。

          通知服務(wù)接口

          如果出現(xiàn)一些問題,我們必須通知到用戶。

          我們可以用各種不同的方式通知用戶。比如使用 UI,發(fā)送郵件,甚至可以讓用戶的手機(jī)振動(dòng)。

          一般來說,通知服務(wù)最好也抽象出來,這樣我們現(xiàn)在就不用考慮實(shí)現(xiàn)了。

          給用戶發(fā)送一條通知:

          //?application/ports.ts

          export?interface?NotificationService?{
          ??notify(message:?string):?void;
          }

          本地存儲(chǔ)接口

          我們會(huì)將新的訂單保存在本地的存儲(chǔ)庫中。

          這個(gè)存儲(chǔ)可以是任何東西:Redux、MobX、任何存儲(chǔ)都可以。存儲(chǔ)庫可以在不同實(shí)體上進(jìn)行拆分,也可以是整個(gè)應(yīng)用程序的數(shù)據(jù)都維護(hù)在一起。不過現(xiàn)在都不重要,因?yàn)檫@些都是實(shí)現(xiàn)細(xì)節(jié)。

          我習(xí)慣的做法是為每個(gè)實(shí)體都創(chuàng)建一個(gè)單獨(dú)的存儲(chǔ)接口:一個(gè)單獨(dú)的接口存儲(chǔ)用戶數(shù)據(jù),一個(gè)存儲(chǔ)購物車,一個(gè)存儲(chǔ)訂單:

          //?application/ports.ts????—?ConardLi

          export?interface?OrdersStorageService?{
          ??orders:?Order[];
          ??updateOrders(orders:?Order[]):?void;
          }

          用例方法

          下面我們來看看能不能用現(xiàn)有的領(lǐng)域方法和剛剛建的接口來構(gòu)建一個(gè)用例。腳本將包含如下步驟:

          • 驗(yàn)證數(shù)據(jù);
          • 創(chuàng)建訂單;
          • 支付訂單;
          • 通知問題;
          • 保存結(jié)果。

          首先,我們聲明出來我們要調(diào)用的服務(wù)的模塊。TypeScript 會(huì)提示我們沒有給出接口的實(shí)現(xiàn),先不要管他。

          //?application/orderProducts.ts?—?ConardLi

          const?payment:?PaymentService?=?{};
          const?notifier:?NotificationService?=?{};
          const?orderStorage:?OrdersStorageService?=?{};

          現(xiàn)在我們可以像使用真正的服務(wù)一樣使用這些模塊。我們可以訪問他們的字段,調(diào)用他們的方法。這在把用例轉(zhuǎn)換為代碼的時(shí)候非常有用。

          現(xiàn)在,我們創(chuàng)建一個(gè)名為 orderProducts 的方法來創(chuàng)建一個(gè)訂單:

          //?application/orderProducts.ts??—?ConardLi
          //...

          async?function?orderProducts(user:?User,?cart:?Cart)?{
          ??const?order?=?createOrder(user,?cart);
          }

          這里,我們把接口當(dāng)作是行為的約定。也就是說以模塊示例會(huì)真正執(zhí)行我們期望的操作:

          //?application/orderProducts.ts??—?ConardLi
          //...

          async?function?orderProducts(user:?User,?cart:?Cart)?{
          ??const?order?=?createOrder(user,?cart);

          ??//?Try?to?pay?for?the?order;
          ??//?Notify?the?user?if?something?is?wrong:
          ??const?paid?=?await?payment.tryPay(order.total);
          ??if?(!paid)?return?notifier.notify("Oops!???");

          ??//?Save?the?result?and?clear?the?cart:
          ??const?{?orders?}?=?orderStorage;
          ??orderStorage.updateOrders([...orders,?order]);
          ??cartStorage.emptyCart();
          }

          注意,用例不會(huì)直接調(diào)用第三方服務(wù)。它依賴于接口中描述的行為,所以只要接口保持不變,我們就不需要關(guān)心哪個(gè)模塊來實(shí)現(xiàn)它以及如何實(shí)現(xiàn)它,這樣的模塊就是可替換的。

          實(shí)現(xiàn)細(xì)節(jié)—適配器層

          我們已經(jīng)把用例“翻譯”成 TypeScript 了,現(xiàn)在我們來檢查一下現(xiàn)實(shí)是否符合我們的需求。

          通常情況下是不會(huì)的,所以我們要通過封裝適配器來調(diào)用第三方服務(wù)。

          添加UI和用例

          首先,第一個(gè)適配器就是一個(gè) UI 框架。它把瀏覽器的 API 與我們的應(yīng)用程序連接起來。在訂單創(chuàng)建的這個(gè)場(chǎng)景,就是“結(jié)帳”按鈕和點(diǎn)擊事件的處理方法,這里會(huì)調(diào)用具體用例的功能。

          //?ui/components/Buy.tsx??—?ConardLi

          export?function?Buy()?{
          ??//?Get?access?to?the?use?case?in?the?component:
          ??const?{?orderProducts?}?=?useOrderProducts();

          ??async?function?handleSubmit(e:?React.FormEvent)?{
          ????setLoading(true);
          ????e.preventDefault();

          ????//?Call?the?use?case?function:
          ????await?orderProducts(user!,?cart);
          ????setLoading(false);
          ??}

          ??return?(
          ????<section>
          ??????<h2>Checkouth2>

          ??????<form?onSubmit={handleSubmit}>{/*?...?*/}form>
          ????section>
          ??);
          }

          我們可以通過一個(gè) Hook 來封裝用例,建議把所有的服務(wù)都封裝到里面,最后返回用例的方法:

          //?application/orderProducts.ts??—?ConardLi

          export?function?useOrderProducts()?{
          ??const?notifier?=?useNotifier();
          ??const?payment?=?usePayment();
          ??const?orderStorage?=?useOrdersStorage();

          ??async?function?orderProducts(user:?User,?cookies:?Cookie[])?{
          ????//?…
          ??}

          ??return?{?orderProducts?};
          }

          我們使用 hook 來作為一個(gè)依賴注入。首先我們使用 useNotifier,usePayment,useOrdersStorage 這幾個(gè) hook 來獲取服務(wù)的實(shí)例,然后我們用函數(shù) useOrderProducts 創(chuàng)建一個(gè)閉包,讓他們可以在 orderProducts 函數(shù)中被調(diào)用。

          另外需要注意的是,用例函數(shù)和其他的代碼是分離的,這樣對(duì)測(cè)試更加友好。

          支付服務(wù)實(shí)現(xiàn)

          用例使用 PaymentService 接口,我們先來實(shí)現(xiàn)一下。

          對(duì)于付款操作,我們依然使用一個(gè)假的 API 。同樣的,我們現(xiàn)在還是沒必要編寫全部的服務(wù),我們可以之后再實(shí)現(xiàn),現(xiàn)在最重要的是實(shí)現(xiàn)指定的行為:

          //?services/paymentAdapter.ts??—?ConardLi

          import?{?fakeApi?}?from?"./api";
          import?{?PaymentService?}?from?"../application/ports";

          export?function?usePayment():?PaymentService?{
          ??return?{
          ????tryPay(amount:?PriceCents)?{
          ??????return?fakeApi(true);
          ????},
          ??};
          }

          fakeApi 這個(gè)函數(shù)會(huì)在 450 毫秒后觸發(fā)的超時(shí),模擬來自服務(wù)器的延遲響應(yīng),它返回我們傳入的參數(shù)。

          //?services/api.ts??—?ConardLi

          export?function?fakeApi<TResponse>(response:?TResponse):?Promise<TResponse>?{
          ??return?new?Promise((res)?=>?setTimeout(()?=>?res(response),?450));
          }

          通知服務(wù)實(shí)現(xiàn)

          我們就簡(jiǎn)單使用 alert 來實(shí)現(xiàn)通知,因?yàn)榇a是解耦的,以后再來重寫這個(gè)服務(wù)也不成問題。

          //?services/notificationAdapter.ts??—?ConardLi

          import?{?NotificationService?}?from?"../application/ports";

          export?function?useNotifier():?NotificationService?{
          ??return?{
          ????notify:?(message:?string)?=>?window.alert(message),
          ??};
          }

          本地存儲(chǔ)實(shí)現(xiàn)

          我們就通過 React.ContextHooks 來實(shí)現(xiàn)本地存儲(chǔ)。

          我們創(chuàng)建一個(gè)新的 context,然后把它傳給 provider,然后導(dǎo)出讓其他的模塊可以通過 Hooks 使用。

          //?store.tsx??—?ConardLi

          const?StoreContext?=?React.createContext({});
          export?const?useStore?=?()?=>?useContext(StoreContext);

          export?const?Provider:?React.FC?=?({?children?})?=>?{
          ??//?...Other?entities...
          ??const?[orders,?setOrders]?=?useState([]);

          ??const?value?=?{
          ????//?...
          ????orders,
          ????updateOrders:?setOrders,
          ??};

          ??return?(
          ????<StoreContext.Provider?value={value}>{children}StoreContext.Provider>
          ??);
          };

          我們可以給每一個(gè)功能點(diǎn)都實(shí)現(xiàn)一個(gè) Hook 。這樣我們就不會(huì)破壞服務(wù)接口和存儲(chǔ),至少在接口的角度來說他們是分離的。

          // services/storageAdapter.ts

          export function useOrdersStorage(): OrdersStorageService {
          return useStore();
          }

          此外,這種方法還可以使我們能夠?yàn)槊總€(gè)商店定制額外的優(yōu)化:創(chuàng)建選擇器、緩存等。

          驗(yàn)證數(shù)據(jù)流程圖

          現(xiàn)在讓我們驗(yàn)證一下用戶是怎么和應(yīng)用程序通信的。

          用戶與 UI 層交互,但是 UI 只能通過端口訪問服務(wù)接口。也就是說,我們可以隨時(shí)替換 UI。

          用例是在應(yīng)用層處理的,它可以準(zhǔn)確地告訴我們需要哪些外部服務(wù)。所有主要的邏輯和數(shù)據(jù)都封裝在領(lǐng)域中。

          所有外部服務(wù)都隱藏在基礎(chǔ)設(shè)施中,并且遵守我們的規(guī)范。如果我們需要更改發(fā)送消息的服務(wù),只需要修改發(fā)送消息服務(wù)的適配器。

          這樣的方案讓代碼更方便替換、更容易測(cè)試、擴(kuò)展性更強(qiáng),以適應(yīng)不斷變化的需求。

          有什么可以改進(jìn)的

          上面介紹的這些已經(jīng)可以讓你開始并初步了解干凈的架構(gòu)了,但是我想指出上面我為了讓示例更簡(jiǎn)單做的一些偷工減料的事情。

          讀完下面的內(nèi)容,大家可以理解 “沒有偷工減料”的干凈架構(gòu)是什么樣子的。

          使用對(duì)象而不是數(shù)字來表示價(jià)格

          你可能已經(jīng)注意到我用一個(gè)數(shù)字來描述價(jià)格,這不是一個(gè)好習(xí)慣。

          //?shared-kernel.d.ts

          type?PriceCents?=?number;

          數(shù)字只能表示數(shù)量,不能表示貨幣,沒有貨幣的價(jià)格是沒有意義的。理想情況下,價(jià)格應(yīng)該是具有兩個(gè)字段的對(duì)象:價(jià)值和貨幣。

          type?Currency?=?"RUB"?|?"USD"?|?"EUR"?|?"SEK";
          type?AmountCents?=?number;

          type?Price?=?{
          ??value:?AmountCents;
          ??currency:?Currency;
          };

          這樣就能解決存儲(chǔ)貨幣的問題了,并可以省去大量的存儲(chǔ)和處理貨幣的精力。在示例中我沒有這么做是為了讓這個(gè)例子盡量簡(jiǎn)單。在真實(shí)的情況里,價(jià)格的結(jié)構(gòu)定義會(huì)更加接近上面的寫法。

          另外,值得一提的是價(jià)格的單位,比如美元的最小單位是美分。以這種方式顯示價(jià)格讓我可以避免考慮浮點(diǎn)數(shù)計(jì)算的問題。

          按功能拆分代碼,而不是按層

          代碼可以 “按功能” 拆分到文件夾中,而不是“按層”,一塊功能就是下面餅圖的一部分。

          這種結(jié)構(gòu)更清晰,因?yàn)樗梢宰屇惴謩e部署不同的功能點(diǎn):

          注意跨組件使用

          如果我們正在討論將系統(tǒng)拆分為組件,就不得不考慮跨組件代碼使用的問題。我們?cè)賮砜纯磩?chuàng)建訂單的代碼:

          import?{?Product,?totalPrice?}?from?"./product";

          export?function?createOrder(user:?User,?cart:?Cart):?Order?{
          ??return?{
          ????user:?user.id,
          ????cart,
          ????created:?new?Date().toISOString(),
          ????status:?"new",
          ????total:?totalPrice(products),
          ??};
          }

          這個(gè)函數(shù)用到了從另一個(gè) Product 模塊引入的 totalPrice 方法。這樣使用本身沒有什么問題,但是如果我們要考慮把代碼拆分到獨(dú)立的功能的時(shí)候,我們不能直接訪問其他模塊的代碼。

          使用 ts-brand ,而不是類型別名

          在共享內(nèi)核的編寫中,我使用了類型別名。它們很容易實(shí)現(xiàn),但缺點(diǎn)是 TypeScript 沒有監(jiān)控并強(qiáng)制執(zhí)行它們的機(jī)制。

          這看起來也不是個(gè)問題:你是用 string 類型去替代 DateTimeString 也不會(huì)怎么樣,代碼還是會(huì)編譯成功。但是,這樣會(huì)讓代碼變得脆弱、可讀性也很差,因?yàn)檫@樣你可以用任意的字符串,導(dǎo)致錯(cuò)誤的可能性會(huì)增加。

          有一種方法可以讓 TypeScript 理解我們想要一個(gè)特定的類型 — ts-brandhttps://github.com/kourge/ts-brand)。它可以準(zhǔn)確的跟蹤類型的使用方式,但會(huì)使代碼更復(fù)雜一些。

          注意領(lǐng)域中可能的依賴

          接下來的問題是我們?cè)?createOrder 函數(shù)的領(lǐng)域中創(chuàng)建了一個(gè)日期:

          import?{?Product,?totalPrice?}?from?"./product";

          export?function?createOrder(user:?User,?cart:?Cart):?Order?{
          ??return?{
          ????user:?user.id,
          ????cart,

          ????//?Вот?эта?строка:
          ????created:?new?Date().toISOString(),

          ????status:?"new",
          ????total:?totalPrice(products),
          ??};
          }

          new Date().toISOString() 這樣的函數(shù)可能會(huì)被重復(fù)調(diào)用很多次,我們可以把它封裝到一個(gè) hleper 里面:

          //?lib/datetime.ts??—?ConardLi

          export?function?currentDatetime():?DateTimeString?{
          ??return?new?Date().toISOString();
          }

          然后在領(lǐng)域中調(diào)用它:

          //?domain/order.ts

          import?{?currentDatetime?}?from?"../lib/datetime";
          import?{?Product,?totalPrice?}?from?"./product";

          export?function?createOrder(user:?User,?cart:?Cart):?Order?{
          ??return?{
          ????user:?user.id,
          ????cart,
          ????created:?currentDatetime(),
          ????status:?"new",
          ????total:?totalPrice(products),
          ??};
          }

          但是領(lǐng)域的原則是不能依賴其他任何東西,所以 createOrder 函數(shù)最好是所有數(shù)據(jù)都從外面?zhèn)鬟M(jìn)來,日期可以作為最后一個(gè)參數(shù):

          //?domain/order.ts??—?ConardLi

          export?function?createOrder(
          ??user:?User,
          ??cart:?Cart,
          ??created:?DateTimeString
          ):?Order?
          {
          ??return?{
          ????user:?user.id,
          ????products,
          ????created,
          ????status:?"new",
          ????total:?totalPrice(products),
          ??};
          }

          這樣我們就不會(huì)破壞依賴規(guī)則,即使創(chuàng)建日期也需要依賴第三方庫:

          function?someUserCase()?{
          ??//?Use?the?`dateTimeSource`?adapter,
          ??//?to?get?the?current?date?in?the?desired?format:
          ??const?createdOn?=?dateTimeSource.currentDatetime();

          ??//?Pass?already?created?date?to?the?domain?function:
          ??createOrder(user,?cart,?createdOn);
          }

          這會(huì)讓領(lǐng)域保持獨(dú)立,也使測(cè)試更容易。

          在前面的示例中,我不這樣做有兩個(gè)原因:它會(huì)分散我們的重點(diǎn),如果它只使用語言本身的特性,我認(rèn)為依賴你自己的 Helper 沒有任何問題。這樣的 Helper 甚至可以被視為共享內(nèi)核,因?yàn)樗鼈冎粫?huì)減少代碼的重復(fù)度。

          注意購物車與訂單的關(guān)系

          在這個(gè)小例子中,Order 會(huì)包含 Cart, 因?yàn)橘徫镘囍槐硎?Product 列表:

          export?type?Cart?=?{
          ??products:?Product[];
          };

          export?type?Order?=?{
          ??user:?UniqueId;
          ??cart:?Cart;
          ??created:?DateTimeString;
          ??status:?OrderStatus;
          ??total:?PriceCents;
          };

          如果購物車有其他的和訂單沒有關(guān)聯(lián)的屬性,可能會(huì)出問題,所以直接用 ProductList 會(huì)更合理:

          type?ProductList?=?Product[];

          type?Cart?=?{
          ??products:?ProductList;
          };

          type?Order?=?{
          ??user:?UniqueId;
          ??products:?ProductList;
          ??created:?DateTimeString;
          ??status:?OrderStatus;
          ??total:?PriceCents;
          };

          讓用例更方便測(cè)試

          用例也有很多要討論的地方。比如,orderProducts 函數(shù)很難獨(dú)立于 React 來測(cè)試,這不太好。理想情況下,測(cè)試不應(yīng)該消耗太多的成本。

          問題的根本原因我們使用 Hooks 來實(shí)現(xiàn)了用例:

          //?application/orderProducts.ts??—?ConardLi

          export?function?useOrderProducts()?{
          ??const?notifier?=?useNotifier();
          ??const?payment?=?usePayment();
          ??const?orderStorage?=?useOrdersStorage();
          ??const?cartStorage?=?useCartStorage();

          ??async?function?orderProducts(user:?User,?cart:?Cart)?{
          ????const?order?=?createOrder(user,?cart);

          ????const?paid?=?await?payment.tryPay(order.total);
          ????if?(!paid)?return?notifier.notify("Oops!???");

          ????const?{?orders?}?=?orderStorage;
          ????orderStorage.updateOrders([...orders,?order]);
          ????cartStorage.emptyCart();
          ??}

          ??return?{?orderProducts?};
          }

          在規(guī)范的實(shí)現(xiàn)中,用例方法可以放在 Hooks 的外面,服務(wù)通過參數(shù)或者使用依賴注入傳入用例:

          type?Dependencies?=?{
          ??notifier?:?NotificationService;
          ??payment?:?PaymentService;
          ??orderStorage?:?OrderStorageService;
          };

          async?function?orderProducts(
          ??user:?User,
          ??cart:?Cart,
          ??dependencies:?Dependencies?=?defaultDependencies
          )?
          {
          ??const?{?notifier,?payment,?orderStorage?}?=?dependencies;

          ??//?...
          }

          然后 Hooks 的代碼就可以當(dāng)做一個(gè)適配器,只有用例會(huì)留在應(yīng)用層。orderProdeucts 方法很容易就可以被測(cè)試了。

          function?useOrderProducts()?{
          ??const?notifier?=?useNotifier();
          ??const?payment?=?usePayment();
          ??const?orderStorage?=?useOrdersStorage();

          ??return?(user:?User,?cart:?Cart)?=>
          ????orderProducts(user,?cart,?{
          ??????notifier,
          ??????payment,
          ??????orderStorage,
          ????});
          }

          配置自動(dòng)依賴注入

          在應(yīng)用層,我們是手動(dòng)將依賴注入服務(wù)的:

          export?function?useOrderProducts()?{
          ??//?Here?we?use?hooks?to?get?the?instances?of?each?service,
          ??//?which?will?be?used?inside?the?orderProducts?use?case:
          ??const?notifier?=?useNotifier();
          ??const?payment?=?usePayment();
          ??const?orderStorage?=?useOrdersStorage();
          ??const?cartStorage?=?useCartStorage();

          ??async?function?orderProducts(user:?User,?cart:?Cart)?{
          ????//?...Inside?the?use?case?we?use?those?services.
          ??}

          ??return?{?orderProducts?};
          }

          當(dāng)然還有更好的做法,依賴注入可以自動(dòng)完成。我們前面已經(jīng)通過最后一個(gè)參數(shù)實(shí)現(xiàn)了最簡(jiǎn)單的注入版本,下面可以進(jìn)一步配置自動(dòng)依賴注入。

          在這個(gè)特定的應(yīng)用程序中,我認(rèn)為設(shè)置依賴注入沒有多大意義。它會(huì)分散我們的注意力并使代碼過于復(fù)雜。在使用了 Reacthooks 的情況下,我們可以將它們用作“容器”,返回指定接口的實(shí)現(xiàn)。是的,雖然還是手動(dòng)實(shí)現(xiàn)的,但它不會(huì)增加上手門檻,并且對(duì)于新手開發(fā)人員來說閱讀速度更快。

          實(shí)際項(xiàng)目中的情況可能更復(fù)雜

          文章中的示例是經(jīng)過精簡(jiǎn)的而且需求也比較簡(jiǎn)單。很明顯,我們實(shí)際開發(fā)中比這個(gè)例子要復(fù)雜的多。所以我還想談?wù)剬?shí)際開發(fā)中使用干凈架構(gòu)時(shí)可能出現(xiàn)的常見問題。

          分支業(yè)務(wù)邏輯

          最重要的問題是我們對(duì)需求的實(shí)際場(chǎng)景研究不夠深入。想象一下,一家商店有一個(gè)產(chǎn)品、一個(gè)打折產(chǎn)品和一種已經(jīng)注銷的產(chǎn)品。我們?cè)趺礈?zhǔn)確描述這些實(shí)體?

          是不是應(yīng)該有一個(gè)可擴(kuò)展的“基礎(chǔ)”實(shí)體呢?這個(gè)實(shí)體究竟應(yīng)該怎么擴(kuò)展?應(yīng)該有額外的字段嗎?這些實(shí)體是否應(yīng)該互斥?

          可能有太多的問題和太多的答案,如果只是假設(shè),我們不可能考慮到所有的情況。

          具體解決方法還要視具體情況而定,我只能推薦幾個(gè)我的經(jīng)驗(yàn)。

          不建議使用繼承,即使它看起來可“擴(kuò)展”。

          復(fù)制粘貼的代碼并不一定都不好,有時(shí)候甚至能發(fā)揮更大的作用。創(chuàng)建兩個(gè)幾乎相同的實(shí)體,觀察它們?cè)诂F(xiàn)實(shí)中的行為。在某些時(shí)候,它們的行為可能區(qū)別很大,有時(shí)候也可能只有一兩個(gè)字段的區(qū)別。合并兩個(gè)非常相似的實(shí)體比寫大量的的檢查要容易很多。

          如果你一定要擴(kuò)展一些內(nèi)容的話。。

          記住協(xié)變、逆變和不變,這樣你就不會(huì)多出一些意想不到的工作。

          在不同的實(shí)體和可擴(kuò)展之間選擇,推薦使用類似于 BEM 中的塊和修飾符概念來幫助你思考,如果我在 BEM 的上下文中考慮它,它可以幫助我確定我是否有一個(gè)單獨(dú)的實(shí)體或代碼的“修飾符擴(kuò)展”。

          BEM - Block Element Modfier(塊元素編輯器)是一個(gè)很有用的方法,它可以幫助你創(chuàng)建出可以復(fù)用的前端組件和前端代碼。

          相互依賴的用例

          第二個(gè)問題是用例相關(guān)的,通過一個(gè)用例的事件觸發(fā)另一個(gè)用例。

          我知道并且對(duì)我有幫助的處理這個(gè)問題的唯一方法是將用例分解為更小的原子用例。它們將更容易組合在一起。

          通常,出現(xiàn)這個(gè)問題是編程中另外一個(gè)大問題的結(jié)果。這就是實(shí)體組合。

          最后

          在本文里,我們介紹了前端的“干凈架構(gòu)”。

          這不是一個(gè)黃金標(biāo)準(zhǔn),而是一個(gè)在很多的項(xiàng)目、規(guī)范和語言上積累的經(jīng)驗(yàn)匯總。

          我發(fā)現(xiàn)它是一種非常方便的方案,可以幫助你解耦你的代碼。讓層、模塊和服務(wù)盡量獨(dú)立。不僅可以獨(dú)立發(fā)布、部署,還可以讓你從一個(gè)項(xiàng)目遷移另一個(gè)項(xiàng)目的時(shí)候也更加容易。

          你理想下的前端架構(gòu)是什么樣的呢?

          往期推薦



          解密初、中、高級(jí)程序員的進(jìn)化之路(前端)


          程序員一定會(huì)有35歲危機(jī)嗎?


          近 20k Star的項(xiàng)目說不做就不做了,但總結(jié)的內(nèi)容值得借鑒


          但凡早知道這28個(gè)網(wǎng)站,都不至于學(xué)得那么不扎實(shí)





          如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)

          2. 歡迎加我微信「huab119」拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

            關(guān)注公眾號(hào)「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。



          點(diǎn)個(gè)在看支持我吧,轉(zhuǎn)發(fā)就更好了

          如果覺得這篇文章還不錯(cuò),來個(gè)【轉(zhuǎn)發(fā)、收藏、在看】三連吧,讓更多的人也看到~


          瀏覽 70
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  鸡巴视频αV | 美女操逼软件 | 影音先锋一起操 | 男女男精品网站 | 人人爽人人干 |