WhatAKitty Daily

A Programmer's Daily Record

以DDD构造通用Shell控制台

WhatAKitty   阅读次数loading...

背景

最近一直在了解DDD相关的知识,自己也以DDD构建了几个应用;网上有许多关于DDD的理论知识,但是对于实例讲解,特别是Java语言的较为稀少。

DDD设计思路

在设计控制台这个应用的时候,整体思路沿用的是DDD驱动设计那本书的思路。

  1. 整理用户用例需求
  2. 构建统一语言
  3. 构建限界上下文
  4. 构建上下文映射图
  5. 构建领域模型

需求用例

  • 用户输入命令,控制台根据命令选择执行者,执行者执行命令后,响应命令输出,并创建快照
  • 用户可以在当前会话查看命令快照,并且选择撤销到某个命令快照上,在这个节点前的所有命令全部回滚
  • 用户可以将多个命令存储为批处理命令,在本次会话中重复性调用执行
  • 用户可以拦截命令执行之前,执行任务之后,执行任务失败,执行任务成功,执行完成的点;即,命令的生命周期

提取实体构建统一语言

根据上述需求,提取名词,这些名词就是需要构建的实体。
上述需求的实体包含有:命令、执行者、批处理、命令结果、历史、命令快照、回滚脚本、会话

构建出如下通用语言:

名词描述
命令用户发出的请求
执行者请求交由计算机系统后,会交由具体某个类,通过这个类内部的业务逻辑执行用户请求
批处理由一批次有序的命令组成的一个命令集合
命令结果命令执行完成后,返回给用户的一些结果信息
会话用户在链接控制台后到断开链接的一整个时间周期
历史在当前会话中,用户自创建会话以后,执行的所有命令列表
命令快照在每次命令执行完成后,会保持当次命令的信息
回滚脚本历史中的每次命令快照,如果支持回滚,则可以回滚命令

构建限界上下文

从提取的实体中,我们按照”高聚合、低耦合“的方式划分不同的领域聚合。划分限界上下文如下图:

限界上下文

很显然,聚合根已经清晰:

  • 命令
  • 历史
  • 会话

构建上下文映射

由需求用例,我们可以了解到聚合和聚合之间的联系。

  • 命令执行后创建
  • 在会话中查看历史
  • 命令执行后创建快照

构建出如下上下文映射图:

上下文映射图

构建领域模型

上述实体、聚合根已经明确,互相之间的关系可以通过用例需求来描述清楚,构建出如下领域模型:

领域模型

方案落地

整体思路已经明确,可以根据上述的领域模型构建出大体的领域层,主要分为三个包,类似三个限界上下文:

  • command
  • session
  • history

Command核心子域

Command是核心子域,是整个控制台的能力体现,主要解决用户命令执行的逻辑。

在这个限界上下文内定义了Command类,并实现了执行、回撤的能力。

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* Command aggregate root
*
* behavior:
* 1. execute
* 2. undo
*
* @author WhatAKitty
* @date 2019/05/01
* @description
**/
@Data
@EqualsAndHashCode(callSuper = true)
public abstract class Command extends AbstractAggregateRoot<String> implements ICommand {

/**
* command name
*/
private final String name;
/**
* the receiver to actually execute
*/
private final IReceiver receiver;

public Command(AggregateId<String> id, String name, IReceiver receiver) {
super(id);
this.name = name;
this.receiver = receiver;
}

@Override
public CommandResult execute(Session session) {
publishEvent(new CommandBeforeExecuteEvent(session));
try {
// invoke
final Object result = execute(session, receiver);
publishEvent(new CommandAfterExecuteEvent(session, result));
return CommandResult.of(true, result);
} catch (Throwable e) {
publishEvent(new CommandExecuteFailedEvent(session, e));
return CommandResult.of(false, null);
} finally {
publishEvent(new CommandFinishedEvent(session));
}
}

@Override
public void undo(Session session) {
if (!supportUndo()) {
throw new UnsupportedOperationException("unsupported undo operation");
}
// undo
undo(session, receiver);
}

/**
* create a batch command task
*
* @param commands commands list
* @return batch command instance
*/
public static BatchCommand createBatch(List<ICommand> commands) {
return new BatchCommand(commands);
}

protected abstract Object execute(Session session, IReceiver receiver);

protected Object undo(Session session, IReceiver receiver) {
return null;
}

}

在这里,Command类被声明为抽象类,因为命令这个聚合根比较特殊,每个聚合根值都是以类的形式存在的,每个具体命令都有自身的执行逻辑。

Command类内每个具体的Command都有一个对应的Receiver,也就是执行者;在执行者内会执行这个命令的具体逻辑,然后将CommandResult,也就是执行结果返回给用户。

比如,在这里默认实现的显示历史命令:

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
/**
* show history command
*
* @author WhatAKitty
* @date 2019/05/02
* @description
**/
public class ShowHistoryCommand extends Command {

public static final String COMMAND_TIP = "history";

public ShowHistoryCommand(AggregateId<String> id) {
super(id, COMMAND_TIP, new ShowHistoryReceiver());
}

@Override
protected Object execute(Session session, IReceiver receiver) {
receiver.invoke(session);
return null;
}

@Override
public boolean supportUndo() {
return false;
}

/**
* show history receiver
*/
public static final class ShowHistoryReceiver extends Receiver {

public ShowHistoryReceiver() {
super(session -> session.getHistory().showHistory());
}

}

}

历史命令继承了Command抽象类,它是一个具体的聚合根值。在命令执行逻辑这里,它将会话参数传递到了执行者处,进行具体的执行。
在这段逻辑内:

1
super(session -> session.getHistory().showHistory());

会话本身会获取会话内存储的命令历史,并执行它的能力,显示历史命令快照列表。

Session支撑子域

Session作为承上启下的支撑子域,内部能力非常丰富,包含有添加用户自定义参数、获取参数、绑定命令快照历史、设置当前执行命令等等能力。

在这里主要描述它的绑定历史以及设置当前命令的两个能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* bind history into this session
*
* @param fetcher not exists history, do get it from fetcher
*/
public History bindHistory(HistoryFetcher fetcher) {
if (this.history != null) {
return this.history;
}

synchronized (HISTORY_LOCK) {
if (this.history == null) {
this.history = fetcher.invoke();
}
return this.history;
}
}

如上代码是绑定历史的具体实现,如果在发现当前会话无历史的情况下,会在加锁的条件下,从fetcher中获取命令历史的实例进行绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* current command to execute
*/
@Getter
private final ThreadLocal<ICommand> command = new ThreadLocal<>();

public void setCurrentCommand(ICommand cmd) {
command.set(cmd);
}

public ICommand currentCommand() {
return command.get();
}

public void removeCommand() {
command.remove();
}

设置当前命令、获取当前命令、移除当前命令主要通过ThreadLocal类来实现,按照控制台设计,用户在执行某个命令的时候是单线程操作。

历史子域

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* history aggregate root
*
* @author WhatAKitty
* @date 2019/05/02
* @description
**/
public class History extends AbstractAggregateRoot<String> {

/**
* the stack of command snapshot
*/
private final Stack<CommandSnapshot> stack = new Stack<>();
/**
* session
*/
private final Session session;

public History(AggregateId<String> id, Session session) {
super(id);
this.session = session;
}

/**
* add command into history stack
*
* @param command the command to execute successfully
*/
public void addHistory(ICommand command) {
// ignore history command itself
if (command instanceof ShowHistoryCommand) {
return;
}
// create command snapshot and push the snapshot into history stack
stack.push(CommandSnapshot.snapshot(command));
}

/**
* show the commands execute previously
*/
public void showHistory() {
int total = stack.size();
for (int i = 0; i < total; i++) {
CommandSnapshot snapshot = stack.get(i);
StreamMgr.getINSTANCE().println(String.format("[%s] %s", i + 1, snapshot.getName()));
}
}

/**
* rollback the current stage
*
* @param index the rollback index, started with 1
*/
public void rollback(int index) {
// check arguments
if (index < 1 || stack.size() < index) {
throw new IllegalArgumentException("exceed max rollback");
}

for (int i = 1; i <= index; i++) {
CommandSnapshot snapshot = stack.remove(i - 1);
snapshot.undo(session);
StreamMgr.getINSTANCE().println(String.format("The command %s has been undo", snapshot.getName()));
}
}

}

历史子域是对命令快照的一个管理,在执行完命令后,可以创建一个命令快照存入历史栈内;在展现命令快照列表的时候,可以从站内读取命令快照。在回滚的时候,可以以栈的形式出栈并执行回滚命令,如果命令不支持回滚,则会回滚失败。

Application层以及仓储层

应用控制台主要为了能够让用户调用命令,那么势必需要将各领域的能力做一个结合:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* command service
*
* execute command and add execute history
*
* @author WhatAKitty
* @date 2019/05/02
* @description
**/
@RequiredArgsConstructor
@Service
public class CommandService {

private final CommandRepository commandRepository;
private final HistoryRepository historyRepository;
private final HistoryFactory historyFactory;

/**
* 执行命令
*
* @param session session
* @param cmd command
*/
public Optional<CommandResult> execute(Session session, String cmd) {
// 获取或者重新创建绑定一个命令历史
session.bindHistory(() -> {
// 创建命令历史
History newOne = historyFactory.create(session);
historyRepository.create(newOne);
return newOne;
});

// 执行具体命令
try {
// 根据cmd从仓储中获取某个具体命令来执行 ①
return commandRepository.findById(AggregateId.of(cmd))
.map(command -> {
session.setCurrentCommand(command);
return command.execute(session);
});
} finally {
session.removeCommand();
}
}

/**
* 创建批处理
*
* @param session session
* @param batchName batch command name
* @param cmds user defined command list
* @return create successfully
*/
public boolean createBatch(Session session, String batchName, List<String> cmds) {
// 从仓储中获取所有命令 ②
List<ICommand> commands = commandRepository.findInIds(cmds.parallelStream()
.map(AggregateId::of)
.collect(Collectors.toList()));
BatchCommand batchCommand = Command.createBatch(commands);
// 将批处理作为一个新命令存储到仓储 ③
return commandRepository.saveCommand(batchName, batchCommand);
}

}

在①、②、③处,都需要跟仓储交互,在仓储中,存储了cmd以及command的实例;在每次应用启动后,都会讲命令以及命令实例的映射加载到内存中以供使用。

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
/**
* default command repository
*
* @author WhatAKitty
* @date 2019/05/03
* @description
**/
public class DefaultCommandRepository extends InMemoryRepository<String, ICommand> implements CommandRepository {

@Override
public Optional<ICommand> findById(AggregateId<String> commandId) {
return Optional.ofNullable(get(commandId));
}

@Override
public List<ICommand> findInIds(List<AggregateId<String>> commandIds) {
return commandIds.parallelStream()
.map(this::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}

@Override
public boolean saveCommand(String commmandName, ICommand command) {
return putIfAbsent(AggregateId.of(commmandName), command) == null;
}

@Override
protected void init() {
// 每次启动后,都会加载如下两个命令。 ④
AggregateId<String> showHistoryId = AggregateId.of(ShowHistoryCommand.COMMAND_TIP);
AggregateId<String> undoHistoryId = AggregateId.of(UnDoCommand.COMMAND_TIP);
put(showHistoryId, new ShowHistoryCommand(showHistoryId));
put(undoHistoryId, new UnDoCommand(undoHistoryId));
}
}

在④处加载了默认的两个命令,一个是展示历史、一个是回滚命令。

如果需要扩展,则可以继承并覆写该方法:

1
2
3
4
5
6
7
8
9
10
11
@Component
public class DemoCommandRepository extends DefaultCommandRepository {

@Override
protected void init() {
super.init();
final AggregateId<String> id = AggregateId.of(SayHelloWorldCommand.COMMAND_TIP);
put(id, new SayHelloWorldCommand(id));
}

}

总结

That’s all.

附录

基础控制台代码
控制台样例代码