Procházet zdrojové kódy

Merge branch 'master' of 47.103.61.198:darren/DDWiki

master
qdjjx před 4 roky
rodič
revize
39286f3086

+ 259
- 0
实践/后端/项目/24.DDD领域驱动基本概念.md Zobrazit soubor

@@ -0,0 +1,259 @@
1
+>  领域驱动设计
2
+- 思想体系
3
+   - DDD 是一种开发思想体系,旨在管理为复杂问题域编写的软件的创建和维护工作。
4
+   - 专注领域要高于其他一切需求。
5
+   - DDD是模式、原则和实践的集合,可以被用到软件设计以管理
6
+- 模式类型
7
+   - 战略模式:对任何应用程序都有用 重点提炼问题域(核心子域)->塑造应用程序架构
8
+   - 战术模式:用于实现富领域模型(只当模型在领域逻辑做够丰富时才有用)
9
+- 实践原则
10
+   - 专注于核心领域 强调在核心子域付出最多努力的需要
11
+   -  通过协作学习 开发团队与业务专家的协作
12
+   - 通过探索和实验来创建模型 
13
+   - 通信 公共语言(UL)
14
+   - 理解模型的活用性 语言边界,避免歧义
15
+   - 让模型持续发展 持续致力与知识提炼
16
+
17
+> 通用语言
18
+- 抛开DDD 想到通用语言首先想到英语 特点:1)简单易学 2)使用效率高 3)国际通用
19
+- 概念:通过团队交流达成共识的能够准确传递业务规则的简单的语言
20
+- 价值 解决了交流障碍问题,使领域专家和开发人员协同合作
21
+
22
+> 领域(重在范围的界限)
23
+- 本质 :可以理解为 就是一个问题域,只要是同一个领域,那问题域就相同。所以,只要我们确定了系统所属的领域,那这个系统的核心业务,既要解决的关键问题、问题的范围边界就基本确定了。
24
+- 与传统开发过程的区别在于 DDD注重领域建模
25
+- 领域拆分: 子域、核心域、通用子域、支撑子域
26
+- 举个例子(比如创建个电商网站 需要涉及的业务。商品、用户、订单、报价、支付、物流、保修相关业务)
27
+   - 大致拆分:商品子域、用户子域、销售子域、订单子域、支付子域、物流子域、维修子域
28
+   - 确定核心域:开发电商网站的目的是为了寻求推广和销售利润的最大化。所以核心域就是销售子域。
29
+   - 通用子域:服务服务于整个业务领域。-》日志子域  可以为该网站提供一个日志系统,来记录一些日志。我们可以设计一个日志子域来供其他子域使用。
30
+   - 支撑子域:作用于业务系统的某些重要业务而非核心业务,它关注于业务的某一方面,来支撑完善业务系统。
31
+      - 我们划分的子域中除了销售子域,其他都可以说是支撑子域。
32
+      - 比如物流子域,就是专注于物流相关业务,支撑着订单发货以及物流跟踪的重要流程。
33
+
34
+
35
+> 界限上下文
36
+- 可拆分为两个词,界限和上下文。 界限是指一个界限,具体的某一个范围。 上下文可理解为语境。
37
+- 例子 还是上述(电子商务)例子
38
+    - 在销售子域和商品子域中就是商品
39
+    - 在物流子域中特质货物,只关注部分属性
40
+- 命名 领域名+上下文 销售上下文 物流上下文
41
+
42
+> 领域模型
43
+- 概念:描述正在解决的问题及提出的解决方案
44
+- 特征:
45
+   - 综合了系统分析和设计
46
+   - 语言、模型、代码 三者紧密绑定
47
+   - 可以通过UML类图来展示
48
+- 例子 在线商城案例
49
+```
50
+Customer
51
+public class Customer
52
+{
53
+    public int Id{get;set;}
54
+    public string Name{get;set;}
55
+    public string BillingAddress{get;set;}
56
+    public string DeliveryAddress{get;set;}
57
+    Public string LoginName{get;set;}
58
+    public string LoginPassword{get;set;}
59
+    public string DayOfBirth{get;set;}
60
+}
61
+order
62
+public class Order
63
+{
64
+    public int Id{get;set;}
65
+    public string CreatedDate{get;set;}
66
+    public string CreatBy{get;set;}
67
+}
68
+OrderLine
69
+public class OrderLine
70
+{
71
+    public int Id{get;set;}
72
+    public string Quantity{get;set;}
73
+    public string Discount{get;set;}
74
+}
75
+Category
76
+public class Category
77
+{
78
+    public int Id{get;set;}
79
+    public string Name{get;set;}
80
+    public string Description{get;set;}
81
+}
82
+CreditCard
83
+public class CreditCart
84
+{
85
+    public int Id{get;set;}
86
+    public string Number{get;set;}
87
+    public string HolderName{get;set;}
88
+    public string ExpirationDate{get;set;}
89
+}
90
+Item
91
+public class Item
92
+{
93
+    public int Id{get;set;}
94
+    public string Name{get;set;}
95
+    public string Description{get;set;}
96
+    public string PurchasePrice{get;set;}
97
+    public string SalesPrice{get;set;}
98
+
99
+}
100
+```  
101
+
102
+> 实体 (Entity)
103
+- 概念: 唯一身份标识+可变性[状态(属性)+行为(方法或领域事件或领域服务)]
104
+-  唯一标识: 针对上述例子 抽象出User实体 要定义其唯一标识。
105
+   - 性格、外貌、昵称、身份证号都可以是User的属性,但为了确保标识的稳定性,智能用身份证号设为唯一身份标识。
106
+   - 唯一标识的类型
107
+      - 一个简单的应用程序里,一个int类型的自增Id就可以作为唯一标识。优点就是占用空间小,查询速度快。
108
+      - 而在一些业务当中,要求唯一标识有意义,通过唯一标识就能识别出一些基本信息,比如支付宝的交易号,其中就包含了日期和用户ID。这种就属于字符串类型的标识,这就对唯一标识的生成提出了挑战。
109
+
110
+   - 唯一标识的生成时机 
111
+     - 即时生成,即在持久化实体之前,先申请唯一标识,再更新到数据库。
112
+     - 延迟生成,即在持久化实体之后。
113
+   -  委派标识和领域标识
114
+     - 基于领域实体概念分析确定的唯一身份标识,我们可以称为领域实体标识
115
+     - 实现层超类型
116
+       - 定义层超类型接口
117
+       ```
118
+       public interface IEntity
119
+       {
120
+
121
+       }
122
+       public interface IEntity<TPrimaryKey> : IEntity
123
+       {
124
+           TPrimaryKey Id { get; set; }
125
+       }
126
+
127
+       ```
128
+       - 实现层超类型
129
+       ```
130
+       public class Entity : Entity<int>, IEntity
131
+       {
132
+
133
+       }
134
+       public class Entity<TPrimaryKey> : IEntity<TPrimaryKey>
135
+       {
136
+            public virtual TPrimaryKey Id { get; set; }
137
+            public override bool Equals(object obj)
138
+            {
139
+                if (obj == null || !(obj is Entity<TPrimaryKey>))
140
+                {
141
+                    return false;
142
+                }
143
+                if (ReferenceEquals(this, obj))
144
+                {
145
+                    return true;
146
+                }
147
+                var other = (Entity<TPrimaryKey>) obj;
148
+                var typeOfThis = GetType();
149
+                var typeOfOther = other.GetType();
150
+                if (!typeOfThis.GetTypeInfo().IsAssignableFrom(typeOfOther) && !typeOfOther.GetTypeInfo().IsAssignableFrom(typeOfThis))
151
+                {
152
+                    return false;
153
+                }
154
+                return Id.Equals(other.Id);
155
+            }
156
+            public override int GetHashCode()
157
+            {
158
+                 return Id.GetHashCode();
159
+            }
160
+            public static bool operator ==(Entity<TPrimaryKey> left, Entity<TPrimaryKey> right)
161
+            {
162
+                if (Equals(left, null))
163
+                {
164
+                   return Equals(right, null);
165
+                }
166
+                return left.Equals(right);
167
+            }
168
+            public static bool operator !=(Entity<TPrimaryKey> left, Entity<TPrimaryKey> right)
169
+            {
170
+                return !(left == right);
171
+            }
172
+       }
173
+       ```
174
+    - 可变性 
175
+      - 实体的状态 eg:订单状态:未支付、正常、已发货、关闭
176
+      - 实体的行为 eg:订单行为:支付、发货、关闭
177
+
178
+
179
+> 值对象
180
+- 概念:值+对象=》将一个值用对象的方式进行标书,表达一个具体的固定不变的概念
181
+- 值特征:
182
+    - 表示一个具体的概念
183
+    - 通过值得属性对其识别
184
+    - 属性判断
185
+    - 固定不变
186
+- 案例分析(购物网站都会维护客户收货地址信息来进行发货处理,一个地址信息一般主要包含省份、城市、区县、街道、邮政编码信息。)
187
+   - 地址是一个值,不会随着时间而变化,它包含了地址所需要的完整属性(省份、城市、区县、街道、邮政编码)
188
+   ```
189
+   public class Address
190
+   {
191
+        /// <summary>
192
+        /// 省份
193
+        /// </summary>
194
+        public string Province { get; private set; }
195
+
196
+        /// <summary>
197
+        /// 城市
198
+        /// </summary>
199
+        public string City { get; private set; }
200
+
201
+        /// <summary>
202
+        /// 区县
203
+        /// </summary>
204
+        public string County { get; private set; }
205
+
206
+        /// <summary>
207
+        /// 街道
208
+        /// </summary>
209
+        public string Street { get; private set; }
210
+
211
+        /// <summary>
212
+        /// 邮政编码
213
+        /// </summary>
214
+        public string Zip { get; private set; }
215
+
216
+        public Address(string province, string city,
217
+        string county, string street, string zip)
218
+        {
219
+            this.Province = province;
220
+            this.City = city;
221
+            this.County = county;
222
+            this.Street = street;
223
+            this.Zip = zip;
224
+        }
225
+
226
+        public override bool Equals(object obj)
227
+        {
228
+            bool isEqual = false;
229
+            if (obj != null && this.GetType() == obj.GetType())
230
+            {
231
+                var that = obj as Address;
232
+                isEqual = this.Province == that.Province
233
+                    && this.City == that.City
234
+                    && this.County == that.County 
235
+                    && this.Street == that.Street 
236
+                    && this.Zip == that.Zip;
237
+            }
238
+            return IsEqual;
239
+        }
240
+        public override int GetHashCode()
241
+        {
242
+            return this.ToString().GetHashCode();
243
+        }
244
+        public override string ToString()
245
+        {
246
+            string address = $"{this.Province}{this.City}" +
247
+                  $"{this.County}{this.Street}({this.Zip})";
248
+            return address;
249
+
250
+        }      
251
+   }
252
+   ```
253
+   - 作用
254
+     - 符合通用语言,更简单明了的表达简单业务概念
255
+     - 简化设计,减少不必要的数据库表设计
256
+       - 值对象不会孤立存在,可作为所属实体的数据列
257
+       - 多个值对象序可以劣化到单个列
258
+
259
+

+ 310
- 0
实践/后端/项目/25.DDD仓储工厂等概念.md Zobrazit soubor

@@ -0,0 +1,310 @@
1
+> 应用服务&领域服务
2
+
3
+- 服务是行为的抽象
4
+- 应用层(Application):负责展现层与领域层之间的协调,协调业务对象来执行特定的应用程序任务。它不包含业务逻辑。
5
+- 领域层(Domain):负责表达业务概念,业务状态信息以及业务规则,是业务软件的核心。
6
+
7
+- 应用服务:表达用例和用户故事的主要手段  eg:购物车结算=》应用行为
8
+    -  应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。
9
+
10
+- 领域服务 实现了全部业务逻辑并且通过各种校验手段保证业务正确性
11
+  - 当领域中的某个操作过程或转换过程不是实体或值对象的职责时,我们便应该将该操作放在一个单独的接口中,即领域服务。请确保该服务和通用语言时一致的;并且保证它是无状态的。
12
+  - 领域服务是用来协调领域对象完成某个操作,用来出路业务逻辑,它本身是一个行为,所以是无状态的。状态由领域对象保存。
13
+  - eg:比如转账操作问题 业务用例
14
+     - 1.检查账号余额是否足够
15
+     - 2.检查目标账户账号是否合法
16
+     - 3.转账
17
+     - 4.短信通知转账双方
18
+- 总结:
19
+  - 1.服务是行为的抽象
20
+  - 2.应用服务通过委托领域对象和领域服务来表达用例和用户故事。
21
+  - 3.领域对象(实体和值对象)负责单一操作。
22
+  - 4.领域服务用于协调多个领域对象共同完成某个业务操作。
23
+  - 5.应用服务不处理业务逻辑,领域服务处理业务逻辑。
24
+
25
+
26
+> 领域事件
27
+
28
+- 领域事件作为领域模型的重要部分,是领域建模的工具之一。
29
+- 用来捕获领域中已经发生的事情
30
+- 并不是领域中所有发生的事情都要建模为领域事件,要忽略无业务价值的事件
31
+- `当用户在购物车点击结算时,生成待付款订单,若支付成功,则更新订单状态为已支付,扣减库存,并推送捡货通知信息到捡货中心。 订单支付成功就是一个领域事件 `
32
+
33
+
34
+- 建模领域事件
35
+   - 使用领域事件来解耦方法:当然是封装不变,应对万变。上例 不变的是订单支付成功这个事件,变化的是针对这个事件不同处理手段。
36
+
37
+   - 抽象事件源 事件源应该至少包含事件发生的时间和触发事件的对象。
38
+   ```
39
+   public interface IEventData
40
+   {
41
+       /// <summary>
42
+       /// 事件发生的时间
43
+       /// </summary>
44
+       DateTime EventTime{get;set;}
45
+
46
+       /// <summary>
47
+       /// 触发事件的对象
48
+       /// </summary>
49
+       object EventSource{get;set;}
50
+   }
51
+   通过实现IEventData我们可以根据自己的需要添加自定义的事件属性。
52
+   ```
53
+   - 抽象事件处理
54
+    ```
55
+     针对事件处理 提取IEventHandler接口
56
+
57
+     /// <summary>
58
+     /// 定义事件处理器公共接口,所有的事件处理都要实现该接口
59
+     /// </summary>
60
+     public interface IEventHandler
61
+     {
62
+
63
+     }
64
+     事件处理要与事件源进行绑定,所以我们再来定义一个泛型接口
65
+
66
+     /// <summary>
67
+     /// 泛型事件处理器接口
68
+     /// </summary>
69
+     /// <typeparam name="TEventData"></typeparam>
70
+      public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData
71
+      {
72
+            /// <summary>
73
+            /// 事件处理器实现该方法来处理事件
74
+            /// </summary>
75
+            /// <param name="eventData"></param>
76
+            void HandleEvent(TEventData eventData);
77
+      }
78
+     ```
79
+
80
+    - 领域事件的发布和订阅
81
+      - 领域事件的发布可以使用`发布-订阅模式`来实现。比较常见的实现法师就是事件总线
82
+      - 事件总线实现的要点
83
+         - 事件总线维护一个事件源与事件处理的映射字典;
84
+         - 通过单例模式,确保事件总线的唯一入口;
85
+         - 利用反射或依赖注入完成事件源与事件处理的初始化绑定
86
+         - 提供统一的事件注册、取消注册和触发接口
87
+      - 事件总线的接口定义
88
+       ```
89
+       public interface IEventBus
90
+       {
91
+           void Register < TEventData > (IEventHandler eventHandler);
92
+
93
+            void UnRegister < TEventData > (Type handlerType) where TEventData: IEventData;
94
+
95
+             void Trigger < TEventData > (Type eventHandlerType, TEventData eventData) where TEventData: IEventData;
96
+
97
+       }
98
+       在应用服务和领域服务中,我们都可以直接调用Register方法来完成领域事件的注册,调用Trigger方法来完成领域事件的发布。
99
+
100
+       ```
101
+- 最终一致性
102
+   -  务一致性是是数据库事务的四个特性之一,也就是ACID特性之一:
103
+   -  原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
104
+   -  一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。
105
+   -  隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行
106
+   -  持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。
107
+ - 案例 
108
+   - 订单系统发布"订单成功支付"事件
109
+   - 库存系统订阅并处理库存扣减逻辑
110
+   - 通知系统订阅并处理检货通知
111
+- 事件存储
112
+   - 事件发布可用于重新发布
113
+   - 通过消息中间件去分布事件
114
+   - 事件溯源
115
+
116
+
117
+
118
+> 聚合
119
+- 在DDD中,聚合也可以用来表示整体与部分的关系。领域对象的显示分组(订单+订单项)
120
+   - 封装业务
121
+   - 保证聚合内领域对象的数据一致性
122
+
123
+- 关键
124
+  - 领域对象间的关联关系 
125
+    - 基于业务用例而非现实生活建立必要关联
126
+    - 减少不必要的关联
127
+    - 将双向关联转换为单项关联
128
+- 聚合是领域对象的显式分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。
129
+  - 领域对象的显式分组
130
+  - 领域行为和不变性
131
+  - 一致性和事务性边界
132
+
133
+- 一致性边界
134
+  - 实例:订单支付成功后,订单状态要更新为已支付状态,且现有库存要根据订单中商品实际销售数量进行扣减。
135
+  - 事务一致性
136
+  - 最终一致性:实现原理是借助领域事件来完成事务的拆分
137
+
138
+
139
+
140
+
141
+> 工厂
142
+
143
+- DDD中工厂的主要目标是隐藏对象的复杂创建逻辑;次要目标就是要清楚的表达对象实例化的意图。
144
+- 工厂模式的实现四种方式
145
+  - 简单工厂:简单实用,但违反开放封闭
146
+  - 工厂方法:开放封闭,单一产品
147
+  - 抽象工厂:开放封闭,多个产品
148
+  - 反射工厂:可以最大限度的解耦
149
+
150
+- 封装内部结构
151
+
152
+  - 以添加商品到购物车为例。 步骤 1、 加载用户购物车 2、获取商品税率 3、创建新的购物车子项
153
+```
154
+  相关应用层代码
155
+  public class AddProductToBasket {
156
+      public void Add (Product product, Guid basketId) {
157
+        public void Add (Product product, Guid basketId) {
158
+            var basket = _basketRepository.FindBy (basketId);
159
+            basket.Add (product);
160
+      }
161
+
162
+  }
163
+  public class Basket {
164
+      public void Add (Product product) {
165
+            if (Contains (product))
166
+                GetItemFor (product).IncreaseItemQuantitBy (1);
167
+            else {
168
+                var rate = TaxRateService.ObtainTaxRateFor (product.Id,
169
+                    country.Id);
170
+                var item = new BasketItem (rate, product.Id, product.price);
171
+                _items.Add (item);
172
+  }
173
+```
174
+
175
+ ```
176
+   引入一个工厂对象来封装购物车子项的创建,包括获取正确的税率
177
+ namespace DomainModel {
178
+    public class Basket {
179
+        // ......
180
+        public void Add (Product product) {
181
+            if (Contains (product))
182
+                GetItemFor (product).IncreaseItemQuantitBy (1);
183
+            else
184
+                _items.Add (BasketItemFactory.CreateItemFor (product,
185
+                    deliveryAddress));
186
+        }
187
+    }
188
+    public class BasketItemFactory {
189
+        public static void CreateBasketFrom (Product product, Country country) {
190
+            var rate = TaxRateService.ObtainTaxRateFor (product.Id, country.Id);
191
+            return new BasketItem (rate, product.Id, product.price);
192
+        }
193
+    }
194
+ }
195
+ 引入工厂模式后,购物车的职责单一了,且隔离了来自购物车子项的变化,比如当税率变化时,或购物车子项需要其他信息创建时,都不会影响到购物车的相关逻辑。
196
+```
197
+
198
+- 还用工厂的好处
199
+ - 工厂将领域对象的使用和创建分离。
200
+ - 通过使用工厂类,可以隐藏创建复杂领域对象的业务逻辑。
201
+ - 工厂类可以根据调用者的需要,创建相应的领域对象。
202
+ - 工厂方法可以封装聚合的内部状态。
203
+
204
+
205
+> 仓储
206
+- 特性 :仓储用来存储和删除聚合,但同时提供针对聚合的显式查询以及汇总。
207
+  - 存储集合
208
+  - 删除集合
209
+- 仓储与数据访问层的区别
210
+  - 仓储限定了只能通过聚合根来持久化和检索领域对象,以确保所有改动和不变性由聚合处理。
211
+  - 仓储通过隐藏聚合持久化和检索的底层技术实现领域层的的持久化无关性(即领域层不需要知道如何持久化领域对象)。
212
+  - 仓储在数据模型和领域模型定义了一个边界。
213
+
214
+- 仓储举例
215
+ ```
216
+ 简单的仓储定义
217
+  public interface ICustomerRepository
218
+  {
219
+      Customer FindBy(Guid id);
220
+      void Add(Customer customer);
221
+      void Remove(Customer customer); 
222
+  }
223
+ ```
224
+
225
+- 仓储的要点
226
+  - 是保持你的领域模型和技术持久化框架的独立性,这样你的领域模型可以隔离来自底层持久化技术的影响。如果没有仓储这一层,你的持久化基础设施可能会泄露到领域模型中,并影响领域模型完整性和最终一致性
227
+
228
+
229
+- 仓储方法需要明确
230
+仓储是原则上是领域模型与持久化存储之间明确的契约,仓储定义的接口方法不仅仅是CURD方法。它是领域模型的扩展,并以领域专家所理解的术语编写。仓储接口的定义应该根据应用程序的用例需求来创建,而不是从类似CURD的数据访问角度来构建。
231
+
232
+- 泛型仓储
233
+  - 举例
234
+  ```
235
+  namespace DomainModel {
236
+    public interface IRepository<T> where T : EntityBase {
237
+        T GetById (int id);
238
+        IEnumerable<T> List ();
239
+        IEnumerable<T> List (Expression<Func<T, bool>> predicate);
240
+        void Add (T entity);
241
+        void Delete (T entity);
242
+        void Edit (T entity);
243
+    }
244
+
245
+    public abstract class EntityBase {
246
+        public int Id { get; protected set; }
247
+    }
248
+   } 
249
+
250
+   泛型仓储实现
251
+   namespace Infrastructure.Persistence {
252
+    public class Repository<T> : IRepository<T> where T : EntityBase {
253
+        private readonly ApplicationDbContext _dbContext;
254
+        public Repository (ApplicationDbContext dbContext) {
255
+            _dbContext = dbContext;
256
+        }
257
+        public virtual T GetById (int id) {
258
+            return _dbContext.Set<T> ().Find (id);
259
+        }
260
+
261
+        public virtual IEnumerable<T> List () {
262
+            return _dbContext.Set<T> ().AsEnumerable ();
263
+        }
264
+
265
+        public virtual IEnumerable<T> List (Expression<Func<T, bool>> predicate) {
266
+            return _dbContext.Set<T> ()
267
+                .Where (predicate)
268
+                .AsEnumerable ();
269
+        }
270
+
271
+        public void Insert (T entity) {
272
+            _dbContext.Set<T> ().Add (entity);
273
+            _dbContext.SaveChanges ();
274
+        }
275
+
276
+        public void Update (T entity) {
277
+            _dbContext.Entry (entity).State = EntityState.Modified;
278
+            _dbContext.SaveChanges ();
279
+        }
280
+
281
+        public void Delete (T entity) {
282
+            _dbContext.Set<T> ().Remove (entity);
283
+            _dbContext.SaveChanges ();
284
+        }
285
+    }
286
+  }
287
+  ```
288
+  
289
+- 总结
290
+  - 仓储作为领域模型和数据模型的中介,它负责映射领域模型到持久化存储。
291
+  - 仓储实现了透明持久化,即领域层不需要关注领域对象如何持久化。
292
+  - 仓储是一个契约,而不是数据访问层。它明确表明聚合所必需的数据操作。
293
+  - ORM框架不是仓储。仓储是一种架构模式。ORM用来以面向对象的方式来表示数据模型。仓储使用ORM来协调领域模型和数据模型。
294
+  - 仓储适用于具有丰富领域模型的限界上下文。对于没有复杂业务逻辑的简单限界上下文,直接使用持久化框架即可。
295
+  - 使用UOW进行事务管理。UOW负责跟踪对象的状态,仓储在UOW协调的事务中进行实际的持久化工作。
296
+  - 仓储用于管理单个聚合,它不应该控制事务。
297
+
298
+
299
+
300
+> 模块
301
+
302
+- 模块的用途也是如此,通过分解领域模型为不同的模块,以降低领域模型的复杂性,提高领域模型的可读性。
303
+- 模块的设计是基于领域模型的,要符合通用语言的表述。其次,模块的设计要符合高内聚低耦合的设计思想。
304
+- 模块设计的原则
305
+  - 根据领域来组织模块
306
+  - 基于通用语言
307
+  - 高内聚低耦合
308
+- 总结
309
+  - 模块是对领域模型进行分解后的产物,是相对独立的功能单元,由一系列高内聚的领域对象组成,相对聚合、实体和值对象来说是更高一层的抽象。
310
+  - 模块化的思想大大简化了领域模型的复杂性,即便于我们设计出高内聚低耦合的系统,也便于我们理解系统的设计。

+ 50
- 0
实践/后端/项目/26.DDD微服务.md Zobrazit soubor

@@ -0,0 +1,50 @@
1
+> DDD分层架构视图
2
+
3
+- DDD分层架构包括:展现层、应用层、领域层和基础层
4
+  - 展现层(Interface):用户界面 Web服务 其他 =(数据传输)=》应用层
5
+  - 应用层(Application):应用层服务
6
+  - 领域层(Domain):实体、值对象 领域层服务
7
+  - 基础实施层(Infrastructure):数据库、总线、API网关、缓存
8
+
9
+> DDD分层架构各层智能
10
+  
11
+- 展现层: 负责向用户显示信息和解释用户指令
12
+- 应用层:主要面向用户用例操作,协调和指挥领域对象来完成业务逻辑。
13
+    - 应用层也是与其他系统的应用层进行交互的必要渠道。应用服务尽量简单,它不包含业务规则或知识,只为下一层的领域对象协调任务,使它们互相协作。
14
+    - 应用层还可进行安全认证、权限校验、分布式和持久化事务控制或向外部应用发送基于事件的消息等
15
+ - 领域层
16
+    - 领域层是软件的核心所在,它实现全部业务逻辑并且通过各种校验手段保证业务正确性。它包含业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系。它负责表达业务概念、业务状态以及业务规则,具体表现形式就是领域模型
17
+- 基础层
18
+    - 基础层为各层提供通用的技术能力,包括:为应用层传递消息、提供 API 管理,为领域层提供数据库持久化机制等。它还能通过技术框架来支持各层之间的交互。
19
+    
20
+
21
+
22
+> 服务视图
23
+- 接口服务
24
+   - 接口服务位于用户接口层,用于处理用户发送的 Restful 请求和解析用户输入的配置文件等,并将信息传递给应用层。
25
+
26
+- 应用服务
27
+   - 应用服务位于应用层。用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装。 应用层的服务包括应用服务和领域事件相关服务。 
28
+   - 应用服务可对微服务内的领域服务以及微服务外的应用服务进行组合和编排,或者对基础层如文件、缓存等数据直接操作形成应用服务,对外提供粗粒度的服务。 领域事件服务包括两类:领域事件的发布和订阅。通过事件总线和消息队列实现异步数据传输,实现微服务之间的解耦。
29
+
30
+- 领域服务
31
+   - 领域服务位于领域层,为完成领域中跨实体或值对象的操作转换而封装的服务,领域服务以与实体和值对象相同的方式参与实施过程。
32
+   - 领域服务对同一个实体的一个或多个方法进行组合和封装,或对多个不同实体的操作进行组合或编排,对外暴露成领域服务。
33
+   - 领域服务封装了核心的业务逻辑。实体自身的行为在实体类内部实现,向上封装成领域服务暴露。 为隐藏领域层的业务逻辑实现,所有领域方法和服务等均须通过领域服务对外暴露。 为实现微服务内聚合之间的解耦,原则上禁止跨聚合的领域服务调用和跨聚合的数据相互关联。
34
+
35
+- 基础服务
36
+   - 基础服务位于基础层。为各层提供资源服务(如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务逻辑的影响。 
37
+   - 基础服务主要为仓储服务,通过依赖反转的方式为各层提供基础资源服务,领域服务和应用服务调用仓储服务接口,利用仓储实现持久化数据对象或直接访问基础资源。
38
+
39
+
40
+> DDD名词和术语
41
+
42
+- Event Storming(事件风暴):事件风暴是一项团队活动,旨在通过领域事件识别出聚合根,进而划分微服务的限界上下文。在活动中,团队先通过头脑风暴的形式罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对于每一个事件,标注出导致该事件的命令(Command),再然后为每个事件标注出命令发起方的角色,命令可以是用户发起,也可以是第三方系统调用或者是定时器触发等。最后对事件进行分类整理出聚合根以及限界上下文。
43
+- Entity(实体):每个实体是唯一的,并且可以相当长的一段时间内持续地变化。我们可以对实体做多次修改,故一个实体对象可能和它先前的状态大不相同。但是,由于它们拥有相同的身份标识,他们依然是同一个实体。例如一件商品在电商商品上下文中是一个实体,通过商品中台唯一的商品 id 来标示这个实体。
44
+- ValueObject(值对象):值对象用于度量和描述事物,当你只关心某个对象的属性时,该对象便可作为一个值对象。实体与值对象的区别在于唯一的身份标识和可变性。当一个对象用于描述一个事物,但是又没有唯一标示,那么它就是一个值对象。例如商品中的商品类别,类别就没有一个唯一标识,通过图书、服装等这些值就能明确表示这个商品类别。
45
+- Aggregate(聚合):聚合是实体的升级,是由一组与生俱来就密切相关实体和值对象组合而成的,整个组合的最上层实体就是聚合。
46
+- Bounded Context(限界上下文):用来封装通用语言和领域对象,为领域提供上下文语境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。使团队所有成员能够明确地知道什么必须保持一致,什么必须独立开发。
47
+
48
+
49
+- 代码示例链接 https://github.com/marcinstelmach/Ecommerce.Api.git
50
+  - 是关于电子商务的API。可用于许多在线商店。

+ 103
- 0
实践/后端/项目/27.DDD事件风暴.md Zobrazit soubor

@@ -0,0 +1,103 @@
1
+> 为什么采用DDD领域驱动?
2
+
3
+- 一句话来说就是解耦,便于扩展。领域驱动也是目前正流行的微服务的前提条件之一。
4
+> 2019年的智能照明版属于DDD领域驱动吗?
5
+
6
+- 2019年的智能照明版中包含了一些DDD的设计思想。比如洋葱头式由内而外的分层;比如用到了MediatR, 是中介者模式的体现,里面包含了Command和对应的Handler;比如CQRS的思想......这些都是DDD的重要思想。但是,2019年的这个版本并没有实践出DDD的精髓。在DDD的设计中,领域模型必须具有松耦合、原子性的特点。所谓的松耦合是指领域实体之间的关系。在2019年的这个版本中是强耦合,领域实体基本上和数据库表是一一对应关系,表和表之间存在外键约束。当系统不断扩展的时候,这种强关系会让领域关系变得纠缠不清。另外,在涉及多表操作的时候,2019年的这个版本有时会通过事务来处理,即通过代码维护领域关系。而真正的DDD会把事务内嵌在Aggregate中。所谓Aggregate有且只有一个聚合根Aggregate Root。
7
+
8
+- 洋葱头式由内而外的分层:
9
+  -  核心层是与领域或技术无关的基础构件块,它包含一些通用的构件块。
10
+  - 领域层(Domain)是业务逻辑的地方,每个类的方法都是按照领域通用语言中的概念进行命名
11
+  - API层是领域层的入口,它使用领域中的术语和对象
12
+  - 基础架构(Infrastructure)层是最外部的一层,它包含了对接各种技术的适配器,例如数据库、用户界面以及外部服务。
13
+
14
+- CQRS的思想:CQRS,全称Command Query Responsibility Segregation。直译过来就是命令查询的职责分离。
15
+  - Command指的是增加/更新数据的处理。
16
+  - Query指的是查询数据的处理。它不会造成数据的变更
17
+
18
+> Aggregate 
19
+
20
+- 是一种模式,在代码中实现的具体形式很简单
21
+  -  定义一个Entity,作为Aggregate Root,称之为聚合根
22
+  - 遵循Aggregate的完整性规则对领域数据进行操作
23
+
24
+- Aggregate的完整性规则
25
+  - 所有的代码只能通过 Aggregate Root,即聚合根这个特殊的 Entity 访问系统的 Entity,而不能随便的操作任一的 Entity。
26
+  - 每个事务范围只能更新一个 Aggregate Root 及它所关联的 Entity 状态。
27
+
28
+  
29
+> Event Storming
30
+- 我们听说过"头脑风暴",大致是让团队成员一起参与讨论出主意。在DDD中,Event Storming,事件风暴,是让领域专家(熟悉业务的人)、技术人员、产品经理等齐聚一堂,用大家都可以理解的形式表达业务逻辑。通常来说,会在一个比较空的房间内腾出一面墙,大家把想法通过便签贴到墙上。
31
+
32
+- 举一个例子:假设用户在某个系统中发布信息,Event Storming中的关键要素大致包括:
33
+  - Event:触发领域状态改变的动作。比如这里叫,NewsPublished
34
+  - Aggregate:这里的消息就是领域实体。比如这里叫,News
35
+  - Command: 所有领域状态的改变,肯定是用户通过系统界面发出的,用户从界面发出的指令叫Command(还记得MediatR中的xxxCommand:IRequest<T>, xxxCommandHandler<TResult, T>吗?这里的IRequest接口就代表着用户发出的Command,每个Command都有响应的Handler)
36
+  - User:操作系统的用户或角色
37
+  - Information:用户发出Command信息需要基本的前提信息
38
+
39
+
40
+- 如果我们能清晰地知道Command, Command所需要的Information, 是哪个用户或角色执行Command, 对那个Aggregate执行Command, 以及Event是什么,那基本上所有相关人员对系统的业务逻辑已经做了比较清晰的、统一的描述。
41
+
42
+# 用ProcessOn报警写报警版的Event Storming
43
+  - 报警相关的发生在Warning Aggregate上。Aggregate用浅黄色表示
44
+  - 事件(Event)用橙色表示 。事件的一般形式为:Aggregate+动词过去式
45
+  - Command, 浅蓝色,一般形式为:动词+Aggregate
46
+  - 备注用紫色表示
47
+  -  用浅绿色来表示读模型。与 CQRS 的 Query 含义相同
48
+
49
+- 根据APP界面,报警被创建后,用户申请维修,这时系统会查询用户的报警套餐(User Pacakge), 这里又出来一个User Package的Aggregate,而且由于是查询,返回的是Read Model(一般用绿色), Read Model交给界面
50
+- 这个界面上有两种可能,一个是用户套餐中的免费维修次数还有,另一个是免费维修次数用完了。如果还有维修次数,用户确认。如果没有维修次数,用户查询所有套餐(Packages),再次Read Model和界面。
51
+
52
+> 报警首页
53
+
54
+>报警相关
55
+- 系统->Create Warning(Command)->Warning(Aggregate)->Warning Created(Event)->当前报警
56
+- 用户->ApplyFix(Command)->Warning(Aggregate)
57
+- 系统->Query User Package(Command)->User Package(Aggregate)->User Package Read Model
58
+  - 根据APP界面,报警被创建后,用户申请维修,这时系统会查询用户的报警套餐(User Pacakge), 这里又出来一个User Package的Aggregate,而且由于是查询,返回的是Read Model(一般用绿色), Read Model交给界面
59
+- 用户->Query Pakages(command)->Packages(Aggregate)->Packages Read Model
60
+  - 这个界面上有两种可能,一个是用户套餐中的免费维修次数还有,另一个是免费维修次数用完了。如果还有维修次数,用户确认。如果没有维修次数,用户查询所有套餐(Packages),再次Read Model和界面。
61
+- 用户-> Confirm Apply Fix(Command)->Warning (Aggregate)->Warning Apply Fix Confirmed(Event)
62
+- 客服->Dispatch Warning(Command)->Warning(Aggregate)->Warning Dispatched(Event)
63
+- 电工-> Accept Warning(command)->Warning->Warning Accepted(Event)
64
+- 电工-> Confirm Warning Repair->Warning->Warning Repair Confirmed(Event)
65
+- 系统->Finish Warning->Warning ->Warning Finished(Event)
66
+
67
+# 开关管理
68
+> 位置分类相关
69
+- 安装人员-> Create Electric Box(Command)->Electric Box(Aggregate)->Electric Box Creted 
70
+- 安装人员-> Create Category(Command)->Category用途(Aggregate)->Category Creted 
71
+- 安装人员-> Create Line Type(Command)->Line Type开关类型(Aggregate)->Category Line Type
72
+- 安装人员->  Add Mac To Electric Box(Command)->Mac(Aggregate)->Mac Added To Electric Box(Event)
73
+- 安装人员->  Add Line To Category(Command)->Line线路(Aggregate)->Lined Added To Category(Event)
74
+- 安装人员->  Add Line To Line Type(Command)->Line线路(Aggregate)->Lined Added To Line Type(Event)
75
+- 安装人员->  set Line (Command)->Line线路(Aggregate)->Line Set(Event)
76
+
77
+> 场景
78
+- 安装人员+用户 TimingInfo+LineInfo(Read model)->Create Scene(Command)->SCene(Aggregate)->Scene Created(Event)
79
+
80
+
81
+> 电费和套餐
82
+
83
+- 管理员->Set User Package->User Package->User Package set
84
+- 管理员->Set User Electricity Type-> User Electricity Type-> User Electricity Type set
85
+- 管理员-> Set Fixed Electricity->User Fixed Electricity->User Fixed Electricity Set
86
+- 管理Set High Low Electricity ->User High Low Electricity->User High Low Electricity Set
87
+- 管理员-> Set Step Electricity->User Step Electricity->User Step Electricity Set
88
+
89
+
90
+
91
+
92
+
93
+
94
+> 领域设计
95
+- 所有的领域应该有一个抽象基类,而且是一个泛型类。
96
+```
97
+public abstract class Entity<T>
98
+{
99
+  public T Id{get;protect set;}
100
+
101
+}
102
+```
103
+-  DDD中有一个很重要的概念 ,叫做Object Value(值对象)。会把一个Aggregate里的对象类型看作是值类型。这样做一方面是减少Aggregate对关系的依赖,一方面可以很好判断Aggregage的状态。同样也是一个泛型抽象基类,如果一个Object类型的对象想要被当作值类型处理,那就继承这个泛型抽象基类。

+ 0
- 0
实践/后端/项目/28.领域设计.md Zobrazit soubor


binární
实践/后端/项目/imgs/2799767-ecd81e1865814899.webp Zobrazit soubor


+ 178
- 0
实践/后端/项目/imgs/28.领域设计.md Zobrazit soubor

@@ -0,0 +1,178 @@
1
+> 领域设计
2
+
3
+- 所有的领域应该有一个抽象基类,而且是一个泛型
4
+```
5
+public abstract class Entity<T>
6
+{
7
+    public T Id{get;protected set;}
8
+}
9
+```
10
+
11
+- DDD中有一个很重要的概念,叫作Object Value Type。会把一个Aggregate里的对象类型看作是值类型。这样做一方面是减少Aggregate对关系的依赖,一方面可以很好判断Aggregage的状态。同样也是一个泛型抽象基类,如果一个Object类型的对象想要被当作值类型处理,那就继承这个泛型抽象基类。
12
+- object value type怎么理解
13
+  - 值对象 主要描述领域的某个方面而本身没有概念表示的对象。
14
+  - 主要是为了方便比较领域对象。
15
+    - 以前
16
+    ```
17
+    public class Order
18
+    {
19
+        public List<OrderLine> Lines{get;set;}
20
+        public Address Address{get;set;}
21
+    }
22
+    ```
23
+    - 实际上Order里面包含了对象类型,是很难比较的
24
+    - 现在
25
+    ```
26
+    public class OrderLine:ValueObject<OrderLine>,
27
+    public class Address:ValueObject<Address>
28
+    ```
29
+    - 这就把原来是引用类型的对象OrderLine和Address转换成值类型了,这就减少了对象之间的相互依赖。
30
+```
31
+ public abstract class ValueObject<T> where T : ValueObject<T>
32
+    {
33
+        /// <summary>
34
+        /// 获取子类需要比较的属性
35
+        /// 抽象方法,通过子类override实现
36
+        /// </summary>
37
+        protected abstract IEnumerable<object> GetAttributesToIncludeInEqualityCheck();
38
+
39
+        /// <summary>
40
+        /// 比较
41
+        /// 可以被子类重写
42
+        /// </summary>
43
+        public virtual bool Equals(T other)
44
+        {
45
+            if (other == null) return false;
46
+
47
+            //两个对象的属性值比较
48
+            return GetAttributesToIncludeInEqualityCheck().SequenceEqual(other.GetAttributesToIncludeInEqualityCheck());
49
+        }
50
+
51
+        /// <summary>
52
+        /// 重写object基类的比较大小的方法
53
+        /// </summary>
54
+        public override bool Equals(object other)
55
+        {
56
+            return Equals(other as T);
57
+        }
58
+
59
+        /// <summary>
60
+        /// 重写object基类的GetHashCode
61
+        /// </summary>
62
+        public override int GetHashCode()
63
+        {
64
+            var hash = 17;
65
+            foreach (var obj in this.GetAttributesToIncludeInEqualityCheck())
66
+            {
67
+                hash = hash * 31 + (obj == null ? 0 : obj.GetHashCode());
68
+            }
69
+            return hash;
70
+        }
71
+
72
+        /// <summary>
73
+        /// 重新定义==
74
+        /// </summary>
75
+        public static bool operator ==(ValueObject<T> left, ValueObject<T> right)
76
+        {
77
+            return Equals(left, right);
78
+        }
79
+
80
+        /// <summary>
81
+        /// 重新定义=
82
+        /// </summary>
83
+
84
+        public static bool operator !=(ValueObject<T> left, ValueObject<T> right)
85
+        {
86
+            return !(left==right);
87
+        }
88
+    }
89
+
90
+```
91
+- 以上,ValueObject<T>就是Object Value Type的抽象基类,大致作了如下事情:
92
+
93
+   - 重写基类object的Equals方法
94
+   - 重写基类object的GetHashCode方法
95
+   - 提供一个抽象方法,让子类根据各自不同的属性比较大小,让子类重写
96
+   - 有一个自定义的比较大小的方法,会调用基类的抽象方法
97
+   - 重新定义==和!=
98
+
99
+- 所有的领域也好,或者Aggregate也好,都需要事件驱动。关于事件,同样需要一个基类。
100
+```
101
+ public abstract class DomainEvent
102
+    {
103
+        public Guid Id { get; protected set; }
104
+        public DateTime OccuredOn { get; protected set; }
105
+
106
+        protected DomainEvent(Guid id, DateTime occuredOn)
107
+        {
108
+            Id = id;
109
+            OccuredOn = occuredOn;
110
+        }
111
+
112
+        protected DomainEvent()
113
+        {
114
+            Id = Guid.NewGuid();
115
+            OccuredOn = SysTime.Now();
116
+        }
117
+    }
118
+    以上,DomainEvent就是事件的抽象基类。
119
+```
120
+- 一个属性是Guid类型,这是很有特色的一个点,事件居然也有Guid类型,就像很多领域都有主键一样
121
+- 在DDD中,完全可以实现Event Sourcing,事件追溯。注意,这里的Event Sourcing和前面说的Event Storming不一样。Event Soucing适合的场景包括:银行、保险公司等。对于一个领域来说,只要执行了相同数量、相同顺序的事件,领域就会来到一个确定的状态。据说,前端框架Redux就用到了Event Stroming,让领域的状态可追溯。当然,一般的项目用不上Event Sourcing。
122
+- 以上,DateTime类型的属性OccurredOn的值是通过一个静态方法获取到的。这样做有什么好处呢?
123
+```
124
+public class SysTime
125
+    {
126
+        public static Func<DateTime> CurrentTimeProvider { get; set; } = () => DateTime.Now;
127
+
128
+        public static DateTime Now() => CurrentTimeProvider();
129
+    }
130
+```
131
+- 以上,Now方法类似方法内的一个工厂方法,产生DateTime的实例。但实例是通过CurrentTimeProvider这个委托得到的。不难理解,通过委托获取DateTime实例增加了灵活性。所谓的委托,就是定义好输入形参和返回类型,委托就是一个方法,把方法看作一个变量,调用委托,就是调用方法。
132
+
133
+- 以上,有了领域的基类Entity, 有了Object Value Type, 有了事件的基类。接下来,事件需要被发布,这样系统内都某个事件订阅者能获悉并执行。所以,事件发布需要一个接口:
134
+```
135
+ public interface IEventPublisher
136
+    {
137
+        void Publish(DomainEvent @event);
138
+    }
139
+```
140
+- 以上,所有的要么是抽象基类,要么是接口,我们可以体会到:在领域层基本都是抽象的。也就是在我们所熟悉的洋葱架构中,越内层的越抽象,越外层越具体,越抽象也意味着更低的耦合性、更好的扩展性。而且,领域层,乃至领域层的外面一层都是抽象居多,这一点也方便单元测试。
141
+
142
+
143
+
144
+- 接下来,会把Event Storming中的设计拿来,考虑如何定义领域
145
+- 首先,所有的领域都会有主键,通常使用Guid类型,但是Guid类型是一个Object类型,这在DDD的领域设计中需要把之转换成值类型,也就是Object Value Type。
146
+```
147
+public class EntityId : ValueObject<EntityId>
148
+    {
149
+        protected override IEnumerable<object> GetAttributesToIncludeInEqualityCheck()
150
+        {
151
+            yield return Value;
152
+        }
153
+
154
+        public Guid Value { get; }
155
+
156
+        public EntityId(Guid value)
157
+        {
158
+            Value = value;
159
+        }
160
+
161
+        protected EntityId() { }
162
+    }
163
+
164
+```
165
+- 以上,EntityId就是Object Value Type,派生于ValueObject<T>。GetAttributesToIncludeInEqualityCheck方法是重写或者说实现基类的方法,当ValueObject的Equals方法的时候会调用GetAttributesToIncludeInEqualityCheck方法。EntityId有一个比较奇怪的地方,就是有个无参的构造函数,这个是为EF Core准备的。
166
+
167
+
168
+- 接着,创建第一个领域或者说Aggregate:
169
+```
170
+ /// <summary>
171
+    /// 报警
172
+    /// </summary>
173
+    public class Warning : Entity<EntityId>
174
+    {
175
+    }
176
+```
177
+- 以上,Warning派生于领域抽象基类Entity<T>,这里的T表示领域的主键类型,这里的主键类型是EntityId,也就是Object Value Type, 可以看作是一个像int类型那样的值类型。
178
+

Načítá se…
Zrušit
Uložit