DDD實戰進階第一波:開發通常業務的大健康行業直銷系統前端
1、實現產品上下文領域層數據庫
從這篇文章開始,咱們根據前面的DDD理論與DDD框架的約束,正式進入直銷系統案例的開發。json
先簡單講下業務方面的需求:產品SPU與產品SKU,產品SPU主要是產品的名字和相關描述,產品SKU包括產品SPU的多個規格,每一個規格有不一樣的價格與PV值。從咱們對DDD概念的理解,產品SPU與產品SKU屬於同一個聚合,產品SPU是聚合根。小程序
產品上下文主要實現產品的上架功能,爲了實現上架功能,咱們首先要實現產品上下文的領域POCO模型與領域邏輯,咱們將產品的POCO模型與領域邏輯創建到一個叫Product.Domain的項目中。後端
產品SPU領域對象POCO代碼:微信小程序
public partial class ProductSPU : IAggregationRootapi
{ [Key] public Guid Id { get; set; } public string Code { get; set; } public string ProductSPUName { get; set; } public string ProductSPUDes { get; set; } public List<ProductSKU> ProductSKUS { get; set; } }
產品SKU領域對象POCO代碼:微信
public partial class ProductSKU : IEntity架構
{ public ProductSKU() { } [Key] public Guid Id { get; set; } public string Code { get; set; } public string Spec { get; set; } public Unit Unit { get; set; } public decimal PV { get; set; } public decimal DealerPrice { get; set; } public byte[] Image { get; set; } public Guid ProductSPUId { get; set; } public string ProductSPUName { get; set; } }
從上面代碼能夠看到,ProductSPU從聚合根接口繼承,ProductSKU從實體接口繼承,ProductSPU包含了一個ProductSKU的集合(也就是引用),這就表明它們同屬一個聚合,在具體使用EF Core作持久化時,會做爲一個事務統一持久化。app
領域對象除了包含自身的屬性,也應該包括自身的業務邏輯,產品上架的功能比較簡單,業務邏輯也比較簡單,主要就是如何生成整個領域對象,以及聚合根與實體業務標識符Code的生成規則。
產品SPU領域對象業務邏輯代碼:
public partial class ProductSPU
{ public ProductSPU CreateProductSPU(Guid id,string spuname,string spudesc,List<ProductSKU> productskus) { this.Id = id; this.Code = "Code " + spuname; this.ProductSPUName = spuname; this.ProductSKUS = productskus; this.ProductSPUDes = spudesc; return this; } }
產品SKU領域對象業務邏輯代碼:
public partial class ProductSKU
{ public ProductSKU CreateProductSKU(string productspuname,Guid productspuid, byte[] image,decimal dealerprice,decimal pv,string unit,string spec) { this.Id = Guid.NewGuid(); this.ProductSPUId = productspuid; this.Code = "Code " + productspuname + spec; this.ProductSPUName = productspuname; this.Image = image; this.DealerPrice = dealerprice; this.PV = pv; switch (unit) { case "盒": this.Unit = Unit.盒; break; case "包": this.Unit = Unit.包; break; case "瓶": this.Unit = Unit.瓶; break; } this.Spec = spec; return this; } }
我將領域對象的屬性與領域對象的邏輯分到不一樣的cs文件中,便於不一樣職責人開發與管理,並且採用相同的名稱空間和Partial關鍵字。
Product.Domain除了要實現領域邏輯以外,還要定義ProductSPU的倉儲接口、經過EF Core定義產品上下文與數據庫上下文之間的映射關係。
倉儲接口定義:
public interface IProductRepository
{ void CreateProduct<T>(T productspu) where T : class, IAggregationRoot; }
從上面能夠看到,這個接口其實就是定義了將ProductSPU這個聚合根持久化到數據庫與的接口。
產品上下文與數據庫上下文映射關係:
1.由於映射關係使用EF Core實現,將來可能被替換掉,因此先定義一個產品上下文接口:
public interface IProductContext
{ }
2.EF Core映射實現
public class ProductEFCoreContext:DbContext,IProductContext
{ public DbSet<ProductSPU> ProductSPU { get; set; } public DbSet<ProductSKU> ProductSKU { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionBuilder) { optionBuilder.UseSqlServer("數據庫鏈接字符串"); } }
3.使用EF Core工具生成數據庫腳本並更新數據庫,在生成腳本時,須要編輯項目文件,並採用EF Core Tools命令生成,這裏就不細講EF Core技術方面的內容。
到這裏,咱們就基本實現了產品上下文的領域層,能夠看到領域層主要是領域邏輯,定義了一個倉儲接口,將數據庫技術解耦,固然要定義領域對象與數據庫之間的映射關係,不然用例沒法完成真正對領域對象的持久化。
2、實現產品上下文倉儲與應用服務層
前面咱們完成了產品上下文的領域層,已經有了關於產品方面的簡單領域邏輯,咱們接着來實現產品上下文關於倉儲持久化與應用層的用例如何來協調領域邏輯與倉儲持久化。
首先你們須要明確的是,產品上下文的領域邏輯是系統的核心,它不該該依賴倉儲,而倉儲應該要依賴領域層,這樣倉儲才能夠把領域邏輯執行完後,纔可能將領域對象持久化到數據庫中,這一點與傳統的架構有本質的區別。
通常咱們會在解決方案中創建一個項目,這個項目就是包含了全部聚合的倉儲實現,具體不一樣上下文的倉儲實現,能夠在這個項目下創建不一樣的文件夾。
1.產品上下文倉儲實現:
public class ProductEFCoreRepository : IProductRepository
{ private readonly DbContext context; public ProductEFCoreRepository(DbContext context) { this.context = context; } public void CreateProduct<T>(T productspu) where T:class,IAggregationRoot { var productdbcontext = this.context as ProductEFCoreContext; var productspunew = productspu as ProductSPU; try { productdbcontext.ProductSPU.Add(productspunew); } catch(Exception error) { throw error; } } }
上面的代碼有幾個要注意的方面:
a.首先會從產品的倉儲接口作繼承,經過EF Core的機制,實現了倉儲接口的CreateProduct方法。
b.使用了產品上下文的EF Core數據訪問上下文ProductEFCoreContext完成了Productspu的數據庫預添加。
c.上一個說法中,可能你們有兩個疑惑,一是爲何不使用productdbcontext標記ProductSPU爲Added狀態,而是使用.Add方法,二是爲何只是完成了添加狀態,而再也不後續調用Commit或SaveChange方法真正持久化到數據庫中?首先,由於將來持久化要將這個聚合中的ProductSPU聚合根與ProductSKU實體做爲一個總體持久化到數據庫中,而Added狀態只能將當前聚合根做爲添加狀態,而不能同時將引用的ProductSKU對象做爲添加狀態,因此不能使用Added狀態而使用.Add方法;其次倉儲實現聚合提交時,只進行數據庫預添,是由於協調領域邏輯與倉儲的應用服務層用例可能涉及到多個聚合,因此可能要同時調用多個領域對象的業務邏輯,多個倉儲,完成後,將多聚合做爲一個總體事務作提交,因此真正的提交應該放到應用服務層更合適,而不是倉儲層。
2.產品上架應用服務層實現:
應用服務層實際就是完成用例,經過應用服務層調用領域邏輯,而後經過應用服務層調用倉儲,最後應用服務層作真正的提交,這樣就把職責分的很是清楚,也在領域邏輯不依賴倉儲的前提下,完成了整個用例和持久化。
a.首先咱們在產品上下文的應用服務層項目中,創建須要添加的產品SPU與對應產品SKU的DTO對象
public class AddProductSPUDTO
{ public string SPUName { get; set; } public string SPUDesc { get; set; } public List<string> SKUSpecs { get; set; } public List<string> SKUUnits { get; set; } public List<decimal> SKUDealerPrices { get; set; } public List<byte[]> SKUImages { get; set; } public List<decimal> SKUPvs { get; set; } }
b.創建一個上架產品的用例服務,協調領域邏輯與倉儲完成用例
public class AddProductSPUUseCase:BaseAppSrv
{ private readonly IRepository irepositorycontext; private readonly IProductRepository iproductrepository; public AddProductSPUUseCase(IRepository irepositorycontext,IProductRepository iproductrepository) { this.irepositorycontext = irepositorycontext; this.iproductrepository = iproductrepository; } public ResultEntity<bool> AddProduct(AddProductSPUDTO addproductspudto) { var productspuid = Guid.NewGuid(); var productskus = new List<ProductSKU>(); for(int i = 0; i < addproductspudto.SKUSpecs.Count; i++) { var productsku = new ProductSKU().CreateProductSKU(addproductspudto.SPUName, productspuid, addproductspudto.SKUImages[i], addproductspudto.SKUDealerPrices[i], addproductspudto.SKUPvs[i], addproductspudto.SKUUnits[i], addproductspudto.SKUSpecs[i]); productskus.Add(productsku); } var productspu = new ProductSPU().CreateProductSPU(productspuid, addproductspudto.SPUName, addproductspudto.SPUDesc, productskus); try { using (irepositorycontext) { iproductrepository.CreateProduct(productspu); irepositorycontext.Commit(); } return GetResultEntity(true); } catch(Exception error) { throw error; } } }
BaseAppSrv是你要定義的一個類,它的GetResultEntity方法功能是完成用例後後,返回接口層的數據格式,這個數據格式會進一步經過接口層返回給前端,返回的數據格式就是ResultEntity<T>,這兩個部分你們能夠本身去實現,也能夠參考個人微信公衆號中的課程。
3、實現產品上下文接口與測試
咱們介紹瞭如何將建立產品的領域邏輯與產品的持久化倉儲經過上架產品的用例組織起來,完成了一個功能。在實際的項目中,多種前端的形態好比PC Web、微信小程序、原生APP等要調用後端的功能,一般要將後端的功能包裝成RESTFUL風格,這樣前端就可使用Http Get或Post方式調用後端的功能,如今咱們先來完成後端的Asp.net Core WebApi,經過WebApi將上架產品的功能暴露出去。
實現上下產品接口:
[Produces("application/json")]
[Route("api/Product")] public class ProductController : Controller { ServiceLocator servicelocator = new ServiceLocator(); [HttpPost] [Route("AddProduct")] public ResultEntity<bool> AddProduct([FromBody] AddProductSPUDTO addproductspudto) { var result = new ResultEntity<bool>(); var productdbcontext = servicelocator.GetService<IProductContext>(); var irepository = servicelocator.GetService<IRepository>(new ParameterOverrides { { "context", productdbcontext } }); var iproductrepository=servicelocator.GetService<IProductRepository>(new ParameterOverrides { { "context", productdbcontext } }); var addproductspuusecase = new AddProductSPUUseCase(irepository,iproductrepository); try { result = addproductspuusecase.AddProduct(addproductspudto); result.IsSuccess = true; result.Count = 1; result.Msg = "上架產品成功!"; } catch(Exception error) { result.ErrorCode = 100; result.Msg = error.Message; } return result; } }
1.首先你們看到接口層是很是薄的一層,它並不包含業務邏輯和數據訪問,它只是初始化一些對象,而後完成應用服務的調用,返回前端所須要的格式的對象。
2.產品數據訪問上下文、倉儲接口、產品上下文倉儲接口等須要經過依賴注入框架來獲取特定的實現類,依賴注入框架能夠採用Asp.net Core自帶的,也能夠採用Unity等框架。這裏略去了依賴注入框架的具體實現,能夠在公衆號內查看。
3.若是在調用應用服務可能拋出異常時,須要詳細指明每一個catch與拋出的內容。
當後端接口完成後,做爲後端開發人員,咱們須要寫單元測試來完成對後端接口的調用,並嘗試獲得指望的結果。咱們在這裏採用MSTest,你也可使用XUnit。
上架產品單元測試:
HttpClient httpclient;
[TestMethod] public void AddProductTest() { httpclient = new HttpClient(); var addproductspudto = new AddProductSPUDTO(); addproductspudto.SPUName = "XXX石榴露"; addproductspudto.SPUDesc = "XXX精華石榴露,用於養生"; addproductspudto.SKUSpecs = new List<string>(); addproductspudto.SKUSpecs.Add("每瓶50毫升"); addproductspudto.SKUSpecs.Add("每瓶100毫升"); addproductspudto.SKUUnits = new List<string>(); addproductspudto.SKUUnits.Add("瓶"); addproductspudto.SKUUnits.Add("瓶"); addproductspudto.SKUPvs = new List<decimal>(); addproductspudto.SKUPvs.Add(120); addproductspudto.SKUPvs.Add(300); addproductspudto.SKUDealerPrices = new List<decimal>(); addproductspudto.SKUDealerPrices.Add(3000); addproductspudto.SKUDealerPrices.Add(4000); var fs = new FileStream(@"c:\test.jpg", FileMode.Open, FileAccess.Read); var imgbytes = new byte[fs.Length]; fs.Read(imgbytes, 0, Convert.ToInt32(fs.Length)); fs.Close(); addproductspudto.SKUImages = new List<byte[]>(); addproductspudto.SKUImages.Add(imgbytes); addproductspudto.SKUImages.Add(imgbytes); string json = JsonConvert.SerializeObject(addproductspudto); HttpContent httpcontent = new StringContent(json); httpcontent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); var response = httpclient.PostAsync("http://localhost:2209/api/Product/AddProduct", httpcontent).Result; var responsevalue = response.Content.ReadAsStringAsync().Result; var responsemsg = JsonConvert.DeserializeObject<ResultEntity<bool>>(responsevalue).Msg; Assert.AreEqual("上架產品成功!", responsemsg); }
[TestMethod]
public void AddProductTest() { httpclient = new HttpClient(); var addproductspudto = new AddProductSPUDTO(); addproductspudto.SPUName = "XXX面膜"; addproductspudto.SPUDesc = "XXX面膜,用於護膚"; addproductspudto.SKUSpecs = new List<string>(); addproductspudto.SKUSpecs.Add("每盒5張"); addproductspudto.SKUSpecs.Add("每盒10張"); addproductspudto.SKUUnits = new List<string>(); addproductspudto.SKUUnits.Add("盒"); addproductspudto.SKUUnits.Add("盒"); addproductspudto.SKUPvs = new List<decimal>(); addproductspudto.SKUPvs.Add(200); addproductspudto.SKUPvs.Add(350); addproductspudto.SKUDealerPrices = new List<decimal>(); addproductspudto.SKUDealerPrices.Add(5000); addproductspudto.SKUDealerPrices.Add(8000); var fs = new FileStream(@"c:\test1.jpg", FileMode.Open, FileAccess.Read); var imgbytes = new byte[fs.Length]; fs.Read(imgbytes, 0, Convert.ToInt32(fs.Length)); fs.Close(); addproductspudto.SKUImages = new List<byte[]>(); addproductspudto.SKUImages.Add(imgbytes); addproductspudto.SKUImages.Add(imgbytes); string json = JsonConvert.SerializeObject(addproductspudto); HttpContent httpcontent = new StringContent(json); httpcontent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); var response = httpclient.PostAsync("http://localhost:2209/api/Product/AddProduct", httpcontent).Result; var responsevalue = response.Content.ReadAsStringAsync().Result; var responsemsg = JsonConvert.DeserializeObject<ResultEntity<bool>>(responsevalue).Msg; Assert.AreEqual("上架產品成功!", responsemsg); }
有了單元測試,咱們後端開發人員就能夠驗證是否後端接口與整個用例是不是正常的,另外單元測試也能夠做爲每日自動構建的一部分。