.net架构设计读书笔记--第三章 第8节 域模型简介(Introducing Domain Model)
一、数据--行为转变
很长的时间,典型的分析方法或多或少是以下两种,第一,收集需求并做一些分析,找出有关实体 (例如,客户、 订单、 产品) 和进程来实现。 第二,手持这种理解你尝试推断一个物理 (和主要关系) 的数据模型,可以支持您确保流程数据模型是关系一致 (主键约束、 归一化、 索引),然后开始构建软件组件对识别的最相关的业务实体的表
你也可以依靠数据库特定功能,如存储过程作为一种方式,同时保持从上层的代码隐藏的数据库结构的执行行为。最后一步找到适合的模型来表示数据和将其移动到表示层。
1. 模型和域的基本原则
多年来,我们一直试图比较领域驱动和数据驱动两种软件设计方法的优劣,很多时候是失败的,我们通常把所有的事作为对象读取数据。
- 不能用对象来代替数据读取
我们都知道数据访问是在.net之前出现的,ado.net是windows开发的一个标志性组件。ADO.NET是基础架构层用来访问关系数据库的基础。使用ado.net从数据库中取得的数据是已经被反序列化的数据,只是这些数据用datareader,或dtattable还表示。为了更进一对数据格式化,可以使用linq to sql与Entity Framework来轻松转转换为可管理的类。
Linq to SQL与Entity Framework在ORM工具下映射到一个现有的关系数据库。但这不能说这就是使用了域设计方法来设计了软件,这只是数据访问层访问数据的一个方法,它只是代替了DataReader或DateSet。
- 可序列化的对象不一定是域模型(A persistent object model is not a domain model)
DDD并不只是批使用对像来代替数据访问的中的date reader,DDD主要是为了表示域在通用语言的概念和上下文边界。在边界上下文中我们通常用Model来示域,这些用来表示域中的对象类通常被称做域模型。
域模型侧重于业务逻辑中对象表示,它是业务逻辑层的一个重要组成部分。
DDD对任何人都有益。DDD最是个种分析方法,它是对领域学习的指南,对领域词汇的定义,它不是编码,也不是技术。它是做为一个架构师在顶层设计中必需掌握技能。
2. 域层内部实现 Inside the domain layer
通常情况下,域模型架构设计为主下文边界的一个单独的层。域模型在表示层、应用层、基础设施层中以不同形式传递数据,在这里我们可以把域模型看做为一个对象模型。
- 域模型
简单的说,定义一个域模型的最佳方式是:一个域模型表示一个业务领域的概念视图。它用实体和值对象来表示现实世界和软件系统里对象映射。
- 模块Modules
转换一个域模型到软件系统里时,可能要定义多个模块。模块最终将整个域内的Model分组,分类。在实际应用中,模块中就是.net中的命名空间或一个程序
在一个上下文边界中,域是由多个模块组成,在.net解决方案中域模型通过命名空间来组织并隔离类库。模块的组织隔离原则。
一个上下文边界一个域模型
一个上下文边界可以有多个模块
一个域模型可以属于欠个模块
- 值对象Value objects
一个DDD模型包括实体entities和值对象,eityties与value objects都是.net下的类,但他们的在概念上不同,职责也不同。在DDD设计中,value object全部被定义为属性表示,当value object 实例被创建之后值就不会变动。
- 实体Eitities
所有的entities都有属性,但实体并不全是由属性组成。当属性不足以标识实体的唯一性时,可以在实体中加入另外的属性,如ID。
有两个清晰的概念,比如:你有两个事务交易,在两个域中要分别独立开来。在事务唯一性的后面,域模型必需可以清晰的表示对像的唯一性。域模型和值对象同时返回,意味着我们要每一个都要创建。
value object只是数据的载体,而entities可以说是数据加上行为。如业务上的订单,一个订单可能表示为一个entity,但在数据层面上一个订单可以还包括商品,配送地址,物流信息等。
- 实体的持久化Persistence of entities
Demain model必需要可以持久化,但是demain model自己没直接可实例化的功能,demain model侧重点不业务逻辑表示。
repositories仓储--实现demain model持久化功能的一种类型的组件。
仓储通常在外部调用域模型,比如:从application layer或从领域内的其它的组件,如域服务(demain services)。仓储的契约在领域层内,仓储的实现则在基础架构层。
- 合集模型
在域模型中,多个实体组成的一个容器被称之为模型合集,模型合集也可以是合集的合集。
使用业务来定义合集,合集是设计的一部分,架构师和开发人员在业务上定义合集更易反映出业务,在边界上下文中,合集代表不变性条件的形式层在,不变性条件是业务的规则,并反过来验证检测域实体。换言之,就是合集要业务的要保持一致性。
通常一致性分为两种,事务一致性和结果一致性。事务一致性批域内的每个事务后合集都要保持一致,结果一致性指不是第个事务都一致。
全集模型的优点有很多,首先,简化了域模型和业务逻辑之间的复杂度,简化了它们之间的颗粒度和关系。从SOLID思想看来,合集有以下几个优点:
- 纯粹的逻辑分组
- 在代码层面上,实体类保留着他自身的实现
- 没有必要将每个分组都生成一个新类
合集在抽象级别层面上将多个实体封闭成一个单元,将实体数据减少,实体关系的逻辑也同时简化。如下图:
- 合集的根
合集的根是合集关联对象的根对象,合集的根在整个域是可见,并可以被直接引用。实体在合集内部有有他的唯一性和生命周期,但它不能被外部引用。以下几点是合集根的基本性:
- 合集根保证合集在业务规则中始终处于有效状态。
- 聚合根是负责所有封装对象的持久性
- 合集根是负责级联更新和删除合集的实体
- 查询操作只能检索聚合的根,通过各表层访问内部实体也需要通过这个合集根
在代码中要清晰的表示合集根,在服务和仓储层在区分它。合集的根通常实现为一个接口,这个接口通常叫IAggregateRoot。
public class Order : IAggregateRoot
{
...
}
接口并不需要明确的实现,它只用于对外表示这个实体是合集的根。
public interface IAggregateRoot
{
}
聚合根也可以给出一个稍微复杂的实现,总这为了让合集更健壮。
public interface IAggregateRoot {
bool CanBeSaved { get; }
}
通过返回true达到标记为IAggregateRoot 的作用,关且说明是非成员接口。
- 域服务Domain services
域服务是域逻辑的一系列实现方法的集合,它不属于某个特定的合集,并很有可能跨多个实体。域服务协调合集个仓储用合适的方法来实现业务上的活动。在某些情况下域服务可能是基础架构的消费服务,如邮件发送。
给定一块业务逻辑,这块逻辑不适用于现用的任何合集,也不能用现有的合集重新设计,这种情况下就考虑引用域服务。所以域服务是在逻辑在任何情况下都不适用的最后解决手段。
- 仓储Repositories
仓储是域服务中是长用的服务类型,主要提供合集的持久化功能。要为每个根合集提供一个仓储,如:CustomerRepository, OrderRepository等。常用的做法是将相同类型的类取为接口,并将接口做为类库的核心。仓储对接口的实现通常被归属为基础架构层。一个仓储通常基于一个接口实现,如:IRepository
public interface IRepository<T> where T:IAggregateRoot
{
// You can keep the interface a plain marker or
// you can have a few common methods here.
T Find (object id);
void Save (T item);
}
public interface IOrderRepository : IRepository<Order>
{
// The actual list of members is up to you
...
}
创建仓储没有绝对正确或绝对错误的方法。一个好的仓储通常是包含了一系列所需要的合集的基于接口的类。你也可以忽略接口只实现持久化功能,如:
public interface IOrderRepository
{
// The actual list of members is up to you
...
}
仓储类成员实现了数据访问,查询、更新、插入等。数据访问的技术由架构师来决定,今天通常使用Entity Framework这样的ORM工具来实现。也没有理由会阻止使用ADO.NET,存储过程,甚至是NoSOL。
需要注意的一点是,实现了IRepository<T>接口,并被标记为契约类,可以IL语言实现注入功能。
[ContractClass(typeof(RepositoryContract<>))]
public interface IRepository<T> where T:IAggregateRoot
{
...
}
[ContractClassFor(typeof(IRepository<>))]
sealed class RepositoryContract<T> : IRepository<T> where T : IAggregateRoot
{
public void Save(T item)
{
Contract.Requires<ArgumentNullException>(item != null, "item");
Contract.Requires<ArgumentException>(item.CanBeSaved);
throw new NotImplementedException();
}
}
当Save方法被调用,合集中的所提供的CanBeSaved方法由CLR自动访问。为了操持一致性,CanBeSaved需要在仓储启用Save方法前检查。
- 域事件
假设以下一个场景:一个在线商城应用,订单提交并被成功处理,支付系统也处理完成,订单的配送信息由一个配送公司来接收,此时订单已经写入系统。现在的问题是:当订单创建时系统应该做什么事?通过使用通用语言可以准确的告诉你该做和什么,这个任务在这称为Task T。
- 事件的顺序逻辑
事件的顺序逻辑首选项就是使用代码将域服务的一系列方法按照订单的处理逻辑顺序组织起来,如下面的这段代码:
void Checkout(ShoppingCart cart)
{
// Proceed through the necessary steps
...
if (success)
{
// Execute task T
...
}
}
上面这段代码有一些问题,首先它没有足够的表达力,所有终端到终端的步骤是整体调用Checkout处理,外面的Checkout方法对没内部方法执行没有可视性。
如何触发相关事件?如果我们只是引发域相关的一个事件会更好吗?所有的事件可以在一个地方删除,这样有两个好处:1、在不需要改动生成事件的代码的情况下动态定义一系列事件。2、使我们有可能有多个地方的同一事件引发。这意味着,处理程序将运行时不考虑实际调用方
- 模式化域事件
到目前为止,域事件被视为域内一个简单类发生的某个特定的动作。好的例子如:订单被创建或用户的忠诚度到达了金牌级别。如果你花少许的时间来考虑事件,你会在几乎所有的域模型中都会存在。一个发票的生成或修改、发货单的生成、一个新客户的注册等等。
根据这个原则,域事件可以被定义为一个简单的类的事件成员。在域模型中,事件特殊表示为一个实现特定接口的域中的类。
public interface IDomainEvent
{
}
public class CustomerReachedGoldMemberStatus : IDomainEvent
{
public Customer Customer { get; set; }
}
EventArgs做事件基数,是.net事件机制的基本选项,别外的常见做法是在域模型内实现自己的事件引擎。通过接口标记事件,事件的信号状态改变通过域实体内部的发布订阅来监听。
public class OrderRequestService
{
public void RegisterOrder(ShoppingCart cart)
{
// Create and persist the new order
...
// Check gold status for the customer who made the order
CheckGoldStatusForCustomer(cart.Customer);
}
public void CheckGoldStatusForCustomer(Customer customer)
{
// Get total amount of orders placed in current year
var totalOrdered = CalculateTotalOrdersForCustomer(customer, DateTime.Today.Year)
if (totalOrdered > 1000)
{
Bus.Raise(new CustomerReachedGoldMemberStatus() { Customer = customer });
}
}
}
- 处理域事件
引发事件是一工作,要寻找一个处理这些事件的方式,以下是事件触发的基本实现:
public class Bus
{
private static IList<IHandler<IDomainEvent>> Handlers = new List<IHandler<IDomainEvent>>();
public static void Register(IHandler<IDomainEvent> handler)
{
if (handler != null)
Handlers.Add(handler);
}
public static void Raise<T>(T eventData) where T : IDomainEvent
{
foreach (var handler in Handlers)
{
if (handler.CanHandle(eventData))
handler.Handle(eventData);
}
}
}
如上面的代码,事件引发实际是让订阅列表中的每个事件有机会执行。任何已注册的处理程序总是得到一个机会去处理给定类型的事件。处理程序是一个小型的类包含一些逻辑来运行在某一特定事件的反应,显然,你可以有多个类来处理同一域事件。也允许将一些事件组合在一起,你可以先执行Task 1,再执行Task 2,如下:
public class GoldStatusHandler : IHandler<IDomainEvent>
{
public void Handle(IDomainEvent eventData)
{
// Some synchronous task
...
return;
}
public bool CanHandle(IDomainEvent eventType)
{
return eventType is CustomerReachedGoldMemberStatus;
}
}
.net架构设计读书笔记--第三章 第8节 域模型简介(Introducing Domain Model)的更多相关文章
- .net架构设计读书笔记--第三章 第9节 域模型实现(ImplementingDomain Model)
我们长时间争论什么方案是实现域业务领域层架构的最佳方法.最后,我们用一个在线商店案例来说明,其中忽略了许多之前遇到的一些场景.在线商店对很多人来说更容易理解. 一.在线商店项目简介 1. 用例 ...
- .net架构设计读书笔记--第三章 第10节 命令职责分离(CQRS)简介(Introducing CQRS)
一.分离查询命令 Separating commands from queries 早期的面向DDD设计方法的难点是如何设计一个类,这个类要包含域的方方面面.通常来说,任务软件系统方法调用可以 ...
- 《Linux内核设计与分析》第六周读书笔记——第三章
<Linux内核设计与实现>第六周读书笔记——第三章 20135301张忻估算学习时间:共2.5小时读书:2.0代码:0作业:0博客:0.5实际学习时间:共3.0小时读书:2.0代码:0作 ...
- .net架构设计读书笔记--第二章 设计体系结构
第五节 探索领域架构 一.领域驱动设计的价值与意义 最初在java中使用,.net要晚些才引入.领域驱动设计出现之初的争议.一个向导,少走弯路 1. 我们真的需要DDD吗? DDD并不适用于每个软 ...
- .net架构设计读书笔记--第一章 基础
第一章 基础 第一节 软件架构与软件架构师 简单的说软件架构即是为客户构建一个软件系统.架构师随便软件架构应运而生,架构师是一个角色. 2000年9月ANSI和IEEE发布了<密集性软件架构建 ...
- 《linux内核设计与实现》读书笔记第三章
第3章 进程管理 3.1 进程 1.进程 进程就是处于执行期的程序. 进程包括: 可执行程序代码 打开的文件 挂起的信号 内核内部数据 处理器状态 一个或多个具有内存映射的内存地址空间 一个或多个执行 ...
- .net架构设计读书笔记--第二章 第7节 神化般的业务层
一.编排业务逻辑的模式1. 事务脚本模式TS(The Transaction Script pattern ) TS模式概述 TS 鼓励你跳过任何的面向对象的设计,你直接到所需的用户操作的业务 ...
- 《Linux内核设计与实现》读书笔记 第三章 进程管理
第三章进程管理 进程是Unix操作系统抽象概念中最基本的一种.我们拥有操作系统就是为了运行用户程序,因此,进程管理就是所有操作系统的心脏所在. 3.1进程 概念: 进程:处于执行期的程序.但不仅局限于 ...
- 《CSS3实战》读书笔记 第三章:选择器:样式实现的标记
第三章:选择器:样式实现的标记 选择器的魔力在于,让你完全实现对网页样式的掌控.不同的选择器可以用在不同的情况下使用.总之把握的原则是:规范的编码,根据合理地使用选择器,比去背选择器的定义有价值的多. ...
随机推荐
- cni 添加网络 流程分析
cnitool: Add or remove network interfaces from a network namespace cnitool add <net> <netns ...
- 车脸检测 Adaboost 检测过程
上一节中我介绍了如何使用Opencv自带的opencv_traincascade.exe来做训练,接下来介绍如何使用训练生成的cascade.xml模型文件来检测车脸. 首先需要说明的是我这里的训练数 ...
- 倍增法-lca codevs 1036 商务旅行
codevs 1036 商务旅行 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 钻石 Diamond 题目描述 Description 某首都城市的商人要经常到各城镇去做生意 ...
- Zero
Zero是我的极品现任BOSS曾用过的QQ昵称.那时候,我正跟京姑娘闹七年之痒,甩她而去赋闲在老家.Zero通过朋友介绍,看了我几篇零散的博客,就给我打电话,让我过来聊聊.本来我跟京姑娘也没有大矛盾, ...
- UESTC 886 方老师金币堆 --合并石子DP
环状合并石子问题. 环状无非是第n个要和第1个相邻.可以复制该行石子到原来那行的右边即可达到目的. 定义:dp[i][j]代表从第i堆合并至第j堆所要消耗的最小体力. 转移方程:dp[i][j]=mi ...
- POJ 3304 Segments【叉积】
题意:有n条线段,问有没有一条直线使得所有线段在这条直线上的投影至少有一个共同点. 思路:逆向思维,很明显这个问题可以转化为是否有一条直线穿过所有线段,若有,问题要求的直线与该直线垂直,并且公共点为垂 ...
- Linux命令学习-top
top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器.下面详细介绍它的使用方法. top - 01:06:48 up 1:22, ...
- 如何修改myeclipse 内存,eclipse.ini中各个参数的作用。
修改MyEclipse/eclipse文件夹中配置文件eclipse.ini中的内存分配就哦了 =================================== 一般的ini文件设置主要包括以下 ...
- C# 无边框窗体之窗体移动
点击窗体任意位置移动窗体: 需要添加命名空间: using System.Runtime.InteropServices; private const int WM_NCLBUTTONDOWN = 0 ...
- 【夯实Mysql基础】MySQL在Linux系统下配置文件及日志详解
本文地址 分享提纲: 1. 概述 2. 详解配置文件 3. 详解日志 1.概述 MySQL配置文件在Windows下叫my.ini,在MySQL的安装根目录下:在Linux下叫my.cnf,该文件位于 ...