1、意图
命令模式是一种行为设计模式,它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作。
简单来说就是原本 A 调用 B 的方法,命令模式引入了一个 C,这时候再调用的时候,就要先 A 调用 C,C 在根据实际情况决定调用 B 的哪一个或者哪几个方法。
2、问题
假如你正在开发一款新的文字编辑器,当前的任务是创建一个包含多个按钮的工具栏,并让每个按钮对应编辑器的不同操作。你创建一个非常简洁的按钮类,他不仅可用于生成工具栏上的按钮,还可用于生成各种对话框的通用按钮。
尽管所有按钮看上去都很相似,但它们可以完成不同的操作(打开、保存、打印和应用等)。最简单的解决方案是在使用按钮的每个地方都创建大量的子类。这些子类中包含按钮点击后必须执行的代码。
但这种方法有严重缺陷。首先,你创建了大量的子类,当每次修改基类按钮时,你都有可能需要修改所有子类的代码。简单来说,GUI 代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码。
还有一个部分最难办。复制/粘贴文字等操作可能会在多个地方被调用。例如用户可以点击工具栏上小小的“复制”按钮,或者通过上下文菜单复制一些内容,又或者直接使用键盘上的 Ctrl + C。
3、解决方案
命令模式建议 GUI 对象不直接提交这些请求。你应该将请求的所有细节抽取出来组成命令类,该类汇总仅包含一个用于触发请求的方法。
命令对象负责连接不同的 GUI 和业务逻辑对象。GUI 对象无需了解业务逻辑对象是否获得了请求,也无需了解其对请求进行处理的方式。GUI 对象触发命令即可,命令对象会自行处理所有细节工作。
下一步是让所有命令实现先相同的接口。该接口通常只有一个没有任何参数的执行方法,让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令。此外还有额外的好处,现在你能在运行时切换连接至发送者的命令对象,以此改变发送者的行为。
你可能会注意到遗漏的一块拼图——请求的参数。GUI 对象可以给业务层对象提供一些参数。但执行命令方法没有任何参数,所以我们如何将请求的详情发送给接收者呢?答案是:使用数据对命令进行预先配置,或者让其能够自行获取数据。
4、模型结构
![602df5252dde671cbb437cab58e99c45.png](https://i-blog.csdnimg.cn/blog_migrate/43e6e0b844e8b5d8d2fa4db3a4e41f7c.jpeg)
5、代码实现(Kotlin)
abstract class Command(val editor: Editor) {
private var backup: String? = null
fun backup() {
this.backup = this.editor.textField.text;
}
fun undo() {
editor.textField.text = backup
}
abstract fun execute(): Boolean
}
class CopyCommand(editor: Editor) : Command(editor) {
override fun execute(): Boolean {
super.editor == this.editor
editor.clipboard = editor.textField.selectedText
return false
}
}
class PasteCommand(editor: Editor) : Command(editor) {
override fun execute(): Boolean {
if (editor.clipboard?.isEmpty() == true) {
return false
}
backup()
editor.textField.insert(editor.clipboard, editor.textField.caretPosition)
return true
}
}
class CutCommand(editor: Editor) : Command(editor) {
override fun execute(): Boolean {
if (editor.textField.selectedText.isEmpty()) {
return false
}
backup()
val source = editor.textField.text
editor.clipboard = editor.textField.selectedText
editor.textField.text = cutString(source)
return true
}
private fun cutString(source: String): String {
val start = source.substring(0, editor.textField.selectionStart)
val end = source.substring(editor.textField.selectionEnd)
return start + end
}
}
import java.util.*
class CommandHistory {
private val history = Stack<Command>()
fun push(c: Command) {
this.history.add(c)
}
fun pop(): Command {
return this.history.pop()
}
fun isEmpty(): Boolean {
return this.history.isEmpty()
}
}
import java.awt.FlowLayout
import javax.swing.*
class Editor {
lateinit var textField: JTextArea
var clipboard: String? = null
private val history = CommandHistory()
fun init() {
val frame = JFrame("Text editor (type & use buttons, Luke!")
val content = JPanel()
frame.contentPane = content
frame.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
content.layout = BoxLayout(content, BoxLayout.Y_AXIS)
this.textField = JTextArea()
this.textField.lineWrap = true
content.add(textField)
val editor = this
val buttons = JPanel(FlowLayout(FlowLayout.CENTER))
val ctrlC = JButton("Ctrl + C").also {
it.addActionListener {
executeCommand(CopyCommand(editor))
}
}
val ctrlX = JButton("Ctrl + X").also {
it.addActionListener {
executeCommand(CutCommand(editor))
}
}
val ctrlV = JButton("Ctrl + V").also {
it.addActionListener {
executeCommand(PasteCommand(editor))
}
}
val ctrlZ = JButton("Ctrl + Z").also {
it.addActionListener {
undo()
}
}
buttons.add(ctrlC)
buttons.add(ctrlX)
buttons.add(ctrlV)
buttons.add(ctrlZ)
content.add(buttons)
frame.setSize(450, 200)
frame.setLocationRelativeTo(null)
frame.isVisible = true
}
private fun executeCommand(command: Command) {
if (command.execute()) {
history.push(command)
}
}
private fun undo() {
if (history.isEmpty()) return
val command = history.pop()
command.undo()
}
}
fun main() {
Editor().init()
}
输出结果:
![fbffcd3ffa4f4c5cb6ab6457e148542e.png](https://i-blog.csdnimg.cn/blog_migrate/375794a1d1315d707874f9f1ef0c3e5e.png)
6、参考
refactoringguru