前言
公司最近有个需求就是要解析用户的画图行为动作,客户端记录保存了用户的行为+数据,然后将各端实现不同的需求,比如用户的画图行为回放,还要实现比如常用编辑器的撤销恢复等功能,在此可以使用命令设计模式实现,看了网上很多文章千篇一律,也只是实现了简单的undo、redo功能,所以也请教同事给了一份写好的客户端算法代码作为参考,然后融合到自己的后端代码中。
实现原理
大概实现原理:将每次执行过的命令和数据保存到undo回退队列中,当执行undo操作时候取出队列数据进行执行即可,若是添加操作,undo时则执行删除操作,若是删除操作,undo时则实行添加操作,每次执行完后放入redo队列中,用于下一次的redo。
由于公司代码不允许外传,所以自己模拟写了一个简单的说话后悔药的功能分享给大家,看懂代码理论上适用于95%以上的撤销恢复场景。
后悔药DEMO案例实现功能:允许你随便说,记录过程+行为,然后只呈现你最终要说出去的话。
代码实现
SomeThingDomain.java:任意数据和行为实体,可以根据自己业务扩展,这里做demo只是呈现保存一句话desc;
/**
* SomeThingDomain
* 任意数据和行为
* @author lcry
*/
@Data
@Builder
public class SomeThingDomain implements Serializable, Cloneable {
/**
* 操作类型
*/
private OperateTypeEnum operateType;
/**
* 索引
*/
private List<Integer> index;
/**
* 说的一句话
*/
private String desc;
/**
* 克隆对象
*/
@Override
public SomeThingDomain clone() {
try {
return (SomeThingDomain) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
OperateTypeEnum.java:操作枚举,可以自己添加更多
/**
* OperateTypeEnum
* 操作类型枚举
*
* @author lcry
*/
public enum OperateTypeEnum {
/**
* 未知
*/
UNKNOWN(0),
/**
* 说一句话
*/
ADD(1),
/**
* 删除随机几段话
*/
DEL(2),
/**
* 清空所有说话
*/
CLEAR(3),
/**
* 撤销说的一些话
*/
UNDO(4),
/**
* 恢复说过的话
*/
REDO(5);
OperateTypeEnum(Integer type) {
}
}
Action.class:动作接口
/**
* Action
* 动作接口
*
* @author lcry
*/
public interface Action {
/**
* 执行命令,可以是增加、删除、清空、剪切、粘贴等操作
*/
void exec();
/**
* 撤销
*/
void undo();
/**
* 恢复
*/
void redo();
}
AddAction.java:添加操作实现类
/**
* AddAction
* 添加操作
* 添加一句话或者多句话
* @author lcry
*/
public class AddAction implements Action {
/**
* 最终要说出的话
*/
private final List<SomeThingDomain> result;
/**
* 缓存添加的那一句话
*/
private final List<SomeThingDomain> temp = new ArrayList<>();
/**
* 当前要添加的那句话
*/
private final SomeThingDomain addSomeThingDomain;
public AddAction(List<SomeThingDomain> result, SomeThingDomain addSomeThingDomain) {
this.result = result;
this.addSomeThingDomain = addSomeThingDomain;
}
@Override
public void exec() {
// 添加一句话
result.add(addSomeThingDomain);
}
@Override
public void undo() {
// 撤销添加的那一句话
result.remove(addSomeThingDomain);
// 清空缓存
temp.clear();
// 把撤销的那句话添加到缓存中,方便下一次在redo
temp.add(addSomeThingDomain);
}
@Override
public void redo() {
// 恢复撤销的那一句话
result.add(addSomeThingDomain);
// 清空缓存
temp.clear();
// 把恢复的那句话添加到缓存中,方便下一次再undo
temp.add(addSomeThingDomain);
}
}
CleanAction.java、DeleteAction.java和AddAction.java代码差不多,只是逻辑不一样,篇幅有限参考文章前面的思路编写即可。
OperateManager.java:操作管理类
/**
* OperateManager
* 操作管理
*
* @author lcry
*/
public class OperateManager {
/**
* 最终要说出的话
*/
private final List<SomeThingDomain> result = new ArrayList<>();
/**
* 动作管理,保存每次操作用于undo、redo
*/
private final ActionManager actionManager = new ActionManager();
/**
* 解析说话过程->最终要表达的话
*
* @param paths 每次说出的一句话,行为+数据
* @return 显示的最终笔迹
*/
public List<SomeThingDomain> getResults(List<SomeThingDomain> paths) {
result.clear();
for (SomeThingDomain op : paths) {
Action action = null;
switch (op.getOperateType()) {
case ADD:
System.out.println("执行添加操作" + print());
action = new AddAction(result, op.clone());
break;
case DEL:
System.out.println("执行删除操作" + print());
action = new DeleteAction(result, op.getIndex());
break;
case CLEAR:
System.out.println("执行删除操作" + print());
action = new CleanAction(result);
break;
case UNDO:
System.out.println("执行撤销操作" + print());
actionManager.undo().undo();
break;
case REDO:
System.out.println("执行恢复操作" + print());
actionManager.redo().redo();
break;
default:
break;
}
if (action != null) {
// 命令模式:封装成对象去执行操作,而不是直接去执行操作
action.exec();
actionManager.setLastAction(action);
}
}
// 清空之前的undo redo 操作队列
actionManager.clear();
return result;
}
/**
* 打印每个步骤的结果
*/
private String print() {
StringBuffer resultTalk = new StringBuffer(" -> 结果:");
result.forEach(a -> resultTalk.append(a.getDesc()));
return resultTalk.toString();
}
}
测试用例代码:
/**
* TestSomeThing
* 测试用例
*
* @author lcry
*/
public class TestSomeThing {
/**
* 测试整个说话流程
*
* @param args
*/
public static void main(String[] args) {
OperateManager operateManager = new OperateManager();
List<SomeThingDomain> results = operateManager.getResults(getDemoSomeThing());
StringBuffer resultTalk = new StringBuffer();
results.forEach(a -> resultTalk.append(a.getDesc()));
System.out.println("最终要说出的话为:" + resultTalk);
}
/**
* 模拟数据
*
* @return 模拟完整的一个说话流程
*/
private static List<SomeThingDomain> getDemoSomeThing() {
// 结果:第一句话|
SomeThingDomain oneAdd = SomeThingDomain.builder().operateType(OperateTypeEnum.ADD).index(Collections.singletonList(0)).desc("第一句话|").build();
// 结果: 第一句话|第二句话|
SomeThingDomain twoAdd = SomeThingDomain.builder().operateType(OperateTypeEnum.ADD).index(Collections.singletonList(1)).desc("第二句话|").build();
// 结果: 第一句话| 第二句话| 第三句话|
SomeThingDomain threeAdd = SomeThingDomain.builder().operateType(OperateTypeEnum.ADD).index(Collections.singletonList(2)).desc("第三句话|").build();
// 结果: 第一句话| 第二句话| 第三句话| 第四句话|
SomeThingDomain fourAdd = SomeThingDomain.builder().operateType(OperateTypeEnum.ADD).index(Collections.singletonList(3)).desc("第四句话|").build();
// 结果: 第一句话| 第二句话| 第三句话| 第四句话| 第五句话|
SomeThingDomain fivesAdd = SomeThingDomain.builder().operateType(OperateTypeEnum.ADD).index(Collections.singletonList(4)).desc("第五句话|").build();
// 结果: 第四句话| 第五句话
SomeThingDomain delete = SomeThingDomain.builder().operateType(OperateTypeEnum.DEL).index(Arrays.asList(0, 1, 2)).build();
SomeThingDomain undo = SomeThingDomain.builder().operateType(OperateTypeEnum.UNDO).build();
SomeThingDomain redo = SomeThingDomain.builder().operateType(OperateTypeEnum.REDO).build();
SomeThingDomain clear = SomeThingDomain.builder().operateType(OperateTypeEnum.CLEAR).build();
return Arrays.asList(oneAdd, twoAdd, threeAdd, fourAdd, fivesAdd, delete, undo, redo, clear, undo);
}
}
执行结果:
....
执行添加操作 -> 结果:
执行添加操作 -> 结果:第一句话|
执行添加操作 -> 结果:第一句话|第二句话|
执行添加操作 -> 结果:第一句话|第二句话|第三句话|
执行添加操作 -> 结果:第一句话|第二句话|第三句话|第四句话|
执行删除操作 -> 结果:第一句话|第二句话|第三句话|第四句话|第五句话|
执行撤销操作 -> 结果:第四句话|第五句话|
执行恢复操作 -> 结果:第一句话|第二句话|第三句话|第四句话|第五句话|
执行删除操作 -> 结果:第四句话|第五句话|
执行撤销操作 -> 结果:
最终要说出的话为:第四句话|第五句话|