Skip to main content

领域层编码实现

完成领域模型的设计后,最开始应该做的是领域层的编码实现。

事实上,主要业务的实现都应该在领域层实现。如果你的领域层很薄,而应用层有非常多逻辑,这可能意味着你需要思考你的实现是否正确。

领域层并不关心具体的存储框架,无论后面是MySQL,或是Mongo,对于数据存储,建议统一使用Repository的概念进行抽象

1. 领域层依赖说明

领域层只依赖了以下类库或框架

  • JDK
  • myddd-spring-boot中的myddd-domain基础类库
  • guava工具类库
  • JPA Annotation类库

原则上,并不需要更多的依赖,类似Hibernate,Mongo Client等,应该由基础设施层提供实现。

guava工具类库

依赖guava的原因在于,就算是在JDK8之后,guava仍然是对JDK的有益补充,它提供了一系列的工具类,会让你的JAVA更简单

比如,使用guava来做防御式编程


public Company createSubCompany(){
Preconditions.checkNotNull(getParent(),"需要指定一个父组织");
setFullPath(getParent().getFullPath() + getParent().getId() + PATH_SPLIT);
return getRepository().save(this);
}

使用Guava的Preconditions,可以轻松简单的实现防御式编码。

虽然有很多类库都能实现这个功能,也有非常多优秀的工具类补充类库,但当前我们只依赖了guava,因为它足够优秀

如果你确实需要Apache commons io等一些优秀的第三方工具类库,把它们加入到领域层也是可以的。

但切记一个原则是:

类库要足够优秀到与JDK能处于同一水准,并且是对JDK的有益补充

JPA Annotation类库

事实上,基于Annotation的JPA,有两种做法,一种是使用JPA的标准Annotation,还有一种是使用Hibernate自带的一些Annotation。

基于DDD的理念,我们选用使用JPA 标准Annotation。

这意味着你不应该使用任何Hibernate的Annotation,当然你不用担心这会不会不够用。JPA是标准,事实上除了Hibernate实现以外,还有类似ObjectDB的实现,都对JPA Annotation提供了支持与实现。

JPA本身是足够的,否则不能成为标准。

import javax.persistence.*;

@Entity
@Table(name = "user_",
indexes = {
@Index(name = "index_user_id", columnList = "user_id_")
})
public class User extends BaseDistributedEntity {

@Column(name = "user_id_",nullable = false)
private String userId;

@Column(name = "user_type_")
private UserType userType = UserType.LOCAL;

@Transient
private String password;

@Column(name = "encode_password_",nullable = false)
private String encodePassword;

@Column(name = "name_",nullable = false)
private String name;

@Column(name = "phone_")
private String phone;

@Column(name = "email_")
private String email;

@Column(name = "disabled_")
private boolean disabled;

@Column(name = "created_")
private long created;

@Column(name = "updated_")
private long updated;

//---省略其它无关代码
}

如上述代码所示,@Entity,@Column,@Table都是JPA的标准。都来源于javax.persistence.*这个包

2. 开始领域层的编码

2.1 定义领域实体或值对象

myddd-domain中提供了三个基础领域实体类库,分别是

  • BaseIDEntity //基于数据库主键ID自增,且带有乐观锁的实体基类
  • BaseDistributedEntity //基于雪花分布式算法的主键生成,并且带有乐观锁的实体基类
  • Entity //实体抽象类

对于我们项目,由于是分布式微服务部署,因此如果你的领域是使用关系型数据库来实现,并非Mongo或Redis等其它存储实现,请使用BaseDistributedEntity

import javax.persistence.*;

@Entity
@Table(name = "user_",
indexes = {
@Index(name = "index_user_id", columnList = "user_id_")
})
public class User extends BaseDistributedEntity {

@Column(name = "user_id_",nullable = false)
private String userId;

@Column(name = "user_type_")
private UserType userType = UserType.LOCAL;

@Transient
private String password;

@Column(name = "encode_password_",nullable = false)
private String encodePassword;

@Column(name = "name_",nullable = false)
private String name;

@Column(name = "phone_")
private String phone;

@Column(name = "email_")
private String email;

@Column(name = "disabled_")
private boolean disabled;

@Column(name = "created_")
private long created;

@Column(name = "updated_")
private long updated;

//---省略其它无关代码
}

如上代码所示,我们定义了一个基于JPA的领域类。

关于JPA的一些知识,包括如何配置一对多,一对一等关系,请自行参阅其它教程。

如果你不是JPA,而是使用Mongo或其它非JPA存储实现,请定义它实现Entity基类就可以了。

2.2 定义仓储(Repository)

在计算机的术语中,仓储(Repository)意味着存储的概念,涉及到对数据的存储,查询等,都可以将其归为仓储

并不需要一个实体对应一个仓储,不是这么理解的。可以为一个聚合根定义一个仓储,比如订单与订单明细,只需要定义一个订单仓储就可以了。订单明细也使用这个仓储,因为它们是一个聚合。

我们定义一个仓储

public interface UserRepository extends AbstractRepository {
User queryUserByUserId(String userId);
}

如上代码所示,我们定义了一个UserRepository仓储,并且实现了AbstractRepository接口。

实现AbstractRepository接口并不是必须的,这个接口中定义了实体的几个常用行为,如保存,删除等

2.3 在实体中引用仓储

当定义好仓储后,我们可以在实体中引用这个仓储,并实现我们的领域行为

public class User extends BaseDistributedEntity {

//...省略其它无关代码

private static UserRepository userRepository;

private static UserRepository getUserRepository(){
if(Objects.isNull(userRepository)){
userRepository = InstanceFactory.getInstance(UserRepository.class);
}
return userRepository;
}

public User createLocalUser(){
if(Strings.isNullOrEmpty(name))throw new UserNameEmptyException();
if(Strings.isNullOrEmpty(password))throw new PasswordEmptyException();
if(Strings.isNullOrEmpty(userId))throw new UserIdEmptyException();

this.created = System.currentTimeMillis();
this.encodePassword = getPasswordEncoder().encodePassword(password);
return getUserRepository().save(this);
}

public static User queryUserByUserId(String userId){
return getUserRepository().queryUserByUserId(userId);
}

}

如上代码所示,我们在User类中定义了UserRepository仓储的静态属性,并使用InstanceFactory实例工厂来获取仓储的实现

而后,我们定义了两个领域行为

  • 定义了createLocalUser,创建本地用户的领域行为
  • 定义了queryUserByUserId,根据用户ID查询用户的行为

这两个行为都通过getUserRepository()方法使用到了仓储

这就是面向对象中的依赖倒转原则,我们的领域行为没有依赖具体的数据存储框架,而是依赖了接口。具体的数据存储框架提供实现。

在领域层中,我们会大量用到依赖倒转原则,事实上,依赖倒转原则是构建良好的,可维护的代码的基础。

2.4 编写单元测试

我们可以开始编写单元测试了。

TDD主张测试先行,但是这一点我并不非常认同。你可以在业务实现前编写测试,或是实现后再编写测试。

@Transactional
public class TestUser extends AbstractTest {

@Test
void testCreateUser(){
Assertions.assertThrows(UserNameEmptyException.class,()-> new User().createLocalUser());

User noPasswordUser = new User();
noPasswordUser.setUserId(randomId());
noPasswordUser.setName(randomId());

Assertions.assertThrows(PasswordEmptyException.class, noPasswordUser::createLocalUser);


User noUserIdUser = new User();
noUserIdUser.setName(randomId());
noUserIdUser.setPassword(randomId());
Assertions.assertThrows(UserIdEmptyException.class, noUserIdUser::createLocalUser);

User createdUser = randomUser().createLocalUser();
Assertions.assertNotNull(createdUser);
}


@Test
void testQueryUserByUserId(){

User notExists = User.queryUserByUserId(randomId());
Assertions.assertNull(notExists);

User createdUser = randomUser().createLocalUser();
Assertions.assertNotNull(createdUser);

User query = User.queryUserByUserId(createdUser.getUserId());
Assertions.assertNotNull(query);
}

}

我们对刚刚编写的业务行为,编写了单元测试。

我们会运行它,当然它不会通过,因为我们现在只定义了UserRepository接口,压根没有实现。

没有关系,接下来我们开始基础设施层的编码工作,会提供一个仓储实现。在那之后,这个单元测试就会通过。

关于单元测试,请查阅有关单元测试章节获取更详细的指引与规范