使用DDD模式实现简单API(8/2)

EFCore实现领域模型的封装和持久化无视(转)

转自:http://www.kamilgrzybek.com/design/domain-model-encapsulation-and-pi-with-entity-framework-2-2/

代码资源:https://github.com/kgrzybek/sample-dotnet-core-cqrs-api(一个非常好的启动脚手架)

介绍
在上一篇文章中,表达了如何使用读模型和写模型实现简单的读写分离模式。这篇文章继续说明DDD模式的实现。在这片文章中,将会描述如何使用EFCore v2.2版本来支持纯净的领域模型。
我决定将持续开发github上的示例程序。我会逐渐地添加新的功能和技术方案,我也会尝试去扩展领域模型使其更接近于实际的应用情况,在小的领域很难解释清楚某些领域模式的概念。总之,我非常推荐你去查看github的代码库。
目标
当我创建领域模型时,我需要考虑很多事情,在这里我主要讨论其中的两点:封装和持久化无视。

封装

封装有两个主要的定义(来自维基)
A language mechanism for restricting direct access to some of the object’s components
以及
A language construct that facilitates the bundling of data with the methods (or other functions) operating on that data
什么是DDD Aggregates,简单来说就是我们需要把集成根内部的一切对外部世界进行隐藏,理想情况下,只暴露出完成业务逻辑需要的公共方法,如下图所示:

 

持久化无视
Persistence Ignorance (PI)规定领域模型应该不关注它的数据是如何存储和取得的,这是一个非常好的、值得遵循的建议。然而实际上,在微软文档中表达道:
Even when it is important to follow the Persistence Ignorance principle for your Domain model, you should not ignore persistence concerns. It is still very important to understand the physical data model and how it maps to your entity object model. Otherwise you can create impossible designs.
如上所述,领域的定义不能完全脱离数据库的结构。尽管如此,我们还是要尽可能地让领域模型和系统的其他部分解耦。
为了更好的理解如何创建领域模型,我创建了下图:

这是一个简单的电子商务领域模型,Customer可以下一个或多个Orders。Order是一组包含数量信息的Products,每个Product根据Currency定义了多个价格。
现在,我们理清了问题,接下来说解决方案:

解决方案


1、创建支持的架构
首先,最重要的事情是创建同时支持封装和持久化无视的应用框架,最常见的几个框架如下:

  • Clean Architecture
  • Onion Architecture
  • Ports And Adapters / Hexagonal Architecture

上述的框架都非常好且都在生产系统中应用过。对我来说Clean Architecture 和Onion Architecture是差不多的,Ports And Adapters / Hexagonal Architecture在命名上有些不同,但是基本概念是一样的。最重要的一点就是,领域模型在中间且不依赖其他的层,如下图:

那么在领域模型的实际编码过程中,如何体现呢:

  1. 没有数据访问代码
  2. 没有针对实体的数据注释
  3. 没有对其他框架类的继承,实体应该是Plain Old CLR Object,即纯C#代码。

 

2、只在基础架构层使用EFCore

任何与数据库的交互都应该在基础架构层实现,也就是说你必须在基础层添加EF上下文,实体匹配和仓储的实现。只有仓储的接口可以保留在领域模型层。

3、使用浅属性

Shadow Properties 是一种非常好的将实体和数据库解耦的方法,这些属性只在EF模型中定义,使用这个我们通常不需要在领域模型中包含外键,这是非常好的方式。

我们看一下Order实体和它定义在CustomerEntityTypeConfiguration中的匹配方法:

 1 public class Order : Entity
 2 {
 3     internal Guid Id;
 4     private bool _isRemoved;
 5     private MoneyValue _value;
 6     private List<OrderProduct> _orderProducts;
 7 
 8     private Order()
 9     {
10         this._orderProducts = new List<OrderProduct>();
11         this._isRemoved = false;
12     }
13 
14     public Order(List<OrderProduct> orderProducts)
15     {
16         this.Id = Guid.NewGuid();
17         this._orderProducts = orderProducts;
18 
19         this.CalculateOrderValue();
20     }
21 
22     internal void Change(List<OrderProduct> orderProducts)
23     {
24         foreach (var orderProduct in orderProducts)
25         {
26             var existingOrderProduct = this._orderProducts.SingleOrDefault(x => x.Product == orderProduct.Product);
27             if (existingOrderProduct != null)
28             {
29                 existingOrderProduct.ChangeQuantity(orderProduct.Quantity);
30             }
31             else
32             {
33                 this._orderProducts.Add(orderProduct);
34             }
35         }
36 
37         var existingProducts = this._orderProducts.ToList();
38         foreach (var existingProduct in existingProducts)
39         {
40             var product = orderProducts.SingleOrDefault(x => x.Product == existingProduct.Product);
41             if (product == null)
42             {
43                 this._orderProducts.Remove(existingProduct);
44             }
45         }
46 
47         this.CalculateOrderValue();
48     }
49 
50     internal void Remove()
51     {
52         this._isRemoved = true;
53     }
54 
55     private void CalculateOrderValue()
56     {
57         var value = this._orderProducts.Sum(x => x.Value.Value);
58         this._value = new MoneyValue(value, this._orderProducts.First().Value.Currency);
59     }
60 }
 1 internal class CustomerEntityTypeConfiguration : IEntityTypeConfiguration<Customer>
 2 {
 3     internal const string OrdersList = "_orders";
 4     internal const string OrderProducts = "_orderProducts";
 5 
 6     public void Configure(EntityTypeBuilder<Customer> builder)
 7     {
 8         builder.ToTable("Customers", SchemaNames.Orders);
 9         
10         builder.HasKey(b => b.Id);
11         
12         builder.OwnsMany<Order>(OrdersList, x =>
13         {
14             x.ToTable("Orders", SchemaNames.Orders);
15             x.HasForeignKey("CustomerId"); // Shadow property
16             x.Property<bool>("_isRemoved").HasColumnName("IsRemoved");
17             x.Property<Guid>("Id");
18             x.HasKey("Id");
19 
20             x.OwnsMany<OrderProduct>(OrderProducts, y =>
21             {
22                 y.ToTable("OrderProducts", SchemaNames.Orders);
23                 y.Property<Guid>("OrderId"); // Shadow property
24                 y.Property<Guid>("ProductId"); // Shadow property
25                 y.HasForeignKey("OrderId");
26                 y.HasKey("OrderId", "ProductId");
27 
28                 y.HasOne(p => p.Product);
29 
30                 y.OwnsOne<MoneyValue>("Value", mv =>
31                 {
32                     mv.Property(p => p.Currency).HasColumnName("Currency");
33                     mv.Property(p => p.Value).HasColumnName("Value");
34                 });
35             });
36 
37             x.OwnsOne<MoneyValue>("_value", y =>
38             {
39                 y.Property(p => p.Currency).HasColumnName("Currency");
40                 y.Property(p => p.Value).HasColumnName("Value");
41             });
42         });
43     }
44 }

在上面代码的第15行所定义的属性并不存在于Order实体中,他的存在是为了定义Customer和Order之间的关系,类似的还有第23、24行。

4、使用Owned Entity Types

使用Owned Entity Types 我们可以获得更好的封装,因为我们可以直接匹配私有和内部变量:

 1 public class Order : Entity
 2 {
 3     internal Guid Id;
 4     private bool _isRemoved;
 5     private MoneyValue _value;
 6     private List<OrderProduct> _orderProducts;
 7 
 8     private Order()
 9     {
10         this._orderProducts = new List<OrderProduct>();
11         this._isRemoved = false;
12     }
 1 x.OwnsMany<OrderProduct>(OrderProducts, y =>
 2 {
 3     y.ToTable("OrderProducts", SchemaNames.Orders);
 4     y.Property<Guid>("OrderId"); // Shadow property
 5     y.Property<Guid>("ProductId"); // Shadow property
 6     y.HasForeignKey("OrderId");
 7     y.HasKey("OrderId", "ProductId");
 8 
 9     y.HasOne(p => p.Product);
10 
11     y.OwnsOne<MoneyValue>("Value", mv =>
12     {
13         mv.Property(p => p.Currency).HasColumnName("Currency");
14         mv.Property(p => p.Value).HasColumnName("Value");
15     });
16 });
17 
18 x.OwnsOne<MoneyValue>("_value", y =>
19 {
20     y.Property(p => p.Currency).HasColumnName("Currency");
21     y.Property(p => p.Value).HasColumnName("Value");
22 });

Owned Types 也是创建Value Objects的一种好的办法,如下代码中MoneyValue的实现:

 1 public class MoneyValue
 2 {
 3     public decimal Value { get; }
 4 
 5     public string Currency { get; }
 6 
 7     public MoneyValue(decimal value, string currency)
 8     {
 9         this.Value = value;
10         this.Currency = currency;
11     }
12 }

5、匹配到私有字段

不仅可以使用EF的Owned Types匹配到私有字段,使用内置的属性同样可以,只需要给予字段的名称和列名就可以。

6、使用值转换

Value Conversions是数据库值和实体属性之间的桥梁,如果两者之间的类型不同,我们就需要使用这种方式。EF有很多在盒外实现的值转换器,另外我们也可以实现自定义的转换器。

1 public enum OrderStatus
2 {
3     Placed = 0,
4     InRealization = 1,
5     Canceled = 2,
6     Delivered = 3,
7     Sent = 4,
8     WaitingForPayment = 5
9 }
1 x.Property("_status").HasColumnName("StatusId").HasConversion
2 (new EnumToNumberConverter<OrderStatus, byte>());

这个转换器简单地将“StatusId”列转换为OrderStatus的私有字段_status。

总结

在这篇文章里,我简要描述了如何实现领域模型的封装和持久化无视,以及达到目标的方法:

  • 创建支持的架构
  • 将访问数据库的代码置于领域模型层之外
  • 使用EFCore的特性:浅属性、拥有类型、私有属性匹配、值转换器。

 

posted @ 2019-10-31 22:06  东方未  阅读(109)  评论(0)    收藏  举报