鼎鼎知识库
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

25.DDD仓储工厂等概念.md 13KB

应用服务&领域服务

  • 服务是行为的抽象
  • 应用层(Application):负责展现层与领域层之间的协调,协调业务对象来执行特定的应用程序任务。它不包含业务逻辑。
  • 领域层(Domain):负责表达业务概念,业务状态信息以及业务规则,是业务软件的核心。

  • 应用服务:表达用例和用户故事的主要手段 eg:购物车结算=》应用行为

    • 应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。
  • 领域服务 实现了全部业务逻辑并且通过各种校验手段保证业务正确性

    • 当领域中的某个操作过程或转换过程不是实体或值对象的职责时,我们便应该将该操作放在一个单独的接口中,即领域服务。请确保该服务和通用语言时一致的;并且保证它是无状态的。
    • 领域服务是用来协调领域对象完成某个操作,用来出路业务逻辑,它本身是一个行为,所以是无状态的。状态由领域对象保存。
    • eg:比如转账操作问题 业务用例
      • 1.检查账号余额是否足够
      • 2.检查目标账户账号是否合法
      • 3.转账
      • 4.短信通知转账双方
  • 总结:

    • 1.服务是行为的抽象
    • 2.应用服务通过委托领域对象和领域服务来表达用例和用户故事。
    • 3.领域对象(实体和值对象)负责单一操作。
    • 4.领域服务用于协调多个领域对象共同完成某个业务操作。
    • 5.应用服务不处理业务逻辑,领域服务处理业务逻辑。

领域事件

  • 领域事件作为领域模型的重要部分,是领域建模的工具之一。
  • 用来捕获领域中已经发生的事情
  • 并不是领域中所有发生的事情都要建模为领域事件,要忽略无业务价值的事件
  • 当用户在购物车点击结算时,生成待付款订单,若支付成功,则更新订单状态为已支付,扣减库存,并推送捡货通知信息到捡货中心。 订单支付成功就是一个领域事件

  • 建模领域事件

    • 使用领域事件来解耦方法:当然是封装不变,应对万变。上例 不变的是订单支付成功这个事件,变化的是针对这个事件不同处理手段。

    • 抽象事件源 事件源应该至少包含事件发生的时间和触发事件的对象。 ``` public interface IEventData { ///

      /// 事件发生的时间 /// DateTime EventTime{get;set;}

    ///

    /// 触发事件的对象 /// object EventSource{get;set;} } 通过实现IEventData我们可以根据自己的需要添加自定义的事件属性。

    - 抽象事件处理
    

    针对事件处理 提取IEventHandler接口

    ///

    /// 定义事件处理器公共接口,所有的事件处理都要实现该接口 /// public interface IEventHandler {

    } 事件处理要与事件源进行绑定,所以我们再来定义一个泛型接口

    ///

    /// 泛型事件处理器接口 /// /// public interface IEventHandler : IEventHandler where TEventData : IEventData {

        /// <summary>
        /// 事件处理器实现该方法来处理事件
        /// </summary>
        /// <param name="eventData"></param>
        void HandleEvent(TEventData eventData);
    

    }

    
    - 领域事件的发布和订阅
      - 领域事件的发布可以使用`发布-订阅模式`来实现。比较常见的实现法师就是事件总线
      - 事件总线实现的要点
         - 事件总线维护一个事件源与事件处理的映射字典;
         - 通过单例模式,确保事件总线的唯一入口;
         - 利用反射或依赖注入完成事件源与事件处理的初始化绑定
         - 提供统一的事件注册、取消注册和触发接口
      - 事件总线的接口定义
    

    public interface IEventBus {

       void Register < TEventData > (IEventHandler eventHandler);
    
        void UnRegister < TEventData > (Type handlerType) where TEventData: IEventData;
    
         void Trigger < TEventData > (Type eventHandlerType, TEventData eventData) where TEventData: IEventData;
    

    } 在应用服务和领域服务中,我们都可以直接调用Register方法来完成领域事件的注册,调用Trigger方法来完成领域事件的发布。

    ```

  • 最终一致性

    • 务一致性是是数据库事务的四个特性之一,也就是ACID特性之一:
    • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
    • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。
    • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行
    • 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。
    • 案例
    • 订单系统发布”订单成功支付”事件
    • 库存系统订阅并处理库存扣减逻辑
    • 通知系统订阅并处理检货通知
  • 事件存储

    • 事件发布可用于重新发布
    • 通过消息中间件去分布事件
    • 事件溯源

聚合

  • 在DDD中,聚合也可以用来表示整体与部分的关系。领域对象的显示分组(订单+订单项)
    • 封装业务
    • 保证聚合内领域对象的数据一致性
  • 关键
    • 领域对象间的关联关系
    • 基于业务用例而非现实生活建立必要关联
    • 减少不必要的关联
    • 将双向关联转换为单项关联
  • 聚合是领域对象的显式分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。

    • 领域对象的显式分组
    • 领域行为和不变性
    • 一致性和事务性边界
  • 一致性边界

    • 实例:订单支付成功后,订单状态要更新为已支付状态,且现有库存要根据订单中商品实际销售数量进行扣减。
    • 事务一致性
    • 最终一致性:实现原理是借助领域事件来完成事务的拆分

工厂

  • DDD中工厂的主要目标是隐藏对象的复杂创建逻辑;次要目标就是要清楚的表达对象实例化的意图。
  • 工厂模式的实现四种方式

    • 简单工厂:简单实用,但违反开放封闭
    • 工厂方法:开放封闭,单一产品
    • 抽象工厂:开放封闭,多个产品
    • 反射工厂:可以最大限度的解耦
  • 封装内部结构

    • 以添加商品到购物车为例。 步骤 1、 加载用户购物车 2、获取商品税率 3、创建新的购物车子项 ``` 相关应用层代码 public class AddProductToBasket { public void Add (Product product, Guid basketId) { public void Add (Product product, Guid basketId) { var basket = _basketRepository.FindBy (basketId); basket.Add (product); }

} public class Basket {

  public void Add (Product product) {
        if (Contains (product))
            GetItemFor (product).IncreaseItemQuantitBy (1);
        else {
            var rate = TaxRateService.ObtainTaxRateFor (product.Id,
                country.Id);
            var item = new BasketItem (rate, product.Id, product.price);
            _items.Add (item);

}


引入一个工厂对象来封装购物车子项的创建,包括获取正确的税率 namespace DomainModel {

public class Basket {
    // ......
    public void Add (Product product) {
        if (Contains (product))
            GetItemFor (product).IncreaseItemQuantitBy (1);
        else
            _items.Add (BasketItemFactory.CreateItemFor (product,
                deliveryAddress));
    }
}
public class BasketItemFactory {
    public static void CreateBasketFrom (Product product, Country country) {
        var rate = TaxRateService.ObtainTaxRateFor (product.Id, country.Id);
        return new BasketItem (rate, product.Id, product.price);
    }
}

} 引入工厂模式后,购物车的职责单一了,且隔离了来自购物车子项的变化,比如当税率变化时,或购物车子项需要其他信息创建时,都不会影响到购物车的相关逻辑。


- 还用工厂的好处
 - 工厂将领域对象的使用和创建分离。
 - 通过使用工厂类,可以隐藏创建复杂领域对象的业务逻辑。
 - 工厂类可以根据调用者的需要,创建相应的领域对象。
 - 工厂方法可以封装聚合的内部状态。


> 仓储
- 特性 :仓储用来存储和删除聚合,但同时提供针对聚合的显式查询以及汇总。
  - 存储集合
  - 删除集合
- 仓储与数据访问层的区别
  - 仓储限定了只能通过聚合根来持久化和检索领域对象,以确保所有改动和不变性由聚合处理。
  - 仓储通过隐藏聚合持久化和检索的底层技术实现领域层的的持久化无关性(即领域层不需要知道如何持久化领域对象)。
  - 仓储在数据模型和领域模型定义了一个边界。

- 仓储举例

简单的仓储定义 public interface ICustomerRepository {

  Customer FindBy(Guid id);
  void Add(Customer customer);
  void Remove(Customer customer); 

}


- 仓储的要点
  - 是保持你的领域模型和技术持久化框架的独立性,这样你的领域模型可以隔离来自底层持久化技术的影响。如果没有仓储这一层,你的持久化基础设施可能会泄露到领域模型中,并影响领域模型完整性和最终一致性


- 仓储方法需要明确
仓储是原则上是领域模型与持久化存储之间明确的契约,仓储定义的接口方法不仅仅是CURD方法。它是领域模型的扩展,并以领域专家所理解的术语编写。仓储接口的定义应该根据应用程序的用例需求来创建,而不是从类似CURD的数据访问角度来构建。

- 泛型仓储
  - 举例

namespace DomainModel {

public interface IRepository<T> where T : EntityBase {
    T GetById (int id);
    IEnumerable<T> List ();
    IEnumerable<T> List (Expression<Func<T, bool>> predicate);
    void Add (T entity);
    void Delete (T entity);
    void Edit (T entity);
}

public abstract class EntityBase {
    public int Id { get; protected set; }
}

}

泛型仓储实现 namespace Infrastructure.Persistence {

public class Repository<T> : IRepository<T> where T : EntityBase {
    private readonly ApplicationDbContext _dbContext;
    public Repository (ApplicationDbContext dbContext) {
        _dbContext = dbContext;
    }
    public virtual T GetById (int id) {
        return _dbContext.Set<T> ().Find (id);
    }

    public virtual IEnumerable<T> List () {
        return _dbContext.Set<T> ().AsEnumerable ();
    }

    public virtual IEnumerable<T> List (Expression<Func<T, bool>> predicate) {
        return _dbContext.Set<T> ()
            .Where (predicate)
            .AsEnumerable ();
    }

    public void Insert (T entity) {
        _dbContext.Set<T> ().Add (entity);
        _dbContext.SaveChanges ();
    }

    public void Update (T entity) {
        _dbContext.Entry (entity).State = EntityState.Modified;
        _dbContext.SaveChanges ();
    }

    public void Delete (T entity) {
        _dbContext.Set<T> ().Remove (entity);
        _dbContext.SaveChanges ();
    }
}

} ```

  • 总结
    • 仓储作为领域模型和数据模型的中介,它负责映射领域模型到持久化存储。
    • 仓储实现了透明持久化,即领域层不需要关注领域对象如何持久化。
    • 仓储是一个契约,而不是数据访问层。它明确表明聚合所必需的数据操作。
    • ORM框架不是仓储。仓储是一种架构模式。ORM用来以面向对象的方式来表示数据模型。仓储使用ORM来协调领域模型和数据模型。
    • 仓储适用于具有丰富领域模型的限界上下文。对于没有复杂业务逻辑的简单限界上下文,直接使用持久化框架即可。
    • 使用UOW进行事务管理。UOW负责跟踪对象的状态,仓储在UOW协调的事务中进行实际的持久化工作。
    • 仓储用于管理单个聚合,它不应该控制事务。

模块

  • 模块的用途也是如此,通过分解领域模型为不同的模块,以降低领域模型的复杂性,提高领域模型的可读性。
  • 模块的设计是基于领域模型的,要符合通用语言的表述。其次,模块的设计要符合高内聚低耦合的设计思想。
  • 模块设计的原则
    • 根据领域来组织模块
    • 基于通用语言
    • 高内聚低耦合
  • 总结
    • 模块是对领域模型进行分解后的产物,是相对独立的功能单元,由一系列高内聚的领域对象组成,相对聚合、实体和值对象来说是更高一层的抽象。
    • 模块化的思想大大简化了领域模型的复杂性,即便于我们设计出高内聚低耦合的系统,也便于我们理解系统的设计。