CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
这是篇关于DDD的文章,我也不知道叫啥好(二)
0
架构设计
小笨蛋
发布于:2022年05月22日
更新于:2022年05月31日
153
#custom-toc-container
> 忽然想起个事,记得有次面试,是一个比较大的厂子,一面是两位面试官一位是刚毕业不久的大学生,另一个是位女士,那位女士第一个问题就是"你们DDD是怎么设计的"?她提完这个问题我就知道她压根就不懂DDD,一顿白话,果然把他们说迷糊了,最终结果是面试通过!但我没去。 书接上回:[这是篇关于DDD的文章,我也不知道叫啥好(一)](https://www.codesnippet.cn/home/list/149 "这是篇关于DDD的文章,我也不知道叫啥好(一)") ### 7.值对象 ![图片alt](/uploads/images/20220522/153842-43392e92910c4e51baaec6b19de5ab2e.png '代码片段:Www.CodeSnippet.Cn') #### 1.引言 提到值对象,我们可能立马就想到值类型和引用类型。而在C#中,值类型的代表是strut和enum,引用类型的代表是class、interface、delegate等。值类型和引用类型的区别,大家肯定都知道,值类型分配在栈上,引用类型分配在堆上。 那是不是值类型对应的就是值对象,引用类型对应的就是实体吗?很抱歉,不是的。 值对象我们要分开来看,其包含两个词:值和对象。值是什么?比如,数字(1、2、3.14),字符串(“hello world”、“DDD”),金额(¥50、$50),地址(北京市昌平区沙河)它们都是一个值,这个值有什么特点呢,固定不变,表述一个具体的概念。对象又是什么?一切皆为对象,是对现实世界的抽象,用来描述一个具体的事物。那值对象=值+对象=将一个值用对象的方式进行表述,来表达一个具体的固定不变的概念。 所以了解值对象,我们关键要抓住关键字——值。 #### 2.值的特征 1就是代表数字1,“Hello DDD”就是一个固定字符串,“¥50”就是表示人民币50元。假设你手上有一沓钞票,我们去超市购物的时候,很显然我们会根据面额去付款,不会拿20元当50元花,也不会把美元当人民币花,毕竟¥50≠$50。那对于钞票来说,我们怎么识别它们,无非就是钞票上印刷的数字面额和货币单位。你可能会说了,每张钞票上都印有编号,就算同样面额的毛爷爷,那它也不一样。这个陈述,我竟然无言以对。但我只想问你,你平时购物付款,是用编号识别面额的啊?编号显然是银行关心的事,与我们无关。 我们这里提到的数字面额、货币单位和编号,除此之外还有发行日期,其实都是钞票的基本特征,在coding中我们会根据场景选择性的对某些特征以属性的形式加以抽象。而在我们日常消费的场景下,显然编号和发行日期这两个特征我们可以直接忽略不计。 从上面这个例子我们可以总结出值的特征: 1. 表示一个具体的概念 2. 通过值的属性对其识别 3. 属性判等 4. 固定不变 #### 3.案例分析 > 购物网站都会维护客户收货地址信息来进行发货处理,一个地址信息一般主要包含省份、城市、区县、街道、邮政编码信息。 如果要让我们设计,我们肯定噼里啪啦就把代码写下来了: ```csharp ///
/// 地址 ///
public class Address { ///
///Id ///
public int AddressId{ get; set; } ///
/// 省份 ///
public string Province { get; set; } ///
/// 城市 ///
public string City { get; set; } ///
/// 区县 ///
public string County { get; set; } ///
/// 街道 ///
public string Street { get; set; } ///
/// 邮政编码 ///
public string Zip { get; set; } } ``` 很简单的类,我想你在没了解DDD值对像之前肯定会这样写,这并不奇怪,为了将Address映射到数据库,我们需要定义一个AddressId作为主键映射,这是数据建模的结果。那在DDD中应该如何设计?别急,我们一步一步的分析。 首先,我们要问自己一个问题,地址是什么?北京市昌平区沙河兆丰家园1号楼 邮政编码: 1234567,它就是一个标准的地址,表述的是一个具体的不变的位置信息。它不会随着时间而变化,它包含了地址所需要的完整属性(省份、城市、区县、街道、邮政编码)信息。所以,地址是一个值。 按照我们现在的设计,如果有多个所处兆丰家园的用户,我们数据库将存在多条相同的地址信息(只是Id不同)。但Id不同,就不是同一个地址吗?我们在做发货处理的时候,难道会因为Id不同,而将货物发往不同的地方吗?很显然不是的。这也再次论证了地址是一个值的事实。 那我们如何抽象设计这个地址呢,让其具有值的特征? 我们一条一条的来进行分析。 1. 表示一个具体的概念 我们上面设计的Address类,也能表示出地址这个概念。 2. 通过值的属性对其识别 也就是不需要唯一标识,删去我们设计的AddressId即可。 3. 属性判等 重写Equals方法,比较属性判断。 4. 固定不变 就是通过构造函数来初始化,所有属性均不提供修改入口。 修改后的Address如下: ```csharp ///
/// 地址 ///
public class Address { ///
/// 省份 ///
public string Province { get; private set; } ///
/// 城市 ///
public string City { get; private set; } ///
/// 区县 ///
public string County { get; private set; } ///
/// 街道 ///
public string Street { get; private set; } ///
/// 邮政编码 ///
public string Zip { get; private set; } public Address(string province, string city, string county, string street, string zip) { this.Province = province; this.City = city; this.County = county; this.Street = street; this.Zip = zip; } public override bool Equals(object obj) { bool isEqual = false; if (obj != null && this.GetType() == obj.GetType()) { var that = obj as Address; isEqual = this.Province == that.Province && this.City == that.City && this.County == that.County && this.Street == that.Street && this.Zip == that.Zip; } return isEqual; } public override int GetHashCode() { return this.ToString().GetHashCode(); } public override string ToString() { string address = $"{this.Province}{this.City}" + $"{this.County}{this.Street}({this.Zip})"; return address; } } ``` 至此,我们的Address就具有了值的特征,我们可以直接使用Address address = new Address("北京", "北京市", "昌平区", "沙河", "1234567");)来表示一个具体的通过属性识别的不可变的位置概念。在DDD中,我们称这个Address为值对象。读到这里,你可能会觉得值对象也不过如此,也可能会有一堆问题,但请稍安勿躁,我们继续讲解。 #### 4.DDD中的值对象 通过上面对值的特征分析,结合实际的案例,我们设计出了一个Address这个值对象。那在DDD中对值对象又是怎样描述的呢? ##### 4.1.值对象的特征 咱们来看看《实现领域驱动设计》上是如何定义的吧: > - 描述了领域中的一件东西 - 不可变的 - 将不同的相关属性组合成了一个概念整体 - 当度量和描述改变时,可以用另外一个值对象予以替换 - 可以和其他值对象进行相等性比较 - 不会对协作对象造成副作用 由此可见,值对象包含了值所具有的全部特征。 另外有一点:个人认为值对象不会孤立的存在,它有其所属。比如我们所说的地址,它是一个客观存在。没有一个具体的上下文语境,它就仅仅是一个字符串。只有在某个具体的领域下,才有其实质意义,比如客户收货地址、售后地址。 ##### 4.2.值对象的问题 说到问题,你可能想到的第一个问题就是持久化的问题。是的,值对象没有标识列如何存储数据库呢? 当下比较流行使用ORM持久化机制,使用ORM将每个类映射到一张数据库表,再将每个属性映射到数据库表中的列会增加程序的复杂性。那如何使用ORM持久化来避免这一问题呢? 1. 单个值对象 上面我们提到值对象不会孤立存在,所以我们可以将值对象中的属性作为所属实体/聚合根的数据列来存储(比如,我们可以将收货地址的属性映射到客户实体中)。这样做就会导致数据表列数增多,但是能够优化查询性能,因为不需要联表查询。 2. 多个值对像序列化到单个列 当每个客户仅允许维护一个收货地址时,我们用上面的方式没有问题。但很显然一个客户可以有多个收货地址。这个时候我们该怎么持久化值对象集合呢?不可能把值对象集合的每个元素映射到外层的实体表中,但是创建多个表又增加复杂性,所以一个变态的方法是使用序列化大对象模式。把一个集合序列化后塞到外层实体表的某一列中,是有点匪夷所思。而且数据库的列宽是有限制的,且不方便查询。但似乎也带来一个好处,大大简化了系统的设计(不用设计多列分别存储了)。 3. 使用数据库实体保存多个值对像 使用层超类型来赋予值对象一个委派标识,以数据库实体的形式保存值对象。 你可能会觉得第3个方法好,因为其更符合传统的设计方式,但其并非DDD推崇的一种方式,因为层超类型让值对象有了实体的影子。在进行持久化设计的时候,我们要谨记**根据领域模型来设计数据模型,而不是根据数据模型来设计领域模型。** ##### 4.3.值对象的作用 通过上面的分析介绍,我们可以体会到值对象带来的以下好处: - 符合通用语言,更简单明了的表达简单业务概念。 - 提升系统性能。 - 简化设计,减少不必要的数据库表设计。 #### 5.建模值对象 值对象作为领域建模工具之一,有其存在的意义。领域中,并不是每一个事物都必须有一个唯一身份标识,对于某些对象,我们更关心它是什么而无需关心它是哪个。所以建模值对象,我们关键要结合通用语言的表述看其是否有**值的含义和特征**。 #### 6. 总结 如果非要对值对象进行总结的话,我希望你记住我开头的那句话: **值对象=值+对象=将一个值用对象的方式进行表述,来表达一个具体的固定不变的概念。** ------------ ### 8.应用服务&领域服务 #### 1. 引言 单从字面理解,不管是领域服务还是应用服务,都是服务。而什么是服务?从SOA到微服务,它们所描述的服务都是一个宽泛的概念,我们可以理解为**服务是行为的抽象**。从前缀来看,根据DDD的经典分层架构,它们又隶属于不同的层,应用服务属于应用层,领域服务属于领域层。 ![图片alt](/uploads/images/20220522/155837-717dac3650e24726bf88718317ace61e.png '代码片段:Www.CodeSnippet.Cn') - 应用层(Application):负责展现层与领域层之间的协调,协调业务对象来执行特定的应用程序任务。它不包含业务逻辑。 - 领域层(Domain):负责表达业务概念,业务状态信息以及业务规则,是业务软件的核心。 所以综合来看应用服务是用来表述应用行为,而领域服务用来表述领域行为。 那怎么理解应用行为和领域行为呢,应用行为描述了一个具体操作从开始到结束的每一个环节,而领域行为是对应用行为的细化,用来处理具体的某一个环节。比如,我们手机购物,从购物车结算这一场景来举例,这就是一个应用行为。而这个应用行为又主要包括金额计算、支付、生成订单,这些子环节就可以理解为一个领域行为。 #### 2. 应用服务 应用服务是用来**表达用例和用户故事**(User Story)的主要手段。 应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责**编排和转发**,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。 应用层相对来说是较“薄”的一层,除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,或者向其他系统发生基于事件的消息通知,另外还可以用于创建邮件以发送给客户等。 应用层作为展现层与领域层的桥梁。展现层使用VO(视图模型)进行界面展示,与应用层通过DTO(数据传输对象)进行数据交互,从而达到展现层与DO(领域对象)解耦的目的。 #### 3.领域服务 领域层就是较“胖”的一层,因为它实现了全部业务逻辑并且通过各种校验手段保证业务正确性。而什么是业务逻辑呢?业务流程、业务策略、业务规则、完整性约束等。 > 当领域中的某个操作过程或转换过程不是实体或值对象的职责时,我们便应该将该操作放在一个单独的接口中,即领域服务。请确保该服务和通用语言时一致的;并且保证它是无状态的。 根据这句话我们有几个问题需要理清: 1. 什么时候使用领域服务? 2. 领域服务无状态怎么理解? 领域服务是用来协调领域对象完成某个操作,用来处理业务逻辑的,它本身是一个行为,所以是无状态的。状态由领域对象(具有状态和行为)保存。 上面也说了,领域对象是具有状态和行为的。那就是说我们也可以在实体或值对象来处理业务逻辑。那我们该如何取舍呢? 一般来说,在下面的几种情况下,我们可以使用领域服务: - 执行一个显著的业务操作过程 - 对领域对象进行转换 - 以多个领域对象为输入,返回一个值对象。 #### 4. 案例分析 我们拿经典的转账问题来分析一下: 而针对转账这一操作,它的业务用例应该是这样的: 1. 检查账号余额是否足够 2. 检查目标账户账号是否合法 3. 转账 4. 短信通知转账双方 其中1,2步是转账的合法性校验属于转账业务的一部分,所以,1,2,3均应该放到领域层通过领域服务来实现。短信通知,它并不是是转账的核心业务,因为这根据具体情况而定,比如只有客户订阅了账号变动通知我才发短信。所以将第4步归类到应用服务中去实现,就确保了领域服务的纯粹性。 而至于持久化的问题,我们可以这样想,领域逻辑应该只关心业务逻辑,才能保证领域逻辑的可重用性。将持久化放到应用层,我们就会有更多的选择性。 #### 5.总结 当应用服务中的逻辑趋于复杂时,我们就要小心领域逻辑泄露到应用服务中去。而在使用领域服务时,我们又要避免过度使用,因为会导致贫血领域模型。毕竟有些单一的操作更适合放到领域对象(实体和值对象)中去。 所以总结以下: 1. 服务是行为的抽象。 2. 应用服务通过委托领域对象和领域服务来表达用例和用户故事。 3. 领域对象(实体和值对象)负责单一操作。 4. 领域服务用于协调多个领域对象共同完成某个业务操作。 5. 应用服务不处理业务逻辑,领域服务处理业务逻辑。 ------------ ### 9.领域事件 #### 1. 引言 > 领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联。 针对官方释义,我们可以理出以下几个要点: 1. 领域事件作为领域模型的重要部分,是领域建模的工具之一。 2. 用来捕获领域中已经发生的事情。 3. 并不是领域中所有发生的事情都要建模为领域事件,要忽略无业务价值的事件。 4. 领域事件是领域专家所关心的(需要跟踪的、希望被通知的、会引起其他模型对象改变状态的)发生在领域中的一些事情。 简而言之,领域事件是用来捕获领域中发生的具有业务价值的一些事情。它的本质就是事件,不要将其复杂化。在DDD中,领域事件作为通用语言的一种,是为了清晰表述领域中产生的事件概念,帮助我们深入理解领域模型。 #### 2. 认识领域事件 > 当用户在购物车点击结算时,生成待付款订单,若支付成功,则更新订单状态为已支付,扣减库存,并推送捡货通知信息到捡货中心。 在这个用例中,“订单支付成功”就是一个领域事件。 考虑一下,在你没有接触领域事件或EDA(事件驱动架构)之前,你会如何实现这个用例。肯定是简单直接的方法调用,在一个事务中分别去调用状态更新方法、扣减库存方法、发送捡货通知方法。这无可厚非,毕竟之前都是这样干的。 那这样设计有什么问题? 1. 试想一下,若现在要求支付成功后,需要额外发送一条付款成功通知到微信公众号,我们怎么实现?想必我们需要额外定义发送微信通知的接口并封装参数,然后再添加对方法的调用。这种做法虽然可以解决需求的变更,但很显然不够灵活耦合性强,也违反了OCP(OCP原则即开-闭原则,看文末资料)。 2. 将多个操作放在同一个事务中,使用事务一致性可以保证多个操作要么全部成功要么全部失败。在一个事务中处理多个操作,若其中一个操作失败,则全部失败。但是,这在业务上是不允许的。客户成功支付了,却发现订单依旧为待付款,这会导致纠纷的。 3. 违反了聚合的一大原则:在一个事务中,只对一个聚合进行修改。在这个用例中,很明显我们在一个事务中对订单聚合和库存聚合进行了修改。 那如何解决这些问题?我们可以借助领域事件的力量。 1. 解耦,可以通过发布订阅模式,发布领域事件,让订阅者自行订阅; 2. 通过领域事件来达到最终一致性,提高系统的稳定性和性能; 3. 事件溯源; 4. 等等。 下面我们就来一一深入。 #### 3.建模领域事件 如何使用领域事件来解耦呢? 当然是封装不变,应对万变。那针对上面的用例,不变的是什么,变的又是什么?不变的是订单支付成功这个事件;变化的是针对这个事件的不同处理手段。 而我们要如何封装呢? 这时我们就要理清事件的本质,事件有因必有果,事件是由事件源和事件处理组合而成的。通过事件源我们来辨别事件的来源,事件处理来表示事件导致的下一步操作。 ![图片alt](/uploads/images/20220522/160956-93221f095d4d41e1b615f5ff3bf8bbf7.png '代码片段:Www.CodeSnippet.Cn') ##### 3.1. 抽象事件源 事件源应该至少包含事件发生的时间和触发事件的对象。我们提取IEventData接口来封装事件源: ```csharp ///
/// 定义事件源接口,所有的事件源都要实现该接口 ///
public interface IEventData { ///
/// 事件发生的时间 ///
DateTime EventTime { get; set; } ///
/// 触发事件的对象 ///
object EventSource { get; set; } } ``` 通过实现IEventData我们可以根据自己的需要添加自定义的事件属性。 ##### 3.2. 抽象事件处理 针对事件处理,我们提取一个IEventHandler接口: ```csharp ///
/// 定义事件处理器公共接口,所有的事件处理都要实现该接口 ///
public interface IEventHandler { } ``` 事件处理要与事件源进行绑定,所以我们再来定义一个泛型接口: ```csharp ///
/// 泛型事件处理器接口 ///
///
public interface IEventHandler
: IEventHandler where TEventData : IEventData { ///
/// 事件处理器实现该方法来处理事件 ///
///
void HandleEvent(TEventData eventData); } ``` 以上,我们就完成了领域事件的抽象。在代码中我们通过实现一个`IEventHandler
`来表达领域事件的概念。 ##### 3.3. 领域事件的发布和订阅 领域事件不是无缘无故产生的,它有一个发布方。同理,它也要有一个订阅方。 那如何和订阅和发布领域事件呢? 领域事件的发布可以使用**发布--订阅模式**来实现。而比较常见的实现方式就是**事件总线**。 ![图片alt](/uploads/images/20220522/161307-75b9f66d781940d3b5a72bfa1b2a11cc.png '代码片段:Www.CodeSnippet.Cn') 事件总线是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。Event Bus就相当于一个介于Publisher(发布方)和Subscriber(订阅方)中间的桥梁。它隔离了Publlisher和Subscriber之间的直接依赖,接管了所有事件的发布和订阅逻辑,并负责事件的中转。 这里就简要说明一下事件总线的实现的要点: 1. 事件总线维护一个事件源与事件处理的映射字典; 2. 通过单例模式,确保事件总线的唯一入口; 3. 利用反射或依赖注入完成事件源与事件处理的初始化绑定; 4. 提供统一的事件注册、取消注册和触发接口。 最后,我们看下事件总线的接口定义: ```csharp public interface IEventBus { void Register
(IEventHandler eventHandler); void UnRegister
(Type handlerType) where TEventData: IEventData; void Trigger
(Type eventHandlerType, TEventData eventData) where TEventData: IEventData; } ``` 在应用服务和领域服务中,我们都可以直接调用Register方法来完成领域事件的注册,调用Trigger方法来完成领域事件的发布。 #### 4. 最终一致性 说到一致性,我们要先搞明白下面几个概念。 **事务一致性** 事务一致性是是数据库事务的四个特性之一,也就是ACID特性之一: > 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。 我们用一张图来理解一下: ![图片alt](/uploads/images/20220522/161546-2b48124b82344d769c95ab034d1c67c0.png '代码片段:Www.CodeSnippet.Cn') 在事务一致性的保证下,上面的图示只会有两个结果: 1. A和B两个操作都成功了。 2. A和B两个操作都失败了。 **数据一致性** 举个简单的例子,假设10个人,每人有100个虚拟币,虚拟币仅能在这10人内流通,不管怎么流通,最终的虚拟币总数都是1000个,这就是数据一致性。 **领域一致性** 简单理解就是在领域中的操作要满足领域中定义的业务规则。比如你转账,并不是你余额充足就可以转账的,还要求账户的状态为非挂失、锁定状态。 回到我们的案例,当支付成功后,更新订单状态,扣减库存,并发送捡货通知。按照我们以往的做法,为了维护订单和库存的数据一致性,我们将这三个操作放到一个应用服务去做(因为应用服务管理事务),事务的一致性可以保证要么全部成功要么全部失败。但是,试想一下,客户支付成功后,订单依旧为待付款状态,这会引起纠纷。另外,由于库存没有及时扣减,很可能会导致库存超卖。怎么办呢? 将事务拆解,使用领域事件来达到最终一致性。 **最终一致性** “最终一致性”是一种设计方法,可以通过将某些操作的执行延迟到稍后的时间来提高应用程序的可扩展性和性能。 ![图片alt](/uploads/images/20220522/161619-c3b993a0e07a47299e72374f812f0f98.png '代码片段:Www.CodeSnippet.Cn') 对于常见于分布式系统的最终一致性工作流中,客户同样在系统中执行一个命令,但这个系统只为维护事务中的领域一致性运行部分的操作,剩余的操作在允许延后执行。针对上图的结果: 1. A操作执行成功,B操作将延后执行。 2. A操作失败,B操作将不会执行。 而针对我们的案例,我们如何使用领域事件来进行事务拆分呢?我们看下下面这张图你就明白了。 ![图片alt](/uploads/images/20220522/161636-695eec54f32a4cff8adb47bd4b047039.png '代码片段:Www.CodeSnippet.Cn') 分析一下,针对我们案例,我们发现一个用例需要修改多个聚合根的情况,并且不同的聚合根还处于不同的限界上下文中。其中订单和库存均为聚合根,分别属于订单系统和库存系统。我们可以这样做: 1. 在订单所在的聚合根中更新订单支付状态,并发布“订单成功支付”的领域事件; 2. 然后库存系统订阅并处理库存扣减逻辑; 3. 通知系统订阅并处理捡货通知。 通过这种方式,我们即保证了聚合的原则,又保证了数据的最终一致性。 #### 5. 事件存储和事件溯源 关于事件存储(Event Store)和事件溯源(Event Sourcing)是一个比较复杂的概念,我们这里就简单介绍下,不做过多展开。 ![图片alt](/uploads/images/20220522/162233-a783b39a46144f908d0fd23754db1c70.png '代码片段:Www.CodeSnippet.Cn') 事件存储,顾名思义,即事件的持久化。那为什么要持久化事件? 1. 当事件发布失败时,可用于重新发布。 2. 通过消息中间件去分发事件,提高系统的吞吐量。 3. 用于事件溯源。 源代码管理工具我们都用过,如Git、TFS、SVN等,通过记录文件每一次的修改记录,以便我们跟踪每一次对源代码的修改,从而我们可以随时回滚到文件的指定修改版本。 事件溯源的本质亦是如此,不过它存储的并非聚合每次变化的结果,而是存储应用在该聚合上的历史领域事件。当需要恢复某个状态时,需要把应用在聚合的领域事件按序“重放”到要恢复状态对应的领域事件为止。 #### 6.总结 经过上面的分析,我们知道引入领域事件的目的主要有两个,一是解耦,二是使用领域事件进行事务的拆分,通过引入事件存储,来实现数据的最终一致性。 最后,对于领域事件,我们可以这样理解: 通过将领域中所发生的活动建模成一系列的离散事件,并将每个事件都用领域对象来表示,来跟踪领域中发生的事情。 也可以简要理解为:**领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理**。 > OCP原则(开-闭原则) 在面向对象领域,有一个很著名的原则:OCP (Open-Closed Principle ),它的核心含意是:一个好的设计应该能够容纳新的功能需求的增加,但是增加的方式不是通过修改又有的模块(类) ,而是通过增加新的模块(类)来完成的,也就是在设计的时候,所有软件组成实体包括接口,函数,函数等必须是可扩展但不可修改的。如果一个设计能够遵循OCP ,那么就能够有效的避免上述的问题。 > 要是一个设计能够符合OCP 原则,就要求我们在进行设计时不能简单的以功能为核心。要实现OCP 的关键是抽象,抽象表征了一个固定的行为,但是对于这个行为可以有很多个不同的具体实现方法。通过抽象,我们就可以用一个固定的抽象的概念来代替哪些容易变化的数量众多的具体的概念,并且使得原来依赖于哪些容易变化的概念的模块,依赖于这个固定的抽象的概念,这样的结果就是:系统新的需求的增加,仅仅会引起具体的概念的增加,而不会影响依赖于具体概念的抽象体的其他模块。在实现的层面上,抽象体是通过抽象类来描述的,在.net 中是接口( interface )。
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2024