1. 引言
1.1. 需求
最近在开发一个基于 Spring Boot 框架的制品管理平台,使用了领域驱动设计的思想进行了业务模型的设计。
开发过程中遇到了一个常见的需求:创建不同类型的制品(Artifact)。需求概括如下:
- 不同的构建任务会生成不同类型的制品,如 Jar 包、Dockers 镜像等;
- 所有类型的制品具备一些公共的属性,如名称、描述、创建时间等;
- 不同类型的制品存在一些独有的属性,如存储位置、是否关联图片等;
- 制品的独有属性用于创建后,触发下游的其他任务。
对于 CRUD Boys 来说,是一个十分常见的需求,最简单的实现方式就是使用贫血模型。
1.2. 贫血模型
贫血模型由 Martin Fowler 在 2003 年提出,是一种将数据和行为分离的设计模式。
在贫血模型中,Controller 层接受不同类型制品的创建请求(DTO),Service 层负责将请求映射为持久化对象(PO),DAO 层负责将持久化对象存储到数据库中。
对于大多数 CRUD 请求来说,贫血模型可以说是一套“万能公式”,支撑起了无数的 Java 服务。但是,贫血模型也有着一些明显的缺点:
- 模型不能反馈业务逻辑,开发人员无法深入地理解业务;
- 大部分的 CRUD 逻辑存在相似性,代码复用性较差;
- 一旦新增了业务逻辑,此处为制品类型,需要新增接口和表结构,维护成本较高。
- 现有流程增加业务逻辑时,需要修改所有的流程,重复工作量较大。
1.3. 基于可复用性的设计
**DRY 原则(Don’t Repeat Yourself)**是软件工程中的一条重要原则,它要求系统减少重复,提高代码的可复用性。
为了做到这一点,首先需要的是对已有的需求进行更高层次的抽象,既要将相似的业务逻辑进行整合,也要让每种类型能够管理自己的特殊属性。其次,适当的引入设计模型也可以简化代码的复杂度,提高代码的可维护性。
2. 需求分析
2.1. 需求建模
让我们回顾一下需求,找到其中的共性和差异性:
- 共性
- 所有类型的制品都有名称、描述、创建时间等属性;
- 制品可以使用相同的逻辑进行处理、存储;
- 制品的独有属性都是用于触发下游任务。
- 差异性
- 不同类型的制品有着不同的特殊属性。
因此,我们首先对创建请求进行抽象,对于 Jar 包和 Docker 镜像类制品,可以创建如下 DTO 类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public abstract class ArtifactDTO {
protected String name;
protected String description;
protected Date createTime;
public abstract ArtifactType getType();
}
public enum ArtifactType {
JAR,
DOCKER;
}
public class JarArtifactDTO extends ArtifactDTO {
private String groupId;
private String artifactId;
private String version;
@Override
public ArtifactType getType() {
return ArtifactType.JAR;
}
}
public class DockerArtifactDTO extends ArtifactDTO {
private String tag;
@Override
public ArtifactType getType() {
return ArtifactType.DOCKER;
}
}
|
DTO 对象如下图所示:
考虑使用 DDD 的设计思想,我们还需要对领域对象进行建模,首先是父类 Artifact
:
1
2
3
4
5
6
7
8
9
| public abstract class Artifact {
protected String name;
protected String description;
protected Date createTime;
// 保存特殊属性,用于触发下游任务
protected Map<String, String> specialProperties;
public abstract ArtifactType getType();
}
|
其次,针对每一个特殊的制品类型,创建一个子类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class JarArtifact extends Artifact {
private String groupId;
private String artifactId;
private String version;
@Override
public ArtifactType getType() {
return ArtifactType.JAR;
}
}
public class DockerArtifact extends Artifact {
private String tag;
@Override
public ArtifactType getType() {
return ArtifactType.DOCKER;
}
}
|
领域对象如下图所示:
2.2. 需求简化
对 DTO 和 Entity 建模后,可以发现制品的创建过程可以被简化为:
- 根据 DTO 的类型创建对应的 Entity;
- 填充 DTO 的共有属性;
- 处理不同类型 DTO 的独有属性。
2.3. 模式选择
在我们的项目中,使用 DTO
表示前端传递的数据,使用 Entity
表示领域模型。每个请求都会经过下述的流程:
- Controller 层接收 DTO;
- Service 层根据 DTO 的内容创建 Entity;
- 处理业务逻辑。
对于这种情况,我们可以使用工厂方法模式和抽象工厂模式来实现。工厂方法模式用于根据 DTO 的类型不同,实现不同种类 Entity 的初始化逻辑;抽象工厂用于根据 DTO 的类型不同,选择不同的工厂方法。
如果对象的初始化较为复杂,还可以使用构建器模式来构建对象。
3. 代码实现
3.1. 工厂方法
首先,我们需要定义一个工厂接口,用于创建不同类型的制品:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| public interface ArtifactFactory<T extends ArtifactDTO> {
/**
* 获取工厂对应的制品类型,用于抽象工厂的选择
*
* @return 制品类型
*/
ArtifactType getArtifactType();
/**
* 根据 DTO 类型创建对应的制品
*
* @param dto 创建制品 DTO
* @return 制品
*/
Artifact createEmptyArtifact();
/**
* 获取制品的特殊属性
*
* @param dto 创建制品 DTO
* @return 特殊属性
*/
Map<String, String> getSpecialProperties(T dto);
/**
* 创建制品
*
* @param dto 创建制品 DTO
* @return 制品
*/
default Artifact createSubArtifact(T dto) {
Artifact artifact = createEmptyArtifact(dto);
artifact.setName(dto.getName());
artifact.setDescription(dto.getDescription());
artifact.setCreateTime(dto.getCreateTime());
artifact.setSpecialProperties(getSpecialProperties(dto));
return artifact;
}
default Artifact createArtifact(ArtifactDTO dto) {
if (dto.getType() != getArtifactType()) {
throw new IllegalArgumentException("Unsupported artifact type: " + dto.getType());
}
return createSubArtifact((T) dto);
}
}
|
在工厂方法中,有以下几点需要注意:
- 为了简化代码,我们提供了一个默认实现
createSubArtifact
方法,用于创建制品、填充共有属性和处理特殊属性; - 我们希望 DTO 到 Entity 的代码可以复用,因此用泛型
T
表示 DTO 类型; - 由于 Java 的泛型擦除机制,我们需要在
createArtifact
方法中进行类型转换,同时在此方法中进行了类型检查。
然后,我们需要为每种类型的制品创建一个具体的工厂类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| @Component
public class JarArtifactFactory implements ArtifactFactory<JarArtifactDTO> {
@Override
public Artifact createEmptyArtifact() {
return new JarArtifact();
}
@Override
public Map<String, String> getSpecialProperties(JarArtifactDTO dto) {
Map<String, String> specialProperties = new HashMap<>();
specialProperties.put("groupId", dto.getGroupId());
specialProperties.put("artifactId", dto.getArtifactId());
specialProperties.put("version", dto.getVersion());
return specialProperties;
}
}
@Component
public class DockerArtifactFactory implements ArtifactFactory<DockerArtifact> {
@Override
public Artifact createEmptyArtifact() {
return new DockerArtifact();
}
@Override
public Map<String, String> getSpecialProperties(DockerArtifact dto) {
Map<String, String> specialProperties = new HashMap<>();
specialProperties.put("tag", dto.getTag());
return specialProperties;
}
}
|
将每一个工厂类标记为 @Component
,可以让 Spring Boot 自动扫描并注册到容器中。
3.2. 抽象工厂
工厂方法创建好后,我们还期望可以根据 DTO 的类型自动选择对应的工厂。
考虑在 Spring 中可以使用 @Autowired
注解来自动注入所有的工厂 Bean,我们可以提供制品类型到工厂的映射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| @Component
public class ArtifactFactoryRegistry {
private final Map<ArtifactType, ArtifactFactory> factoryMap;
/**
* 构造函数,使用 Spring 自动注入所有的工厂列表
*
* @param factories 工厂列表
*/
@Autowired
public ArtifactFactoryRegistry(List<ArtifactFactory> factories) {
factoryMap = factories.stream()
.collect(Collectors.toMap(ArtifactFactory::getArtifactType, Function.identity()));
}
/**
* 根据 DTO 类型创建对应的制品
*
* @param dto 创建制品 DTO
* @return 制品
*/
public Artifact createArtifact(ArtifactDTO dto) {
ArtifactFactory factory = factoryMap.get(dto.getType());
if (factory == null) {
throw new IllegalArgumentException("Unsupported artifact type: " + dto.getType());
}
return factory.createArtifact(dto);
}
}
|
3.3. 使用
在 Service 层中,我们可以直接使用 ArtifactFactoryRegistry
来创建制品:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @Service
public class ArtifactService {
private final ArtifactFactoryRegistry factoryRegistry;
@Autowired
public ArtifactService(ArtifactFactoryRegistry factoryRegistry) {
this.factoryRegistry = factoryRegistry;
}
public void createArtifact(ArtifactDTO dto) {
Artifact artifact = factoryRegistry.createArtifact(dto);
// ... 省略其他业务逻辑
}
}
|
4. 优势
通过引入抽象工厂和工厂方法模式,我们可以获得以下优势:
- 易扩展:新增一个制品类型,只需要新增一个工厂类,Service 层的代码完全不需要修改;
- 好维护:不同类型的制品使用不同的工厂,代码逻辑清晰,易于维护。
5. 后续优化
5.1. 接口可以合并
在示例代码中仅提供了 Service 的使用,在我们目前的视线中,Controller 层还是为一个类型的制品单独提供了创建的接口,后续计划使用 Jackson 的反序列化特性,将所有的制品类型统一为一个接口。
6. 结论
DRY 原则是软件工程中的一条重要原则,它要求系统减少重复,提高代码的可复用性。
通过对需求深度分析,提供更高阶别的抽象,可以帮助找到系统中的共性和差异,提升代码的复用性。
通过引入设计模式,可以进一步简化代码的复杂度,提高代码的可维护性,有助于后续的扩展。
将复杂的问题化繁为简,在一层层的抽丝剥茧后,找到最简洁的设计并予以实现,这正是软件开发的乐趣所在。
7. 参考
- Martin Fowler: AnemicDomainModel
- Abstract Factory Pattern
- Factory Method Pattern