.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思想看来,合集有以下几个优点:

  1.     纯粹的逻辑分组
  2.     在代码层面上,实体类保留着他自身的实现
  3.     没有必要将每个分组都生成一个新类

合集在抽象级别层面上将多个实体封闭成一个单元,将实体数据减少,实体关系的逻辑也同时简化。如下图:

      

 

  • 合集的根

    合集的根是合集关联对象的根对象,合集的根在整个域是可见,并可以被直接引用。实体在合集内部有有他的唯一性和生命周期,但它不能被外部引用。以下几点是合集根的基本性:

  1.   合集根保证合集在业务规则中始终处于有效状态。
  2.   聚合根是负责所有封装对象的持久性
  3.   合集根是负责级联更新和删除合集的实体
  4.   查询操作只能检索聚合的根,通过各表层访问内部实体也需要通过这个合集根

在代码中要清晰的表示合集根,在服务和仓储层在区分它。合集的根通常实现为一个接口,这个接口通常叫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;

        }

    }

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2016-03-12 00:45  徐某人  阅读(435)  评论(0编辑  收藏  举报