av激情亚洲男人的天堂国语,日韩欧美精品一中文字幕,无码av一区二区三区无码,国产又色又爽又刺激的a片,国产又色又爽又刺激的a片

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

領(lǐng)域?qū)?

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

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

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

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

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

在這兩種情況下,都會返回一個更新之后的購物車對象。

應用層

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

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

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

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

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

適配器層

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

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

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

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

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

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

依賴規(guī)則

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

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

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

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

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

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

獨立領(lǐng)域

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

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

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

獨立用例

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

用例的代碼也是扁平的,并且容易測試,擴展性強。我們會在后面的示例中看到這一點。

可替換的第三方服務

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

這樣如果其他人改動了代碼,不會直接影響我們。適配器也會減少應用運行時錯誤的傳播。

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

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

需要更多時間

首先是時間,設(shè)計、實現(xiàn)都需要更多的時間,因為直接調(diào)用第三方服務總是比寫適配器簡單。

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

有時會顯得多余

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

上手更困難

完全按照干凈架構(gòu)進行設(shè)計和實現(xiàn)會讓新手上手更加困難,因為他首先要了解清楚應用是怎么運行起來的。

代碼量增加

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

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

如何降低這些成本

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

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

抽象領(lǐng)域

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

即使我們直接跳過其他層,抽象的領(lǐng)域也更加容易重構(gòu)。因為它們的代碼是集中封裝在一個地方的,其他層需要的時候可以方便添加。

遵守依賴規(guī)則

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

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

商店應用的設(shè)計

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

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

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

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

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

首先,我們來對實體、用例和功能進行定義,并對它們進行分層。

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

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

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

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

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

  • 計算總價的方法
  • 檢測用戶口味的方法
  •  檢測商品是否在購物車的方法

設(shè)計應用層

應用層包含用例,一個用包含一個參與者、一個動作和一個結(jié)果。

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

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

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

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

用例方法就是描述這個場景的代碼。

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

設(shè)計適配器層

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

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

  • 用戶界面;
  • API請求模塊;
  •  本地存儲的適配器;
  • API返回到應用層的適配器。

對比 MVC 架構(gòu)

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

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

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

實現(xiàn)細節(jié)—領(lǐ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 目錄下,應用層定義在 application 目錄下,適配器都定義在 service 目錄下。最后我們還會討論目錄結(jié)構(gòu)是否會有其他的替代方案。

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

我們在領(lǐng)域中有 4 個實體:

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

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

// domain/user.ts
export type UserName = string;
export type User = {
id: UniqueId;
name: UserName;
email: Email;
preferences: Ingredient[];
allergies: Ingredient[];
};

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

// 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)建一個新訂單,我們再來添加一個訂單實體類型。

// domain/order.ts  — ConardLi
export type OrderStatus = "new" | "delivery" | "completed";
export type Order = {
user: UniqueId;
cart: Cart;
created: DateTimeString;
status: OrderStatus;
total: PriceCents;
};

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

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

我們可以檢查以下幾點:

  •  參與者是否是一個用戶
  • 訂單里是否有足夠的信息
  • 有些實體是否需要擴展
  • 在未來是否有足夠的可擴展性

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

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

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

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

比如,為了判斷某個用戶是喜歡還是厭惡某個口味,我們可以創(chuàng)建兩個函數(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);
}

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

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

創(chuà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),
};
}

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

你可能已經(jīng)注意到我們在描述領(lǐng)域類型的時候使用的一些類型。例如 Email,UniqueId 或 DateTimeString 。這些其實都是類型別名:

// shared-kernel.d.ts
type Email = string;
type UniqueId = string;
type DateTimeString = string;
type PriceCents = number;

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

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

在實踐中,共享內(nèi)核可以這樣解釋:我們用到 TypeScript,使用它的標準類型庫,但我們不會把它們看作是一個依賴項。這是因為使用它們的模塊互相不會產(chǎn)生影響并且可以保持解耦。

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

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

實現(xiàn)細節(jié)—應用層

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

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

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

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

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

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

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

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

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

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

設(shè)計用例

我們選擇結(jié)賬這個場景來做用例設(shè)計,它更具代表性,因為它是異步的,而且會與很多第三方服務進行交互。

我們可以想一想,通過整個用例我們要表達什么。用戶的購物車里有一些餅干,當用戶點擊購買按鈕的時候:

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

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

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

當然,理想情況下,用例不應該接收兩個單獨的參數(shù),而是接收一個封裝后的對象,為了精簡代碼,我們先這樣處理。

編寫應用層的接口

我們再來仔細看看用例的步驟:訂單創(chuàng)建本身就是一個領(lǐng)域函數(shù),其他一切操作我們都要調(diào)用外部服務。

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

想一想我們可能會用到的服務:

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

注意,我們現(xiàn)在討論的是這些服務的 interface ,而不是它們的具體實現(xiàn)。在這個階段,描述必要的行為對我們來說很重要,因為這是我們在描述場景時在應用層所依賴的行為。

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

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

支付系統(tǒng)接口

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

// application/ports.ts  — ConardLi
export interface PaymentService {
tryPay(amount: PriceCents): Promise;
}

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

通知服務接口

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

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

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

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

// application/ports.ts
export interface NotificationService {
notify(message: string): void;
}

本地存儲接口

我們會將新的訂單保存在本地的存儲庫中。

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

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

// application/ports.ts    — ConardLi
export interface OrdersStorageService {
orders: Order[];
updateOrders(orders: Order[]): void;
}

用例方法

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

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

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

// application/orderProducts.ts — ConardLi
const payment: PaymentService = {};
const notifier: NotificationService = {};
const orderStorage: OrdersStorageService = {};

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

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

// application/orderProducts.ts  — ConardLi
//...
async function orderProducts(user: User, cart: Cart) {
const order = createOrder(user, cart);
}

這里,我們把接口當作是行為的約定。也就是說以模塊示例會真正執(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();
}

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

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

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

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

添加UI和用例

首先,第一個適配器就是一個 UI 框架。它把瀏覽器的 API 與我們的應用程序連接起來。在訂單創(chuàng)建的這個場景,就是“結(jié)帳”按鈕和點擊事件的處理方法,這里會調(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 (

Checkout


{/* ... */}


);
}


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

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

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

支付服務實現(xiàn)

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

對于付款操作,我們依然使用一個假的 API 。同樣的,我們現(xiàn)在還是沒必要編寫全部的服務,我們可以之后再實現(xiàn),現(xiàn)在最重要的是實現(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 這個函數(shù)會在 450 毫秒后觸發(fā)的超時,模擬來自服務器的延遲響應,它返回我們傳入的參數(shù)。

// services/api.ts  — ConardLi
export function fakeApi(response: TResponse): Promise {
return new Promise((res) => setTimeout(() => res(response), 450));
}


通知服務實現(xiàn)

我們就簡單使用 alert 來實現(xiàn)通知,因為代碼是解耦的,以后再來重寫這個服務也不成問題。

// services/notificationAdapter.ts  — ConardLi
import { NotificationService } from "../application/ports";
export function useNotifier(): NotificationService {
return {
notify: (message: string) => window.alert(message),
};
}

本地存儲實現(xiàn)

我們就通過 React.Context 和 Hooks 來實現(xiàn)本地存儲。

我們創(chuàng)建一個新的 context,然后把它傳給 provider,然后導出讓其他的模塊可以通過 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 (
{children}
);
};

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

// services/storageAdapter.ts
export function useOrdersStorage(): OrdersStorageService {
return useStore();
}

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

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

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

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

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

<
當前文章:如何構(gòu)建前端領(lǐng)域的“干凈架構(gòu)”
網(wǎng)頁鏈接:http://uogjgqi.cn/article/dhhccji.html
掃二維碼與項目經(jīng)理溝通

我們在微信上24小時期待你的聲音

解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流