Newbe.Claptrap 框架入門,第三步 —— 定義 Claptrap,管理商品庫存


接上一篇 Newbe.Claptrap 框架入門,第二步 —— 簡單業務,清空購物車 ,我們繼續要了解一下如何使用 Newbe.Claptrap 框架開發業務。通過本篇閱讀,您便可以開始學會添加一個全新的 Claptrap。

Newbe.Claptrap 是一個用於輕松應對並發問題的分布式開發框架。如果您是首次閱讀本系列文章。建議可以先從本文末尾的入門文章開始了解。

開篇摘要

本篇,我通過實現 “管理庫存” 的需求來了解一下如何在已有的項目樣例中定義一個 Claptrap。

結合前一篇的基本步驟,定義 Claptrap 只要而外增加一些步驟就可以了。完整的步驟如下所示,其中標記為 “新內容” 的部分屬於本篇的區別於前篇的新內容:

  1. 定義 ClaptrapTypeCode (新內容)
  2. 定義 State (新內容)
  3. 定義 Grain 接口 (新內容)
  4. 實現 Grain (新內容)
  5. 注冊 Grain (新內容)
  6. 定義 EventCode
  7. 定義 Event
  8. 實現 EventHandler
  9. 注冊 EventHandler
  10. 實現 IInitialStateDataFactory (新內容)
  11. 修改 Controller

這是一個從下向上的過程,實際的編碼過程中開發也可以有所調整。

本篇實現的業務用例:

  1. 實現表示庫存數據的 SKU(Stock keeping Unit) 對象。
  2. 能夠對 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);
+     }
+ }

 

其中增加了以下內容:

  1. 標記了 ClaptrapState,使得 State 與 Grain 進行關聯。
  2. 接口繼承了 IClaptrapGrain,這是框架定義的 Grain 接口,這是依托於 Orleans 運行必須繼承的接口。
  3. 增加了 GetInventoryAsync 方法,表示 “獲取當前庫存”。
  4. 增加了 UpdateInventoryAsync 方法,表示 “增量更新當前庫存”。diff > 0 表示增加庫存,diff < 0 表示減少庫存。
  5. 需要注意的是 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();
+         }
+     }
+ }

 

其中增加了以下內容:

  1. 繼承 ClaptrapBoxGrain<SkuState> 並實現 ISkuGrainClaptrapBoxGrain 是框架定義的 Grain 基類,其中的泛型參數表示對應的 State 類型。
  2. 實現 GetInventoryAsync 方法,從 StateData 中讀取當前的庫存。
  3. 實現 UpdateInventoryAsync 方法,添加業務判斷代碼,若不滿足業務操作的條件則拋出異常。
  4. UpdateInventoryAsync 的最后我們現在拋出 NotImplementedException ,因為當前事件還沒有定義,需要等待后續的代碼實現。
  5. 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; }
+     }
+ }

 

 
  1. Diff 表示此次更新庫存的數額,diff > 0 表示增加庫存,diff < 0 表示減少庫存。
  2. 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();
+         }
+     }
+ }

 

 
  1. 因為事件中已經包含了更新后的庫存,故而直接對 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;
+         }
+     }
+ }

 

 
  1. IInitialStateDataFactory 會在 Claptrap 初次激活時被調用,用來創建 State 的初始值。
  2. 注入 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,
+             });
+         }
+     }
+ }

 

 
  1. 新增 API 讀取特定 SkuId 的庫存。按照樣例代碼的實現,可以傳入 yueluo-123 得到庫存數額為 666。不存在的 SkuId 將會拋出異常。
  2. 此處沒有創建更新庫存的對外 API,因為本示例將在下篇進行下單購物時進行庫存操作,此處暫不需要 API。

小結

至此,我們就完成了 “管理商品庫存” 這個簡單需求的所有內容。

您可以從以下地址來獲取本文章對應的源代碼:

最后但是最重要!

最近作者正在構建以反應式Actor模式事件溯源為理論基礎的一套服務端開發框架。希望為開發者提供能夠便於開發出 “分布式”、“可水平擴展”、“可測試性高” 的應用系統 ——Newbe.Claptrap

本篇文章是該框架的一篇技術選文,屬於技術構成的一部分。如果讀者對該內容感興趣,歡迎轉發、評論、收藏文章以及項目。您的支持是促進項目成功的關鍵。

聯系方式:

您還可以查閱本系列的其他選文:

理論入門篇

  1. Newbe.Claptrap - 一套以 “事件溯源” 和 “Actor 模式” 作為基本理論的服務端開發框架

術語介紹篇

  1. Actor 模式
  2. 事件溯源(Event Sourcing)
  3. Claptrap
  4. Minion
  5. 事件 (Event)
  6. 狀態 (State)
  7. 狀態快照 (State Snapshot)
  8. Claptrap 設計圖 (Claptrap Design)
  9. Claptrap 工廠 (Claptrap Factory)
  10. Claptrap Identity
  11. Claptrap Box
  12. Claptrap 生命周期(Claptrap Lifetime Scope)
  13. 序列化(Serialization)

實現入門篇

  1. Newbe.Claptrap 框架入門,第一步 —— 創建項目,實現簡易購物車
  2. Newbe.Claptrap 框架入門,第二步 —— 簡單業務,清空購物車
  3. Newbe.Claptrap 框架入門,第三步 —— 定義 Claptrap,管理商品庫存

樣例實踐篇

  1. 構建一個簡易的火車票售票系統,Newbe.Claptrap 框架用例,第一步 —— 業務分析
  2. 在線體驗火車票售票系統

其他番外篇

  1. 談反應式編程在服務端中的應用,數據庫操作優化,從 20 秒到 0.5 秒
  2. 談反應式編程在服務端中的應用,數據庫操作優化,提速 Upsert
  3. 十萬同時在線用戶,需要多少內存?——Newbe.Claptrap 框架水平擴展實驗
  4. docker-mcr 助您全速下載 dotnet 鏡像
  5. 十多位全球技術專家,為你獻上近十個小時的.Net 微服務介紹
  6. 年輕的樵夫喲,你掉的是這個免費 8 核 4G 公網服務器,還是這個隨時可用的 Docker 實驗平台?

GitHub 項目地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 項目地址:https://gitee.com/yks/Newbe.Claptrap

您當前查看的是先行發布於 www.newbe.pro 上的博客文章,實際開發文檔隨版本而迭代。若要查看最新的開發文檔,需要移步 claptrap.newbe.pro

Newbe.Claptrap

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM