讓我們來實現一個簡單的 “電商購物車” 需求來了解一下如何使用 Newbe.Claptrap 進行開發。
業務需求
實現一個簡單的 “電商購物車” 需求,這里實現幾個簡單的業務:
- 獲取當前購物車中的商品和數量
- 向購物車中添加商品
- 從購物車中移除特定的商品
安裝項目模板
首先,需要確保已經安裝了 .NetCore SDK 3.1 。可以點擊此處來獲取最新的版本進行安裝。
SDK 安裝完畢后,打開控制台運行以下命令來安裝最新的項目模板:
dotnet new --install Newbe.Claptrap.Template
安裝完畢后,可以在安裝結果中查看到已經安裝的項目模板。
創建項目
選擇一個位置,創建一個文件夾,本示例選擇在 D:\Repo
下創建一個名為 HelloClaptrap
的文件夾。該文件夾將會作為新項目的代碼文件夾。
打開控制台,並且將工作目錄切換到 D:\Repo\HelloClaptrap
。然后運行以下命令便可以創建出項目:
dotnet new newbe.claptrap --name HelloClaptrap
通常來說,我們建議將
D:\Repo\HelloClaptrap
創建為 Git 倉庫文件夾。通過版本控制來管理您的源碼。
編譯與啟動
項目創建完成之后,您可以會用您偏愛的 IDE 打開解決方案進行編譯。
編譯完成后,通過 IDE 上 “啟動” 功能,同時啟動 Web 和 BackendServer 兩個項目。(VS 需要以控制台方式啟動服務,如果使用 IIS Express,需要開發者看一下對應的端口號來訪問 Web 頁面)
啟動完成后,便可以通過 http://localhost:36525/swagger
地址來查看樣例項目的 API 描述。其中包括了三個主要的 API:
GET
/api/Cart/{id}
獲取特定 id 購物車中的商品和數量POST
/api/Cart/{id}
添加新的商品到指定 id 的購商品DELETE
/api/Cart/{id}
從指定 id 的購物車中移除特定的商品
您可以通過界面上的 Try It Out 按鈕來嘗試對 API 進行幾次調用。
第一次添加商品,沒有效果?
是的,您說的沒錯。項目模板中的業務實現是存在 BUG 的。
接下來我們來打開項目,通過添加一些斷點來排查並解決這些 BUG。
並且通過對 BUG 的定位,您可以了解框架的代碼流轉過程。
添加斷點
以下根據不同的 IDE 說明需要增加斷點的位置,您可以選擇您習慣的 IDE 進行操作。
如果您當前手頭沒有 IDE,也可以跳過本節,直接閱讀后面的內容。
Visual Studio
按照上文提到的啟動方式,同時啟動兩個項目。
導入斷點:打開 “斷點” 窗口,點擊按鈕,從項目下選擇 breakpoints.xml
文件。可以通過以下兩張截圖找到對應的操作位置。
Rider
按照上文提到的啟動方式,同時啟動兩個項目。
Rider 目前沒有斷點導入功能。因此需要手動的在以下位置創建斷點:
文件 | 行號 |
---|---|
CartController | 30 |
CartController | 34 |
CartGrain | 24 |
CartGrain | 32 |
AddItemToCartEventHandler | 14 |
AddItemToCartEventHandler | 28 |
開始調試
接下來,我們通過一個請求來了解一下整個代碼運行的過程。
首先,我們先通過 swagger 界面來發送一個 POST 請求,嘗試為購物車添加商品。
CartController Start
首先命中斷點是 Web API 層的 Controller 代碼:
[HttpPost("{id}")] public async Task<IActionResult> AddItemAsync(int id, [FromBody] AddItemInput input) { var cartGrain = _grainFactory.GetGrain<ICartGrain>(id.ToString()); var items = await cartGrain.AddItemAsync(input.SkuId, input.Count); return Json(items); }
在這段代碼中,我們通過_grainFactory
來創建一個 ICartGrain
實例。
這實例本質是一個代理,這個代理將指向 Backend Server 中的一個具體 Grain。
傳入的 id 可以認為是定位實例使用唯一標識符。在這個業務上下文中,可以理解為 “購物車 id” 或者 “用戶 id”(如果每個用戶只有一個購物車的話)。
繼續調試,進入下一步,讓我們來看看 ICartGrain 內部是如何工作的。
CartGrain Start
接下來命中斷點的是 CartGrain 代碼:
public async Task<Dictionary<string, int>> AddItemAsync(string skuId, int count) { var evt = this.CreateEvent(new AddItemToCartEvent { Count = count, SkuId = skuId, }); await Claptrap.HandleEventAsync(evt); return StateData.Items; }
可以通過調試器看到傳入的 skuId 和 count 都是從 Controller 傳遞過來的參數。
在這里您可以完成以下這些操作:
- 通過事件對 Claptrap 中的數據進行修改
- 讀取 Claptrap 中保存的數據
這段代碼中,我們創建了一個 AddItemToCartEvent
對象來表示一次對購物車的變更。
然后將它傳遞給 Claptrap 進行處理了。
Claptrap 接受了事件之后就會更新自身的 State 數據。
最后我們將 StateData.Items 返回給調用方。(實際上 StateData.Items 是 Claptrap.State.Data.Items 的一個快捷屬性。因此實際上還是從 Claptrap 中讀取。)
通過調試器,可以看到 StateData 的數據類型如下所示:
public class CartState : IStateData { public Dictionary<string, int> Items { get; set; } }
這就是樣例中設計的購物車狀態。我們使用一個 Dictionary
來表示當前購物車中的 SkuId 及其對應的數量。
繼續調試,進入下一步,讓我們看看 Claptrap 是如何處理傳入的事件的。
AddItemToCartEventHandler Start
再次命中斷點的是下面這段代碼:
public class AddItemToCartEventHandler : NormalEventHandler<CartState, AddItemToCartEvent> { public override ValueTask HandleEvent(CartState stateData, AddItemToCartEvent eventData, IEventContext eventContext) { var items = stateData.Items ?? new Dictionary<string, int>(); if (items.TryGetValue(eventData.SkuId, out var itemCount)) { itemCount += eventData.Count; } // else // { // itemCount = eventData.Count; // } items[eventData.SkuId] = itemCount; stateData.Items = items; return new ValueTask(); } }
這段代碼中,包含有兩個重要參數,分別是表示當前購物車狀態的 CartState
和需要處理的事件 AddItemToCartEvent
。
我們按照業務需求,判斷狀態中的字典是否包含 SkuId,並對其數量進行更新。
繼續調試,代碼將會運行到這段代碼的結尾。
此時,通過調試器,可以發現,stateData.Items 這個字典雖然增加了一項,但是數量卻是 0 。原因其實就是因為上面被注釋的 else 代碼段,這就是第一次添加購物車總是失敗的 BUG 成因。
在這里,不要立即中斷調試。我們繼續調試,讓代碼走完,來了解整個過程如何結束。
實際上,繼續調試,斷點將會依次命中 CartGrain 和 CartController 對應方法的方法結尾。
這其實就是三層架構!
絕大多數的開發者都了解三層架構。其實,我們也可以說 Newbe.Claptrap 其實就是一個三層架構。下面我們通過一個表格來對比一下:
傳統三層 | Newbe.Claptrap | 說明 |
---|---|---|
Presentation 展示層 | Controller 層 | 用來與外部的系統進行對接,提供對外的互操作能力 |
Business 業務層 | Grain 層 | 根據業務對傳入的業務參數進行業務處理(樣例中其實沒寫判斷,需要判斷 count > 0) |
Persistence 持久化層 | EventHandler 層 | 對業務結果進行更新 |
當然上面的類似只是一種簡單的描述。具體過程中,不需要太過於糾結,這只是一個輔助理解的說法。
您還有一個待修復的 BUG
接下來我們重新回過頭來修復前面的 “首次加入商品不生效” 的問題。
這是一個考慮單元測試框架
在項目模板中存在一個項目 HelloClaptrap.Actors.Tests
,該項目包含了對主要業務代碼的單元測試。
我們現在已經知道,AddItemToCartEventHandler
中注釋的代碼是導致 BUG 存在的主要原因。
我們可以使用 dotnet test
運行一下測試項目中的單元測試,可以得到如下兩個錯誤:
A total of 1 test files matched the specified pattern. X AddFirstOne [130ms] Error Message: Expected value to be 10, but found 0. Stack Trace: at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message) at FluentAssertions.Execution.TestFrameworkProvider.Throw(String message) at FluentAssertions.Execution.DefaultAssertionStrategy.HandleFailure(String message) at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc) at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc) at FluentAssertions.Execution.AssertionScope.FailWith(String message, Object[] args) at FluentAssertions.Numeric.NumericAssertions`1.Be(T expected, String because, Object[] becauseArgs) at HelloClaptrap.Actors.Tests.Cart.Events.AddItemToCartEventHandlerTest.AddFirstOne() in D:\Repo\HelloClaptrap\HelloClaptrap\HelloClaptrap.Actors.Tests\Cart\Events\AddItemToCartEventHandlerTest.cs:line 32 at HelloClaptrap.Actors.Tests.Cart.Events.AddItemToCartEventHandlerTest.AddFirstOne() in D:\Repo\HelloClaptrap\HelloClaptrap\HelloClaptrap.Actors.Tests\Cart\Events\AddItemToCartEventHandlerTest.cs:line 32 at NUnit.Framework.Internal.TaskAwaitAdapter.GenericAdapter`1.GetResult() at NUnit.Framework.Internal.AsyncToSyncAdapter.Await(Func`1 invoke) at NUnit.Framework.Internal.Commands.TestMethodCommand.RunTestMethod(TestExecutionContext context) at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute(TestExecutionContext context) at NUnit.Framework.Internal.Execution.SimpleWorkItem.PerformWork() X RemoveOne [2ms] Error Message: Expected value to be 90, but found 100. Stack Trace: at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message) at FluentAssertions.Execution.TestFrameworkProvider.Throw(String message) at FluentAssertions.Execution.DefaultAssertionStrategy.HandleFailure(String message) at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc) at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc) at FluentAssertions.Execution.AssertionScope.FailWith(String message, Object[] args) at FluentAssertions.Numeric.NumericAssertions`1.Be(T expected, String because, Object[] becauseArgs) at HelloClaptrap.Actors.Tests.Cart.Events.RemoveItemFromCartEventHandlerTest.RemoveOne() in D:\Repo\HelloClaptrap\HelloClaptrap\HelloClaptrap.Actors.Tests\Cart\Events\RemoveItemFromCartEventHandlerTest.cs:line 40 at HelloClaptrap.Actors.Tests.Cart.Events.RemoveItemFromCartEventHandlerTest.RemoveOne() in D:\Repo\HelloClaptrap\HelloClaptrap\HelloClaptrap.Actors.Tests\Cart\Events\RemoveItemFromCartEventHandlerTest.cs:line 40 at NUnit.Framework.Internal.TaskAwaitAdapter.GenericAdapter`1.GetResult() at NUnit.Framework.Internal.AsyncToSyncAdapter.Await(Func`1 invoke) at NUnit.Framework.Internal.Commands.TestMethodCommand.RunTestMethod(TestExecutionContext context) at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute(TestExecutionContext context) at NUnit.Framework.Internal.Execution.SimpleWorkItem.PerformWork() Test Run Failed. Total tests: 7 Passed: 5 Failed: 2
我們看一下其中一個出錯的單元測試的代碼:
[Test] public async Task AddFirstOne() { using var mocker = AutoMock.GetStrict(); await using var handler = mocker.Create<AddItemToCartEventHandler>(); var state = new CartState(); var evt = new AddItemToCartEvent { SkuId = "skuId1", Count = 10 }; await handler.HandleEvent(state, evt, default); state.Items.Count.Should().Be(1); var (key, value) = state.Items.Single(); key.Should().Be(evt.SkuId); value.Should().Be(evt.Count); }
AddItemToCartEventHandler
是該測試主要測試的組件,由於 stateData 和 event 都是通過手動構建的,因此開發者可以很容易就按照需求構建出需要測試的場景。不需要構建什么特殊的內容。
現在,只要將 AddItemToCartEventHandler
中那段被注釋的代碼還原,重新運行這個單元測試。單元測試便就通過了。BUG 也就自然的修復了。
當然,上面還有另外一個關於刪除場景的單元測試也是失敗的。開發者可以按照上文中所述的 “斷點”、“單元測試” 的思路,來修復這個問題。
數據已經持久化了
您可以嘗試重新啟動 Backend Server 和 Web, 您將會發現,您之前操作的數據已經被持久化的保存了。
我們將會在后續的篇章中進一步介紹。
小結
通過本篇,我們初步了解了一下,如何創建一個基礎的項目框架來實現一個簡單的購物車場景。
這里還有很多內容我們沒有詳細的說明:項目結構、部署、持久化等等。您可以進一步閱讀后續的文章來了解。
最后但是最重要!
最近作者正在構建以反應式
、Actor模式
和事件溯源
為理論基礎的一套服務端開發框架。希望為開發者提供能夠便於開發出 “分布式”、“可水平擴展”、“可測試性高” 的應用系統 ——Newbe.Claptrap
本篇文章是該框架的一篇技術選文,屬於技術構成的一部分。如果讀者對該內容感興趣,歡迎轉發、評論、收藏文章以及項目。您的支持是促進項目成功的關鍵。
如果你對該項目感興趣,你可以通過 github issues 提交您的看法。
如果您無法正常訪問 github issue,您也可以發送郵件到 newbe-claptrap@googlegroups.com 來參與我們的討論。
點擊鏈接 QQ 交流【Newbe.Claptrap】:https://jq.qq.com/?_wv=1027&k=5uJGXf5。
您還可以查閱本系列的其他選文:
- Newbe.Claptrap - 一套以 “事件溯源” 和 “Actor 模式” 作為基本理論的服務端開發框架
- 十萬同時在線用戶,需要多少內存?——Newbe.Claptrap 框架水平擴展實驗
- 談反應式編程在服務端中的應用,數據庫操作優化,從 20 秒到 0.5 秒
- 談反應式編程在服務端中的應用,數據庫操作優化,提速 Upsert
- Newbe.Claptrap 項目周報 1 - 還沒輪影,先用輪跑
GitHub 項目地址:https://github.com/newbe36524/Newbe.Claptrap
Gitee 項目地址:https://gitee.com/yks/Newbe.Claptrap
- 本文作者: newbe36524
- 本文鏈接: https://www.newbe.pro/Newbe.Claptrap/Get-Started-1/
- 版權聲明: 本博客所有文章除特別聲明外,均采用 BY-NC-SA 許可協議。轉載請注明出處!