1. 程式人生 > 實用技巧 >lingo 解基礎 0 - 1 揹包問題

lingo 解基礎 0 - 1 揹包問題

接上一篇Newbe.Claptrap 框架入門,第一步 —— 建立專案,實現簡易購物車,我們繼續要了解一下如何使用 Newbe.Claptrap 框架開發業務。通過本篇閱讀,您便可以開始嘗試使用 Claptrap 實現業務了。

開篇摘要

本篇,我通過實現 “清空購物車” 的需求來了解一下如何在已有的專案樣例中增加一個業務實現。

主要包含有以下這些步驟:

  1. 定義 EventCode
  2. 定義 Event
  3. 實現 EventHandler
  4. 註冊 EventHandler
  5. 修改 Grain 介面
  6. 實現 Grain
  7. 修改 Controller

這是一個從下向上的過程,實際的編碼過程中開發也可以自上而下進行實現。

定義 Event Code

EventCode 是 Claptrap 系統每個事件的唯一編碼。其在事件的識別,序列化等方面起到了重要的作用。

開啟HelloClaptrap.Models專案中的ClaptrapCodes類。

新增 “清空購物車事件” 的 EventCode。

  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; + public const string RemoveAllItemsFromCart = "remoeAllItems" + CartEventSuffix; } }

定義 Event

Event 是事件溯源的關鍵。用於改變 Claptrap 中的 State。並且 Event 會被持久化在持久層。

HelloClaptrap.Models專案的Cart/Events資料夾下建立RemoveAllItemsFromCartEvent類。

新增如下程式碼:

+ using Newbe.Claptrap;
+
+ namespace HelloClaptrap.Models.Cart.Events
+ {
+     public class RemoveAllItemsFromCartEvent : IEventData
+     {
+     }
+ }

由於在這個簡單的業務場景中,清空購物車不需要特定的引數。因此,只要建立空型別即可。

IEventData介面是框架中表示事件的空介面,用於在泛型推斷時使用。

實現 EventHandler

EventHandler用於將事件更新到 Claptrap 的State上。例如此次的業務場景,那麼 EventHandler 就負責將 State 購物車中的內容清空即可。

HelloClaptrap.Actors專案的Cart/Events資料夾下建立RemoveAllItemsFromCartEventHandler類。

新增如下程式碼:

+ using System.Threading.Tasks;
+ using HelloClaptrap.Models.Cart;
+ using HelloClaptrap.Models.Cart.Events;
+ using Newbe.Claptrap;
+
+ namespace HelloClaptrap.Actors.Cart.Events
+ {
+     public class RemoveAllItemsFromCartEventHandler
+         : NormalEventHandler<CartState, RemoveAllItemsFromCartEvent>
+     {
+         public override ValueTask HandleEvent(CartState stateData,
+             RemoveAllItemsFromCartEvent eventData,
+             IEventContext eventContext)
+         {
+             stateData.Items = null;
+             return new ValueTask();
+         }
+     }
+ }

這裡有一些常見的問題:

  1. NormalEventHandler 是什麼?

    NormalEventHandler 是框架定義的一個簡單基類,用於方便實現 Handler。
    其中第一個泛型引數是 Claptrap 對應的 State 型別。結合前篇文件中,我們的購物車 State 型別就是 CartState。
    第二個泛型引數是該 Handler 需要處理的 Event 型別。

  2. 為什麼用stateData.Items = null;而不用stateData.Items.Clear();

    stateData 是儲存在記憶體中的物件,Clear 不會縮小字典已佔用的自身記憶體。當然,一般一個購物車也不會有數十萬商品。但其實關鍵是在於,更新 State 時,需要注意的是 Claptrap 是一種常駐於記憶體中的物件,數量增加時會加劇記憶體的消耗。因此,儘可能在 State 中保持更少的資料。

  3. ValueTask 是什麼?

    可以通過這篇《Understanding the Whys, Whats, and Whens of ValueTask》進行了解。

EventHandler 實現完成之後,不要忘記對其進行單元測試。這裡就不羅列了。

註冊 EventHandler

實現並測試完 EventHandler 之後,便可以將 EventHandler 進行註冊,以便與 EventCode 以及 Claptrap 進行關聯。

開啟HelloClaptrap.Actors專案的CartGrain類。

使用 Attribute 進行標記。

  using Newbe.Claptrap;
  using Newbe.Claptrap.Orleans;

  namespace HelloClaptrap.Actors.Cart
  {
      [ClaptrapEventHandler(typeof(AddItemToCartEventHandler), ClaptrapCodes.AddItemToCart)]
      [ClaptrapEventHandler(typeof(RemoveItemFromCartEventHandler), ClaptrapCodes.RemoveItemFromCart)]
+     [ClaptrapEventHandler(typeof(RemoveAllItemsFromCartEventHandler), ClaptrapCodes.RemoveAllItemsFromCart)]
      public class CartGrain : ClaptrapBoxGrain<CartState>, ICartGrain
      {
          public CartGrain(
              IClaptrapGrainCommonService claptrapGrainCommonService)
              : base(claptrapGrainCommonService)
          {
          }

          ....

ClaptrapEventHandlerAttribute是框架定義的一個 Attribute,可以標記在 Grain 的實現類上,以實現 EventHandler 、 EventCode 和 ClaptrapGrain 三者之間的關聯。

關聯之後,如果在此 Grain 中產生的對應 EventCode 的事件將會由指定的 EventHandler 進行處理。

修改 Grain 介面

修改 Grain 介面的定義,才能夠提供外部與 Claptrap 的互操作性。

開啟HelloClaptrap.IActors專案的ICartGrain介面。

新增介面以及 Attribute。

  using System.Collections.Generic;
  using System.Threading.Tasks;
  using HelloClaptrap.Models;
  using HelloClaptrap.Models.Cart;
  using HelloClaptrap.Models.Cart.Events;
  using Newbe.Claptrap;
  using Newbe.Claptrap.Orleans;

  namespace HelloClaptrap.IActor
  {
      [ClaptrapState(typeof(CartState), ClaptrapCodes.CartGrain)]
      [ClaptrapEvent(typeof(AddItemToCartEvent), ClaptrapCodes.AddItemToCart)]
      [ClaptrapEvent(typeof(RemoveItemFromCartEvent), ClaptrapCodes.RemoveItemFromCart)]
+     [ClaptrapEvent(typeof(RemoveAllItemsFromCartEvent), ClaptrapCodes.RemoveAllItemsFromCart)]
      public interface ICartGrain : IClaptrapGrain
      {
          Task<Dictionary<string, int>> AddItemAsync(string skuId, int count);
          Task<Dictionary<string, int>> RemoveItemAsync(string skuId, int count);
          Task<Dictionary<string, int>> GetItemsAsync();
+         Task RemoveAllItemsAsync();
      }
  }

其中增加了兩部分內容:

  1. 標記了ClaptrapEvent,使得事件與 Grain 進行關聯。注意,這裡與前一步的ClaptrapEventHandler是不同的。此處標記的是 Event,上一步標記的是 EventHandler。
  2. 增加了 RemoveAllItemsAsync 方法,表示 “清空購物車” 的業務行為。需要注意的是 Grain 的方法定義有一定限制。詳細可以參見《Developing a Grain》

實現 Grain

接下來按照上一步的介面修改,來修改相應的實現類。

開啟HelloClaptrap.Actors專案中的Cart資料夾下的CartGrain類。

新增對應的實現。

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Threading.Tasks;
  using HelloClaptrap.Actors.Cart.Events;
  using HelloClaptrap.IActor;
  using HelloClaptrap.Models;
  using HelloClaptrap.Models.Cart;
  using HelloClaptrap.Models.Cart.Events;
  using Newbe.Claptrap;
  using Newbe.Claptrap.Orleans;

  namespace HelloClaptrap.Actors.Cart
  {
      [ClaptrapEventHandler(typeof(AddItemToCartEventHandler), ClaptrapCodes.AddItemToCart)]
      [ClaptrapEventHandler(typeof(RemoveItemFromCartEventHandler), ClaptrapCodes.RemoveItemFromCart)]
      [ClaptrapEventHandler(typeof(RemoveAllItemsFromCartEventHandler), ClaptrapCodes.RemoveAllItemsFromCart)]
      public class CartGrain : ClaptrapBoxGrain<CartState>, ICartGrain
      {
          public CartGrain(
              IClaptrapGrainCommonService claptrapGrainCommonService)
              : base(claptrapGrainCommonService)
          {
          }

+         public Task RemoveAllItemsAsync()
+         {
+             if (StateData.Items?.Any() != true)
+             {
+                 return Task.CompletedTask;
+             }
+
+             var removeAllItemsFromCartEvent = new RemoveAllItemsFromCartEvent();
+             var evt = this.CreateEvent(removeAllItemsFromCartEvent);
+             return Claptrap.HandleEventAsync(evt);
+         }
      }
  }

增加了對介面方法的對應實現。需要注意的有以下幾點:

  1. 一定要增加if (StateData.Items?.Any() != true)這行判斷。因為這可以明顯的減小儲存的開銷。

    事件在當執行Claptrap.HandleEventAsync(evt)便會持久化。而就此處的場景而言,如果購物車中原本就沒有內容,清空或者持久化這個事件只是增加開銷,而沒有實際的意義。
    因此,在此之前增加判斷可以減小儲存的無用消耗。

  2. 一定要判斷 State 以及傳入引數是否滿足事件執行的條件。

    這與上一點所描述的內容側重不同。上一點側重表明 “不要產生沒有意義的事件”,這一點表明 “絕不產生 EventHandler 無法消費的事件”。
    在事件溯源模式中,業務的完成是以事件的持久化完成作為業務確定完成的依據。也就是說事件只要入庫了,就可以認為這個事件已經完成了。
    而在 EventHandler 中,只能接受從持久化層讀出的事件。此時,按照事件的不可變性,已經無法再修改事件,因此一定要確保事件是可以被 EventHandler 消費的。所以,在Claptrap.HandleEventAsync(evt)之前進行判斷尤為重要。
    因此,一定要實現單元測試來確保 Event 的產生和 EventHandler 的處理邏輯已經被覆蓋。

  3. 此處需要使用到一些 TAP 庫中的一些方法,可以參見基於任務的非同步模式

修改 Controller

前面的所有步驟完成之後,就已經完成了 Claptrap 的所有部分。但由於 Claptrap 無法直接提供與外部程式的互操作性。因此,還需要在在 Controller 層增加一個 API 以便外部進行 “清空購物車” 的操作。

開啟HelloClaptrap.Web專案的Controllers資料夾下的CartController類。

  using System.Threading.Tasks;
  using HelloClaptrap.IActor;
  using Microsoft.AspNetCore.Mvc;
  using Orleans;

  namespace HelloClaptrap.Web.Controllers
  {
      [Route("api/[controller]")]
      public class CartController : Controller
      {
          private readonly IGrainFactory _grainFactory;

          public CartController(
              IGrainFactory grainFactory)
          {
              _grainFactory = grainFactory;
          }

+         [HttpPost("{id}/clean")]
+         public async Task<IActionResult> RemoveAllItemAsync(int id)
+         {
+             var cartGrain = _grainFactory.GetGrain<ICartGrain>(id.ToString());
+             await cartGrain.RemoveAllItemsAsync();
+             return Json("clean success");
+         }
      }
  }

小結

至此,我們就完成了 “清空購物車” 這個簡單需求的所有內容。

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

最後但是最重要!

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

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

聯絡方式:

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

  1. Newbe.Claptrap - 一套以 “事件溯源” 和 “Actor 模式” 作為基本理論的服務端開發框架
  2. 十萬同時線上使用者,需要多少記憶體?——Newbe.Claptrap 框架水平擴充套件實驗
  3. 談反應式程式設計在服務端中的應用,資料庫操作優化,從 20 秒到 0.5 秒
  4. 談反應式程式設計在服務端中的應用,資料庫操作優化,提速 Upsert
  5. docker-mcr 助您全速下載 dotnet 映象
  6. Newbe.Claptrap 專案週報 1 - 還沒輪影,先用輪跑
  7. Newbe.Claptrap 框架入門,第一步 —— 建立專案,實現簡易購物車
  8. Newbe.Claptrap 框架入門,第二步 —— 簡單業務,清空購物車
  9. Newbe.Claptrap 框架中為什麼用 Claptrap 和 Minion 兩個詞?

GitHub 專案地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 專案地址:https://gitee.com/yks/Newbe.Claptrap

您當前檢視的是先行釋出於www.newbe.pro上的部落格文章,實際開發文件隨版本而迭代。若要檢視最新的開發文件,需要移步http://claptrap.newbe.pro