思考命令模式
命令模式就是通过将请求封装成对象,实现请求的发送者和接收者之间的解耦,同时支持队列操作、日志记录和撤销操作。
1.命令模式的本质
命令模式的本质:封装请求。
命令模式的本质是将请求封装成一个对象,使得可以用不同的请求对客户端进行参数化,支持队列操作、日志记录和撤销操作,并且能够实现请求的发送者和接收者之间的解耦。
什么是
将请求封装成一个对象,使得可以用不同的请求对客户端进行参数化
?
例如下面的厨师做菜案例, 我们可以定义一个抽象的命令接口 Command,其中包含一个执行方法 execute()。然后,我们可以创建具体的命令类,如 FishCommand(做鱼香肉丝的命令) 和 MeatCommand(做北京烤鸭的命令),每个类封装了不同的请求操作。
客户端可以根据需要创建不同的命令对象,并将这些命令对象传递给请求者对象(如菜单)。这样,不同的请求操作可以通过命令对象的参数化来实现,而不需要直接调用具体的请求操作。
2.何时选用命令模式
建议在以下情况时选用命令模式。
- 当需要将请求操作的调用者和接收者解耦时,以便能够灵活地对它们进行变化和扩展。
- 当需要对请求进行参数化,使得可以在不同的请求之间进行切换和组合,从而实现动态的请求操作。
- 当需要支持命令的撤销、恢复、排队、日志记录等特性时。
- 当需要实现任务调度、异步处理、队列操作等场景时。
- 当需要构建具有嵌套命令结构的系统,以支持复杂的操作和组合。
典型场景:
-
菜单和按钮操作:在图形用户界面中,可以将不同的菜单项和按钮操作封装成具体的命令对象,并将其与相应的请求操作关联。这样,可以轻松地添加、修改和组合不同的命令,实现灵活的界面交互。
-
任务调度和异步处理:命令模式可以用于实现任务调度器,将任务封装成命令对象并按照需要进行调度和执行。同时,可以支持异步处理和任务队列,以提高系统的并发性和性能。
-
撤销和恢复操作:命令模式可以记录命令的执行历史,使得可以轻松地实现撤销和恢复操作。每个命令对象可以保存执行所需的状态和参数,从而可以在需要时撤销执行操作,或者重新执行先前的操作。
-
日志记录和审计功能:由于每个命令对象都封装了执行操作的细节和参数,可以很容易地记录和存储命令对象。这样可以实现日志记录和审计功能,用于跟踪和监控系统的操作。
-
复杂操作和组合:命令模式支持构建具有嵌套命令结构的系统,以支持复杂的操作和组合。可以将多个命令对象组合成一个复合命令,从而实现更高层次的操作和控制。
3.优缺点
命令模式的优点
-
解耦调用者和接收者
命令模式将请求操作封装成命令对象,使得调用者和接收者之间解耦,调用者无需知道具体的接收者,只需调用命令对象即可,从而提高了系统的灵活性和可维护性。 -
容易扩展新的命令
由于命令模式将请求操作封装成命令对象,因此非常容易添加新的命令类,而无需修改现有的代码,符合开闭原则。 -
支持队列操作和撤销操
命令模式可以将命令对象放入队列中,实现命令的排队、延迟和调度等操作。同时,由于命令对象封装了操作的状态和参数,可以实现命令的撤销和恢复。 -
支持日志记录和审计
由于命令对象封装了请求操作的细节和参数,可以很容易地记录和存储命令对象,实现日志记录和审计功能。
缺点。
- 类膨胀
使用命令模式会增加代码量,因为每个具体的命令都需要一个单独的类来实现。如果命令非常多,可能会导致类的数量剧增,增加系统的复杂性。 - 可能引入过多的细粒度命令对象
在一些简单的场景下,命令模式可能会引入过多的细粒度命令对象,增加了系统的开销。
4.命令模式的结构
- Command:定义命令的接口,声明执行的方法。
- ConcreteCommand:命令接口实现对象,是“虚”的实现;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
- Receiver:接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
- Invoker:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
- Client:创建具体的命令对象,并且设置命令对象的接收者。注意这个不是我们常规意义上的客户端,而是在组装命令对象和接收者,或许,把这个Client称为装配者会更好理解,因为真正使用命令的客户端是从Invoker来触发执行。
5.实现
耦合写法
模拟电脑开机,点击机箱开机按钮,调用主板初始化系统,然后用户就能操作了
1.主板类
/**
* @description:主板接口
*/
public interface MainBoardApi {
/**
* 开机
*/
void open();
}
/**
* @description:技嘉主板
*/
public class JiJiaMainBoard implements MainBoardApi{
@Override
public void open() {
System.out.println("技嘉主板正在开机,请稍后");
System.out.println("接通电源.............");
System.out.println("设备检查.............");
System.out.println("装载系统.............");
System.out.println("机器正常运行,请操作....");
}
}
/**
* @description:微星主板
*/
public class WeiXinMainBoard implements MainBoardApi{
@Override
public void open() {
System.out.println("微星主板正在开机,请稍后");
System.out.println("接通电源.............");
System.out.println("设备检查.............");
System.out.println("装载系统.............");
System.out.println("机器正常运行,请操作....");
}
}
2.开机按钮类
/**
* @description:机箱开机按钮
*/
public class BoxButton {
/**
* 点击开机按钮,就开机
*/
public void boot(int flag){
if (1==flag){
new JiJiaMainBoard().open();
}else {
new WeiXinMainBoard().open();
}
}
}
3.测试类
/**
* @description:最开始耦合写法
*/
public class Test1 {
public static void main(String[] args) {
//点击按钮开机
new BoxButton().boot(2);
}
}
现在,机箱按钮直接调用主板,是强耦合的关系,很不利于维护
命令模式优化耦合写法
改造上面的耦合写法,主板接口和实现类不变
1.调用程序类(也就是上面的机箱按钮)
/**
* @description:调用程序(机箱开机按钮)
*/
public class Invoker {
/**
* 持有命令对象
*/
private Command command=null;
public void setCommand(Command command) {
this.command = command;
}
/**
* 开机
*/
public void boot(){
command.execute();
}
}
2.命令类
/**
* @description:命令接口
*/
public interface Command {
/**
* 执行命令
*/
void execute();
}
/**
* @description:具体命令类(这里是开机命令)
*/
@AllArgsConstructor
public class ConcreteCommand implements Command{
/**
* 持有主板对象
*/
private MainBoardApi mainBoardApi;
@Override
public void execute() {
//命令类不能进行开机操作
//调用主板进行开机
mainBoardApi.open();
}
}
3.测试类
/**
* @description:测试类
* @createTime 2022/11/30 13:06
*/
public class Client {
public static void main(String[] args) {
//把命令和实现组装起来
Command command=new ConcreteCommand(new WeiXinMainBoard());
//为机箱按钮设置命令
Invoker invoker = new Invoker();
invoker.setCommand(command);
//模拟开机按钮
invoker.boot();
}
}
4.效果
命令模式实现撤销
撤销有两种:
- 补偿式(反操作式)
- 存储恢复式
模拟假如存在一个存在一个撤销按钮,电脑开机后,点击撤销按钮,撤销开机操作,也就是进行关机
1.主板类增加关机功能
/**
* @description:主板接口
*/
public interface MainBoardApi {
/**
* 开机
*/
void open();
/**
* 关机
*/
void close();
}
/**
* @description:技嘉主板
*/
public class JiJiaMainBoard implements MainBoardApi{
@Override
public void open() {
System.out.println("技嘉主板正在开机,请稍后");
System.out.println("接通电源.............");
System.out.println("设备检查.............");
System.out.println("装载系统.............");
System.out.println("机器正常运行,请操作....");
}
@Override
public void close() {
System.out.println("技嘉主板正在关机,请稍后");
System.out.println("关机成功.............");
}
}
/**
* @description:微星主板
*/
public class WeiXinMainBoard implements MainBoardApi{
@Override
public void open() {
System.out.println("微星主板正在开机,请稍后");
System.out.println("接通电源.............");
System.out.println("设备检查.............");
System.out.println("装载系统.............");
System.out.println("机器正常运行,请操作....");
}
@Override
public void close() {
System.out.println("微星主板正在关机,请稍后");
System.out.println("关机成功.............");
}
}
2.命令接口增加撤销命令
/**
* @description:命令接口
*/
public interface Command {
/**
* 执行命令
*/
void execute();
/**
* 撤销命令
*/
void undo();
}
/**
* @description:具体命令类(这里是开机命令)
*/
@AllArgsConstructor
public class ConcreteCommand implements Command{
/**
* 持有主板对象
*/
private MainBoardApi mainBoardApi;
@Override
public void execute() {
//命令类不能进行开机操作
//调用主板进行开机
mainBoardApi.open();
}
@Override
public void undo() {
//撤销开机,也就是关机
mainBoardApi.close();
}
}
3.调用程序增加关机功能
/**
* @description:调用程序(机箱开机按钮)
*/
public class Invoker {
/**
* 持有命令对象
*/
private Command command=null;
public void setCommand(Command command) {
this.command = command;
}
/**
* 开机
*/
public void boot(){
command.execute();
}
/**
* 关机
*/
public void shutdown(){
command.undo();
}
}
4.测试类
public class Client {
public static void main(String[] args) {
//把命令和实现组装起来
Command command=new ConcreteCommand(new WeiXinMainBoard());
//为机箱按钮设置命令
Invoker invoker = new Invoker();
invoker.setCommand(command);
//模拟开机按钮
invoker.boot();
//模拟点击撤销按钮,电脑关机
invoker.shutdown();
}
}
5.结果
命令模式实现厨师做菜
模拟两个厨师,一个做热菜,一个做凉菜,服务员点菜
1.厨师类
/**
* @description:厨师接口
*/
public interface CookApi {
/**
* 做菜
* @param name
*/
void cook(String name);
}
/**
* @description:热菜
*/
public class HotCook implements CookApi{
@Override
public void cook(String name) {
System.out.println("厨师正在做:"+name);
}
}
/**
* @description:凉菜
*/
public class CoolCook implements CookApi{
@Override
public void cook(String name) {
System.out.println("厨师正在做:"+name);
}
}
2.命令类
/**
* @description:命令接口
*/
public interface Command {
/**
* 执行命令
*/
void execute();
}
/**
* @description:生鱼刺身
*/
public class FishCommand implements Command{
private CookApi cookApi=null;
public void setCookApi(CookApi cookApi) {
this.cookApi = cookApi;
}
@Override
public void execute() {
cookApi.cook("生鱼刺身");
}
}
/**
* @description:北京烤鸭
*/
public class MeatCommand implements Command{
private CookApi cookApi=null;
public void setCookApi(CookApi cookApi) {
this.cookApi = cookApi;
}
@Override
public void execute() {
cookApi.cook("北京烤鸭");
}
}
3.菜单类
/**
* @description:菜单
*/
public class Menu{
private Collection<Command> menu=new ArrayList<>();
/**
* 点菜,将菜品加入菜单
* @param cmd
*/
public void addCommand(Command cmd){
menu.add(cmd);
}
public void execute() {
//遍历菜单,做菜
for (Command cmd:menu){
cmd.execute();
}
}
}
4.服务员(调用程序)
/**
* @description:调用程序(服务员)
*/
public class Waiter {
private Menu menu=new Menu();
/**
* 客人点菜
* @param cmd
*/
public void orderDish(Command cmd){
//判断菜品是热菜还是凉菜
if (cmd instanceof FishCommand){
((FishCommand)cmd).setCookApi(new HotCook());
}else {
((MeatCommand)cmd).setCookApi(new CoolCook());
}
//加到菜单中
menu.addCommand(cmd);
}
/**
* 点菜完毕,执行命令去做菜
*/
public void orderOver(){
menu.execute();
}
}
5.测试类
public class Client {
public static void main(String[] args) {
Waiter waiter = new Waiter();
//点菜
waiter.orderDish(new FishCommand());
waiter.orderDish(new MeatCommand());
//点菜完毕
waiter.orderOver();
}
}
6.结果
命令模式实现排队
修改代码如下
1.测试类
public class Client {
public static void main(String[] args) {
//创建3位厨师
HotCook cook1 = new HotCook("张三");
HotCook cook2 = new HotCook("李四");
HotCook cook3 = new HotCook("王五");
//启动线程
new Thread(cook1).start();
new Thread(cook2).start();
new Thread(cook3).start();
//模拟10桌客人
for (int i=0;i<10;i++){
Waiter waiter = new Waiter();
//每个客人都点了北京烤鸭和生鱼刺身
waiter.orderDish(new FishCommand(i));
waiter.orderDish(new MeatCommand(i));
//点菜完毕
waiter.orderOver();
}
}
}
2.服务员类
/**
* @description:调用程序(服务员)
*/
public class Waiter {
private Menu menu =new Menu();
/**
* 客人点菜
* @param cmd
*/
public void orderDish(Command cmd){
//加到菜单中
menu.addCommand(cmd);
}
/**
* 点菜完毕,执行命令去做菜
*/
public void orderOver(){
menu.execute();
}
}
3.菜单类
/**
* @description:菜单
*/
public class Menu {
private Collection<Command> menu=new ArrayList<>();
/**
* 点菜,将菜品加入菜单
* @param cmd
*/
public void addCommand(Command cmd){
menu.add(cmd);
}
/**
* 获取菜单中所有命令
* @return
*/
public Collection<Command> getCommands(){
return this.menu;
}
public void execute() {
//将菜单传给后厨
CommandQueue.addMenu(this);
}
}
4.后厨管理类(队列)
/**
* @description:菜单队列
*/
public class CommandQueue {
private static List<Command> cmds=new ArrayList<>();
/**
* 将菜单中做菜的命令取出来,加入一个队列
* @param menu
*/
public synchronized static void addMenu(Menu menu){
for (Command cmd:menu.getCommands()){
cmds.add(cmd);
}
}
/**
* 按顺序取出一个命令
* @return
*/
public synchronized static Command getOneCommand(){
Command cmd=null;
if (cmds.size()>0){
//按顺序取出第一个命令
cmd=cmds.get(0);
//取完就移除
cmds.remove(0);
}
return cmd;
}
}
5.命令类
/**
* @description:命令接口
*/
public interface Command {
/**
* 执行命令
*/
void execute();
/**
* 设置命令接收者
* @param cookApi
*/
void setCookApi(CookApi cookApi);
}
/**
* @description:生鱼刺身
*/
public class FishCommand implements Command{
private CookApi cookApi=null;
/**
* 桌号
*/
private int tableId;
@Override
public void setCookApi(CookApi cookApi) {
this.cookApi = cookApi;
}
public FishCommand(int tableId) {
this.tableId = tableId;
}
@Override
public void execute() {
cookApi.cook(tableId,"生鱼刺身");
}
}
/**
* @description:北京烤鸭
*/
public class MeatCommand implements Command{
private CookApi cookApi=null;
/**
* 桌号
*/
private int tableId;
@Override
public void setCookApi(CookApi cookApi) {
this.cookApi = cookApi;
}
public MeatCommand(int tableId) {
this.tableId = tableId;
}
@Override
public void execute() {
cookApi.cook(tableId,"北京烤鸭");
}
}
6.厨师类,暂时只用了一个实现类,可以不使用接口
/**
* @description:厨师接口
*/
public interface CookApi {
/**
* 做菜
* @param tableId 桌号
* @param name
*/
void cook(int tableId,String name);
}
/**
* @description:热菜
*/
@AllArgsConstructor
public class HotCook implements CookApi,Runnable{
private String name;
@Override
public void cook(int tableId, String name) {
//做菜时间
int cookTime=(int)(20*Math.random());
System.out.println(this.name+"厨师正在做"+tableId+"号桌的热菜:"+name);
try {
Thread.sleep(cookTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name+"厨师做好了"+tableId+"号桌的热菜:"+name+",共耗时"+cookTime);
}
@Override
public void run() {
while (true){
//从后厨取一个做菜命令
Command oneCommand = CommandQueue.getOneCommand();
if (oneCommand!=null){
//设置自己为做菜厨师
oneCommand.setCookApi(this);
//执行命令
oneCommand.execute();
}
//做完菜休息1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
命令模式实现日志持久化
1.FishCommand和MeatCommand实现序列化接口
public class MeatCommand implements Command, Serializable {}
public class FishCommand implements Command, Serializable {}
2.序列化和反序列化工具类
/**
* @description:文件操作类
*/
public class FileUtil {
/**
* 读文件,反序列化
* @param pathName
* @return
*/
public static List readFile(String pathName){
List list=new ArrayList();
ObjectInputStream oin=null;
File file = new File(pathName);
if (file.exists()){
try {
oin=new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)));
//反序列化
list= (List) oin.readObject();
} catch (Exception e) {
e.printStackTrace();
}finally {
if (oin!=null){
try {
oin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return list;
}
/**
* 写入文件,序列化
* @param pathName
* @param list
*/
public static void writeFile(String pathName,List list){
File file = new File(pathName);
ObjectOutputStream oos=null;
try {
oos=new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
//序列化
oos.writeObject(list);
} catch (Exception e) {
e.printStackTrace();
}finally {
if (oos!=null){
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3.后厨管理类
/**
* @description:菜单队列
*/
public class CommandQueue {
private final static String FILE_NAME="C:\\Users\\Lenovo\\Desktop\\commandqueue.txt";
private static List<Command> cmds=null;
/**
* 静态代码块,每次重启先加载
*/
static {
cmds= FileUtil.readFile(FILE_NAME);
if (cmds==null){
cmds=new ArrayList<>();
}
}
/**
* 将菜单中做菜的命令取出来,加入一个队列
* @param menu
*/
public synchronized static void addMenu(Menu menu){
for (Command cmd:menu.getCommands()){
cmds.add(cmd);
}
//覆盖写入文件
FileUtil.writeFile(FILE_NAME,cmds);
}
/**
* 按顺序取出一个命令
* @return
*/
public synchronized static Command getOneCommand(){
Command cmd=null;
if (cmds.size()>0){
//按顺序取出第一个命令
cmd=cmds.get(0);
//取完就移除
cmds.remove(0);
//覆盖写入文件
FileUtil.writeFile(FILE_NAME,cmds);
}
return cmd;
}
}
执行
在第八桌的时候终止程序,然后再重启看效果,会从上次中断的地方继续做菜,然后再开始新得做菜