领域驱动设计(二)

593 阅读6分钟

背景

Evens DDD强调Model Driven Design,即模型驱动设计。那在现实世界,我们用什么来建模?- 模型构造块。

一、实体(Entity)

实体,反映的是现实世界的对象,这些对象必须满足两个条件 (1)具有唯一身份标识 (2)具有可变性 唯一身份标识,是用来区别其他实体的度量,比如对Person对象来说,身份证ID就可以作为唯一身份标识,对Product对象来说,产品编码可以作为唯一身份标识。可变性,是对对象而言具备状态,当状态发生变化时,就可以说具备可变性,比如订单对象Order,订单状态具有未支付,已支付,待发货等等,这些状态是会发生变化的

在我们的日常开发中,常常把表结构通过代码工具生成一一对应的Entity类,这些类只有属性没有行为,所以称为贫血模型,这些模型无法反映现实世界,而把行为逻辑放在Service类中,这完全是事务脚本式开发。

public class Product extends Entity {


    private Set<ProductBacklogItem> backlogItems;
    private String description;
    private ProductDiscussion discussion;
    private String discussionInitiationId;
    private String name;
    private ProductId productId;
    private ProductOwnerId productOwnerId;
    private TenantId tenantId;

    ...

    // 行为 => 初始化一个讨论
    public void initiateDiscussion(DiscussionDescriptor aDescriptor) {
        if (aDescriptor == null) {
            throw new IllegalArgumentException("The descriptor must not be null.");
        }


        if (this.discussion().availability().isRequested()) {
            this.setDiscussion(this.discussion().nowReady(aDescriptor));


            DomainEventPublisher
                .instance()
                .publish(new ProductDiscussionInitiated(
                        this.tenantId(),
                        this.productId(),
                        this.discussion()));
        }
    }
    
    ...
}


二、值对象(Value Object)

值对象也是反映现实世界的对象,但它跟Entity本质上不同的是,值对象没有唯一标识,且没有状态可变性,从这两点可以说明,值对象和Entity所表示的对象是不一样的。

值对象,具备以下条件: (1)不变性。值对象没有状态,只有属性,当属性发生改变时,就需要整体替换,即。 (2)整体性。如上面代码的Product实体,当ProductDiscussion(它是一个值对象)对象发生属性变化时 ,不是取update ProductDiscussion的属性,而是站在Product实体的角度,先remove掉旧的ProductDiscussion,然后再new一个新的ProductDiscussion并替换掉discussion。 (3)无副作用行为。即柔性设计里的CQRS,把这个描述为查询方法,简单来说值对象的行为,是不会去改变对象的状态的

三、领域服务(domain service)

什么不是领域服务?

在我们平时的开发中,业务逻辑都放在了service类,还包括了事务控制,任务分配等动作,这些应该是应用服务(application service,后面的文章再讲)的事,所以就混淆了领域服务和应用服务。

通常来说,领域服务主要关注于特定于某个领域的业务,而且领域服务是无状态的,其实很难区分到底要不要使用一个领域服务,在《实现领域驱动设计》里罗列了3点,在这种情况下可以使用领域服务:

(1)执行一个显著的业务操作; (2)对领域对象进行转换; (3)对多个领域对象作为输入进行计算,结果产生一个值对象

第二点和第三点都好理解,对象转换和创建值对象。但第一点“显著的”业务操作,什么样算是“显著的”?

public class AuthenticationService extends AssertionConcern {

    private EncryptionService encryptionService;
    private TenantRepository tenantRepository;
    private UserRepository userRepository;
    ...


    public UserDescriptor authenticate(
            TenantId aTenantId,
            String aUsername,
            String aPassword) {

        this.assertArgumentNotNull(aTenantId, "TenantId must not be null.");
        this.assertArgumentNotEmpty(aUsername, "Username must be provided.");
        this.assertArgumentNotEmpty(aPassword, "Password must be provided.");

        UserDescriptor userDescriptor = UserDescriptor.nullDescriptorInstance();

        Tenant tenant = this.tenantRepository().tenantOfId(aTenantId);

        if (tenant != null && tenant.isActive()) {
            String encryptedPassword = this.encryptionService().encryptedValue(aPassword);
            User user =
                    this.userRepository()
                        .userFromAuthenticCredentials(
                            aTenantId,
                            aUsername,
                            encryptedPassword);

            if (user != null && user.isEnabled()) {
                userDescriptor = user.userDescriptor();
            }
        }

        return userDescriptor;
    }
    ...
}

我们来看一下这个例子:系统必须对User进行认证,并且只有当Tenant处于激活状态时,才能对User进行认证,这里就用到了领域服务。首先,这是个业务过程,然后,这个业务过程放在User不合适,放在Tenant也不太合适,至于“显著的”这个其实是有点见仁见智,至少是对业务领域不断推敲和反思才能确定的。

所以,我个人觉得,需要用到领域服务的,应该是某个业务过程,放在哪个Entity或Value Object都不合适时,就可以考虑使用。

四、聚合(Aggregate)

聚合是由实体和值对象在一致性边界之内组成的,一致性是聚合的重要概念。

如上面的Product,就是一个聚合,聚合和聚合之间的访问,是通过唯一标识来依赖的,这里可以通ProductId,比如一个订单聚合Order,那么Order如果要引用Product,就可以引用ProductId,这样可以减小聚合的大小,在内存方面也可以减少垃圾回收的对象。

关于一致性,在Events DDD里,举了一个订单完整性的例子:

有一个采购订单,采购是有额度限制的,比如只能采购1000美刀,那么有几个人要采购n个物品(也叫采购项),每个物品的价格不一样,不管这些人采购多少数量和多少金额物品,但是必须要满足一个条件,采购项的总金额,不能大于采购额度,所以这里就有一个一致性规则,或者说是固定规则。

设计聚合有以下原则:

(1)设计小聚合,但是度量应该是多少,还是看实际情况; (2)通过唯一标识引用其他聚合; (3)在边界之外使用最终一致性。上面所说的一致性规则,其实是在一个事物范围内保证的,也就是说,一个聚合对应一个事物,在一个聚合内需要保证强一致性,但是不同聚合之间,是使用最终一致性,这种可以使用领域事件(后面的文章再分析)实现。

五、模块(Module)

模块,在Java里也可以视为Package,模块名本身应该是通用语言的一部分,而且模块内应该具备高内聚性。


ackage com.saasovation.identityaccess;
package com.saasovation.collaboration;
package com.saasovation.agilepm;

五、资源库(repository)

repository概念其实在日常开发中也有遇到,比如Spring的@Repository注解,但是repository与DAO是有区别的,DAO是从数据库表的角度看待问题,并且提供CRUD操作,DAO模式通常只是数据库表的一层封装;而资源库是从领域角度看待问题,更偏向于对象。

public interface ProductRepository {

    public Collection<Product> allProductsOfTenant(TenantId aTenantId);
    public ProductId nextIdentity();
    public Product productOfDiscussionInitiationId(TenantId aTenantId, String aDiscussionInitiationId);
    public Product productOfId(TenantId aTenantId, ProductId aProductId);
    public void remove(Product aProduct);
    public void removeAll(Collection<Product> aProductCollection);
    public void save(Product aProduct);
    public void saveAll(Collection<Product> aProductCollection);
}

并不是所有领域对象都需要repository,严格来讲,只有聚合才需要repository,这里需要理解一下,DDD和事物脚本开发有个很大的不一样,在事物脚本模式下,可能我们更新一个产品项的一个属性,然后就去update数据库,但是在DDD里,所有的更新操作,是通过repository去save product聚合的,即productReposity.save(product),这是,统一去更新或者添加对象或属性。

六、工厂(Factory)

工厂应该是最熟悉的,比如设计模式里的抽象工厂模式,工厂方法和Builder模式。在DDD里,工厂是领域设计的一部分,承担创建复杂对象和聚合,并且工厂本身不承担业务逻辑。

public class NotificationLogFactory {

    private EventStore eventStore;

    …

    public NotificationLog createCurrentNotificationLog() {
        return this.createNotificationLog(
                this.calculateCurrentNotificationLogId(eventStore));
    }


    public NotificationLog createNotificationLog(
            NotificationLogId aNotificationLogId) {
        long count = this.eventStore().countStoredEvents();
        NotificationLogInfo info = new NotificationLogInfo(aNotificationLogId, count);
        return this.createNotificationLog(info);
    }

七、构造块之间的关系

image.png

引用:

1.《领域驱动设计:软件核心复杂性应对之道》Eric Evans

2.《实现领域驱动设计》Vaughn Vernon

  1. github.com/VaughnVerno…