接上一篇 Newbe.Claptrap 框架入門,第二步 —— 簡單業務,清空購物車 ,我們繼續要了解一下如何使用 Newbe.Claptrap 框架開發業務。通過本篇閱讀,您便可以開始學會新增一個全新的 Claptrap。
Newbe.Claptrap 是一個用於輕鬆應對併發問題的分散式開發框架。如果您是首次閱讀本系列文章。建議可以先從本文末尾的入門文章開始瞭解。
開篇摘要
本篇,我通過實現 “管理庫存” 的需求來了解一下如何在已有的專案樣例中定義一個 Claptrap。
結合前一篇的基本步驟,定義 Claptrap 只要而外增加一些步驟就可以了。完整的步驟如下所示,其中標記為 “新內容” 的部分屬於本篇的區別於前篇的新內容:
- 定義 ClaptrapTypeCode (新內容)
- 定義 State (新內容)
- 定義 Grain 介面 (新內容)
- 實現 Grain (新內容)
- 註冊 Grain (新內容)
- 定義 EventCode
- 定義 Event
- 實現 EventHandler
- 註冊 EventHandler
- 實現 IInitialStateDataFactory (新內容)
- 修改 Controller
這是一個從下向上的過程,實際的編碼過程中開發也可以有所調整。
本篇實現的業務用例:
- 實現表示庫存資料的 SKU(Stock keeping Unit) 物件。
- 能夠對 SKU 進行更新和讀取。
定義 ClaptrapTypeCode
ClaptrapTypeCode 是一個 Claptrap 的唯一編碼。其在 State 的識別,序列化等方面起到了重要的作用。
開啟 HelloClaptrap.Models
專案中的 ClaptrapCodes
類。
新增 SKU 的 ClaptrapTypeCode。
namespace HelloClaptrap.Models { public static class ClaptrapCodes { public const string CartGrain = "cart_claptrap_newbe"; private const string CartEventSuffix = "_e_" + CartGrain; public const string AddItemToCart = "addItem" + CartEventSuffix; public const string RemoveItemFromCart = "removeItem" + CartEventSuffix; #region Sku + public const string SkuGrain = "sku_claptrap_newbe"; + private const string SkuEventSuffix = "_e_" + SkuGrain; #endregion } }
定義 State
State 在 Actor 模式中代表了 Actor 物件當前的資料表現。
由於 Claptrap 是基於事件溯源模式的 Actor。因此定義恰好的 State 非常重要。
在該示例當中,我們只需要記錄當前 SKU 的庫存即可,因此,State 的設計非常的簡單。
在 HelloClaptrap.Models
專案新增 Sku
資料夾,並在該資料夾下建立 SkuState
類。
新增如下程式碼:
+ using Newbe.Claptrap; + + namespace HelloClaptrap.Models.Sku + { + public class SkuState : IStateData + { + public int Inventory { get; set; } + } + }
Inventory 表示當前 SKU 的庫存。
IStateData
介面是框架中表示 State 的空介面,用於在泛型推斷時使用。
定義 Grain 介面
定義 Grain 介面的定義,才能夠提供外部與 Claptrap 的互操作性。
在 HelloClaptrap.IActors
專案中新增 ISkuGrain
介面。
新增介面以及 Attribute。
+ using System.Threading.Tasks; + using HelloClaptrap.Models; + using HelloClaptrap.Models.Sku; + using Newbe.Claptrap; + using Newbe.Claptrap.Orleans; + + namespace HelloClaptrap.IActor + { + [ClaptrapState(typeof(SkuState), ClaptrapCodes.SkuGrain)] + public interface ISkuGrain : IClaptrapGrain + { + /// <summary> + /// Get latest inventory of this sku + /// </summary> + /// <returns></returns> + Task<int> GetInventoryAsync(); + + /// <summary> + /// Update inventory by add diff, diff could be negative number + /// </summary> + /// <param name="diff"></param> + /// <returns>Inventory after updating</returns> + Task<int> UpdateInventoryAsync(int diff); + } + }
其中增加了以下內容:
- 標記了
ClaptrapState
,使得 State 與 Grain 進行關聯。 - 介面繼承了
IClaptrapGrain
,這是框架定義的 Grain 介面,這是依託於 Orleans 執行必須繼承的介面。 - 增加了 GetInventoryAsync 方法,表示 “獲取當前庫存”。
- 增加了 UpdateInventoryAsync 方法,表示 “增量更新當前庫存”。
diff > 0
表示增加庫存,diff < 0
表示減少庫存。 - 需要注意的是 Grain 的方法定義有一定限制。詳細可以參見《Developing a Grain》。
實現 Grain
定義好 ISkuGrain 之後,便可以新增程式碼進行實現。
在 HelloClaptrap.Actors
專案新建 Sku
資料夾,並在該資料夾中新增 SkuGrain
類。
+ using System; + using System.Threading.Tasks; + using HelloClaptrap.IActor; + using HelloClaptrap.Models; + using HelloClaptrap.Models.Sku; + using Newbe.Claptrap; + using Newbe.Claptrap.Orleans; + + namespace HelloClaptrap.Actors.Sku + { + public class SkuGrain : ClaptrapBoxGrain<SkuState>, ISkuGrain + { + public SkuGrain(IClaptrapGrainCommonService claptrapGrainCommonService) + : base(claptrapGrainCommonService) + { + } + + public Task<int> GetInventoryAsync() + { + return Task.FromResult(StateData.Inventory); + } + + public async Task<int> UpdateInventoryAsync(int diff) + { + if (diff == 0) + { + throw new BizException("diff can`t be 0"); + } + + var old = StateData.Inventory; + var newInventory = old + diff; + if (newInventory < 0) + { + throw new BizException( + $"failed to update inventory. It will be less than 0 if add diff amount. current : {old} , diff : {diff}"); + } + + throw new NotImplementedException(); + } + } + }
其中增加了以下內容:
- 繼承
ClaptrapBoxGrain<SkuState>
並實現ISkuGrain
,ClaptrapBoxGrain
是框架定義的 Grain 基類,其中的泛型參數列示對應的 State 型別。 - 實現 GetInventoryAsync 方法,從 StateData 中讀取當前的庫存。
- 實現 UpdateInventoryAsync 方法,新增業務判斷程式碼,若不滿足業務操作的條件則丟擲異常。
- UpdateInventoryAsync 的最後我們現在丟擲 NotImplementedException ,因為當前事件還沒有定義,需要等待後續的程式碼實現。
- BizException 是一個自定義異常,可以自行新增。實際開發中也可以不使用丟擲異常的方式表示業務中斷,改用狀態碼或者其他返回值也是可以的。
註冊 Grain
Claptrap 對應的 Grain 需要在應用程式啟動時進行註冊,這樣框架才能掃描發現。
由於示例程式碼採用的是程式集範圍內掃描,因此實際上不需要進行修改。
這裡指出發生註冊的位置:
開啟 HelloClaptrap.BackendServer
專案的 Program
類。
using System; using Autofac; using HelloClaptrap.Actors.Cart; using HelloClaptrap.IActor; using HelloClaptrap.Repository; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Newbe.Claptrap; using Newbe.Claptrap.Bootstrapper; using NLog.Web; using Orleans; namespace HelloClaptrap.BackendServer { public class Program { public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) .UseClaptrap( builder => { + builder + .ScanClaptrapDesigns(new[] + { + typeof(ICartGrain).Assembly, + typeof(CartGrain).Assembly, + }); }, builder => { builder.RegisterModule<RepositoryModule>(); }) .UseOrleansClaptrap() .UseOrleans(builder => builder.UseDashboard(options => options.Port = 9000)) .ConfigureLogging(logging => { logging.ClearProviders(); logging.SetMinimumLevel(LogLevel.Trace); }) .UseNLog(); } }
因為 ISkuGrain 和 SkuGrain 分別於 ICartGrain 和 CartGrain 屬於同一程式集,因而此處不需要修改。
定義 EventCode
前面我們已經實現了 Claptrap 的主要部分,但唯獨沒有完成更新庫存的操作。這是因為更新庫存是需要對 State 進行更新的。而我們都知道 Claptrap 是基於事件溯源的 Actor 模式,對 State 的更新需要通過事件才能完成。故而由這裡開始,我們來通過事件更新庫存。
EventCode 是 Claptrap 系統每個事件的唯一編碼。其在事件的識別,序列化等方面起到了重要的作用。
開啟 HelloClaptrap.Models
專案中的 ClaptrapCodes
類。
新增 “更新庫存” 的 EventCode。
namespace HelloClaptrap.Models { public static class ClaptrapCodes { #region Cart public const string CartGrain = "cart_claptrap_newbe"; private const string CartEventSuffix = "_e_" + CartGrain; public const string AddItemToCart = "addItem" + CartEventSuffix; public const string RemoveItemFromCart = "removeItem" + CartEventSuffix; public const string RemoveAllItemsFromCart = "remoeAllItems" + CartEventSuffix; #endregion #region Sku public const string SkuGrain = "sku_claptrap_newbe"; private const string SkuEventSuffix = "_e_" + SkuGrain; + public const string SkuInventoryUpdate = "inventoryUpdate" + SkuEventSuffix; #endregion } }
定義 Event
Event 是事件溯源的關鍵。用於改變 Claptrap 中的 State。並且 Event 會被持久化在持久層。
在 HelloClaptrap.Models
專案的 Sku/Events
資料夾下建立 InventoryUpdateEvent
類。
新增如下程式碼:
+ using Newbe.Claptrap; + + namespace HelloClaptrap.Models.Sku.Events + { + public class InventoryUpdateEvent : IEventData + { + public int Diff { get; set; } + public int NewInventory { get; set; } + } + }
- Diff 表示此次更新庫存的數額,
diff > 0
表示增加庫存,diff < 0
表示減少庫存。 - NewInventory 表示更新之後的庫存。此處,提前給出一個建議,但由於篇幅問題,不展開討論:建議在事件中包含 State 的更新後資料。
實現 EventHandler
EventHandler
用於將事件更新到 Claptrap 的 State
上。
在 HelloClaptrap.Actors
專案的 Sku/Events
資料夾下建立 InventoryUpdateEventHandler
類。
新增如下程式碼:
+ using System.Threading.Tasks; + using HelloClaptrap.Models.Sku; + using HelloClaptrap.Models.Sku.Events; + using Newbe.Claptrap; + + namespace HelloClaptrap.Actors.Sku.Events + { + public class InventoryUpdateEventHandler + : NormalEventHandler<SkuState, InventoryUpdateEvent> + { + public override ValueTask HandleEvent(SkuState stateData, + InventoryUpdateEvent eventData, + IEventContext eventContext) + { + stateData.Inventory = eventData.NewInventory; + return new ValueTask(); + } + } + }
- 因為事件中已經包含了更新後的庫存,故而直接對 StateData 進行賦值即可。
註冊 EventHandler
實現並測試完 EventHandler 之後,便可以將 EventHandler 進行註冊,以便與 EventCode 以及 Claptrap 進行關聯。
開啟 HelloClaptrap.Actors
專案的 SkuGrain
類。
使用 Attribute 進行標記,並修改 UpdateInventoryAsync 執行事件。
using System.Threading.Tasks; + using HelloClaptrap.Actors.Sku.Events; using HelloClaptrap.IActor; using HelloClaptrap.Models; using HelloClaptrap.Models.Sku; + using HelloClaptrap.Models.Sku.Events; using Newbe.Claptrap; using Newbe.Claptrap.Orleans; namespace HelloClaptrap.Actors.Sku { + [ClaptrapEventHandler(typeof(InventoryUpdateEventHandler), ClaptrapCodes.SkuInventoryUpdate)] public class SkuGrain : ClaptrapBoxGrain<SkuState>, ISkuGrain { public SkuGrain(IClaptrapGrainCommonService claptrapGrainCommonService) : base(claptrapGrainCommonService) { } public Task<int> GetInventoryAsync() { return Task.FromResult(StateData.Inventory); } public async Task<int> UpdateInventoryAsync(int diff) { if (diff == 0) { throw new BizException("diff can`t be 0"); } var old = StateData.Inventory; var newInventory = old + diff; if (newInventory < 0) { throw new BizException( $"failed to update inventory. It will be less than 0 if add diff amount. current : {old} , diff : {diff}"); } - throw new NotImplementedException(); + var evt = this.CreateEvent(new InventoryUpdateEvent + { + Diff = diff, + NewInventory = newInventory + }); + await Claptrap.HandleEventAsync(evt); + return StateData.Inventory; } } }
實現 IInitialStateDataFactory
前面我們已經完成了庫存的查詢和更新。不過通常來說庫存有一個初始數額,我們本節在補充這部分邏輯。
在 HelloClaptrap.Actors
專案的 Sku
資料夾下建立 SkuStateInitHandler
類。
+ using System.Threading.Tasks; + using HelloClaptrap.Models.Sku; + using HelloClaptrap.Repository; + using Newbe.Claptrap; + + namespace HelloClaptrap.Actors.Sku + { + public class SkuStateInitHandler : IInitialStateDataFactory + { + private readonly ISkuRepository _skuRepository; + + public SkuStateInitHandler( + ISkuRepository skuRepository) + { + _skuRepository = skuRepository; + } + + public async Task<IStateData> Create(IClaptrapIdentity identity) + { + var skuId = identity.Id; + var inventory = await _skuRepository.GetInitInventoryAsync(skuId); + var re = new SkuState + { + Inventory = inventory + }; + return re; + } + } + }
IInitialStateDataFactory
會在 Claptrap 初次啟用時被呼叫,用來建立 State 的初始值。- 注入
ISkuRepository
從資料庫中讀取 Sku 對應的庫存初始數額,具體的程式碼此處不進行羅列,讀者可以檢視樣例倉庫中的實現。
除了實現程式碼之外,還需要進行註冊才會被呼叫。
開啟 HelloClaptrap.Actors
專案的 SkuGrain
類。
using System.Threading.Tasks; using HelloClaptrap.Actors.Sku.Events; using HelloClaptrap.IActor; using HelloClaptrap.Models; using HelloClaptrap.Models.Sku; using HelloClaptrap.Models.Sku.Events; using Newbe.Claptrap; using Newbe.Claptrap.Orleans; namespace HelloClaptrap.Actors.Sku { + [ClaptrapStateInitialFactoryHandler(typeof(SkuStateInitHandler))] [ClaptrapEventHandler(typeof(InventoryUpdateEventHandler), ClaptrapCodes.SkuInventoryUpdate)] public class SkuGrain : ClaptrapBoxGrain<SkuState>, ISkuGrain { public SkuGrain(IClaptrapGrainCommonService claptrapGrainCommonService) : base(claptrapGrainCommonService) { } public Task<int> GetInventoryAsync() { return Task.FromResult(StateData.Inventory); } public async Task<int> UpdateInventoryAsync(int diff) { if (diff == 0) { throw new BizException("diff can`t be 0"); } var old = StateData.Inventory; var newInventory = old + diff; if (newInventory < 0) { throw new BizException( $"failed to update inventory. It will be less than 0 if add diff amount. current : {old} , diff : {diff}"); } var evt = this.CreateEvent(new InventoryUpdateEvent { Diff = diff, NewInventory = newInventory }); await Claptrap.HandleEventAsync(evt); return StateData.Inventory; } } }
修改 Controller
前面的所有步驟完成之後,就已經完成了 Claptrap 的所有部分。但由於 Claptrap 無法直接提供與外部程式的互操作性。因此,還需要在在 Controller 層增加一個 API 以便外部進行 “讀取庫存” 的操作。
在 HelloClaptrap.Web
專案的 Controllers
資料夾下新建 SkuController
類。
+ using System.Threading.Tasks; + using HelloClaptrap.IActor; + using Microsoft.AspNetCore.Mvc; + using Orleans; + + namespace HelloClaptrap.Web.Controllers + { + [Route("api/[controller]")] + public class SkuController : Controller + { + private readonly IGrainFactory _grainFactory; + + public SkuController( + IGrainFactory grainFactory) + { + _grainFactory = grainFactory; + } + + [HttpGet("{id}")] + public async Task<IActionResult> GetItemsAsync(string id) + { + var skuGrain = _grainFactory.GetGrain<ISkuGrain>(id); + var inventory = await skuGrain.GetInventoryAsync(); + return Json(new + { + skuId = id, + inventory = inventory, + }); + } + } + }
- 新增 API 讀取特定 SkuId 的庫存。按照樣例程式碼的實現,可以傳入
yueluo-123
得到庫存數額為 666。不存在的 SkuId 將會丟擲異常。 - 此處沒有建立更新庫存的對外 API,因為本示例將在下篇進行下單購物時進行庫存操作,此處暫不需要 API。
小結
至此,我們就完成了 “管理商品庫存” 這個簡單需求的所有內容。
您可以從以下地址來獲取本文章對應的原始碼:
最後但是最重要!
最近作者正在構建以反應式
、Actor模式
和事件溯源
為理論基礎的一套服務端開發框架。希望為開發者提供能夠便於開發出 “分散式”、“可水平擴充套件”、“可測試性高” 的應用系統 ——Newbe.Claptrap
本篇文章是該框架的一篇技術選文,屬於技術構成的一部分。如果讀者對該內容感興趣,歡迎轉發、評論、收藏文章以及專案。您的支援是促進專案成功的關鍵。
聯絡方式:
- Github Issue
- Gitee Issue
- 公開郵箱 newbe-claptrap@googlegroups.com (傳送到該郵箱的內容將被公開)
- Gitter
您還可以查閱本系列的其他選文:
理論入門篇
術語介紹篇
- Actor 模式
- 事件溯源(Event Sourcing)
- Claptrap
- Minion
- 事件 (Event)
- 狀態 (State)
- 狀態快照 (State Snapshot)
- Claptrap 設計圖 (Claptrap Design)
- Claptrap 工廠 (Claptrap Factory)
- Claptrap Identity
- Claptrap Box
- Claptrap 生命週期(Claptrap Lifetime Scope)
- 序列化(Serialization)
實現入門篇
- Newbe.Claptrap 框架入門,第一步 —— 建立專案,實現簡易購物車
- Newbe.Claptrap 框架入門,第二步 —— 簡單業務,清空購物車
- Newbe.Claptrap 框架入門,第三步 —— 定義 Claptrap,管理商品庫存
樣例實踐篇
其他番外篇
- 談反應式程式設計在服務端中的應用,資料庫操作優化,從 20 秒到 0.5 秒
- 談反應式程式設計在服務端中的應用,資料庫操作優化,提速 Upsert
- 十萬同時線上使用者,需要多少記憶體?——Newbe.Claptrap 框架水平擴充套件實驗
- docker-mcr 助您全速下載 dotnet 映象
- 十多位全球技術專家,為你獻上近十個小時的.Net 微服務介紹
- 年輕的樵夫喲,你掉的是這個免費 8 核 4G 公網伺服器,還是這個隨時可用的 Docker 實驗平臺?
GitHub 專案地址:https://github.com/newbe36524/Newbe.Claptrap
Gitee 專案地址:https://gitee.com/yks/Newbe.Claptrap
您當前檢視的是先行釋出於 www.newbe.pro 上的部落格文章,實際開發文件隨版本而迭代。若要檢視最新的開發文件,需要移步 claptrap.newbe.pro。
- 本文作者: newbe36524
- 本文連結: https://www.newbe.pro/Newbe.Claptrap/Get-Started-3/
- 版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!