背景 最近一直在了解DDD相关的知识,自己也以DDD构建了几个应用;网上有许多关于DDD的理论知识,但是对于实例讲解,特别是Java语言的较为稀少。
DDD设计思路 在设计控制台这个应用的时候,整体思路沿用的是DDD驱动设计那本书的思路。
整理用户用例需求 构建统一语言 构建限界上下文 构建上下文映射图 构建领域模型 需求用例 用户输入命令,控制台根据命令选择执行者,执行者执行命令后,响应命令输出,并创建快照 用户可以在当前会话查看命令快照,并且选择撤销到某个命令快照上,在这个节点前的所有命令全部回滚 用户可以将多个命令存储为批处理命令,在本次会话中重复性调用执行 用户可以拦截命令执行之前,执行任务之后,执行任务失败,执行任务成功,执行完成的点;即,命令的生命周期 提取实体构建统一语言 根据上述需求,提取名词,这些名词就是需要构建的实体。 上述需求的实体包含有:命令、执行者、批处理、命令结果、历史、命令快照、回滚脚本、会话
构建出如下通用语言:
名词 描述 命令 用户发出的请求 执行者 请求交由计算机系统后,会交由具体某个类,通过这个类内部的业务逻辑执行用户请求 批处理 由一批次有序的命令组成的一个命令集合 命令结果 命令执行完成后,返回给用户的一些结果信息 会话 用户在链接控制台后到断开链接的一整个时间周期 历史 在当前会话中,用户自创建会话以后,执行的所有命令列表 命令快照 在每次命令执行完成后,会保持当次命令的信息 回滚脚本 历史中的每次命令快照,如果支持回滚,则可以回滚命令
构建限界上下文 从提取的实体中,我们按照”高聚合、低耦合“的方式划分不同的领域聚合。划分限界上下文如下图:
很显然,聚合根已经清晰:
构建上下文映射 由需求用例,我们可以了解到聚合和聚合之间的联系。
构建出如下上下文映射图:
构建领域模型 上述实体、聚合根已经明确,互相之间的关系可以通过用例需求来描述清楚,构建出如下领域模型:
方案落地 整体思路已经明确,可以根据上述的领域模型构建出大体的领域层,主要分为三个包,类似三个限界上下文:
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 @Data @EqualsAndHashCode (callSuper = true )public abstract class Command extends AbstractAggregateRoot <String > implements ICommand { private final String name; 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 { 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(session, receiver); } 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 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 ; } 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 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 @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 public class History extends AbstractAggregateRoot <String > { private final Stack<CommandSnapshot> stack = new Stack<>(); private final Session session; public History (AggregateId<String> id, Session session) { super (id); this .session = session; } public void addHistory (ICommand command) { if (command instanceof ShowHistoryCommand) { return ; } stack.push(CommandSnapshot.snapshot(command)); } 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())); } } public void rollback (int index) { 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 @RequiredArgsConstructor @Service public class CommandService { private final CommandRepository commandRepository; private final HistoryRepository historyRepository; private final HistoryFactory historyFactory; public Optional<CommandResult> execute (Session session, String cmd) { session.bindHistory(() -> { History newOne = historyFactory.create(session); historyRepository.create(newOne); return newOne; }); try { return commandRepository.findById(AggregateId.of(cmd)) .map(command -> { session.setCurrentCommand(command); return command.execute(session); }); } finally { session.removeCommand(); } } 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 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.
附录 基础控制台代码 控制台样例代码