命令模式
1.命令模式的定义
- 将一个请求封装成一个对象,从而让用户使用不同的请求把客户参数化;
- 对请求排队或者记录请求日志,以及支持可撤销的操作
1.1 使用场景
作为一个开发人员,掌握Git是一个必须的事情。一般来说,我们都是通过敲命令去实现我们的目的。
比如,我们敲如下一行代码:
git commit -m "init"
看这个没什么感觉,我们还是用常规代码去实现这一过程吧。
class LocalReceiver{
fun init() {
println("init:将这个目录变成git可以管理的仓库")
}
fun add(){
println("add:将文件添加到仓库")
}
fun commit(){
println("commit:将文件提交到仓库")
}
fun reset() {
println("reset:版本回退操作")
}
}
class ClientActivity:Activity() {
fun initView()
btCommit.setOnClickListener {
val receiver = LocalReceiver()
receiver.commit()
}
}
}
- 说实话,如果真的就像Button中写的那么简单就能实现命令,那真的没什么必要去使用什么命令模式!
- 我们使用Git的时候,需要满足一定条件下才可以实现我们的命令。
- 你可能会说,将条件封装到receiver当中不就好了吗?但是有时候receiver做的操作是比较细的,是高内聚的,纯粹一点的实现,我们就不要参杂那么多东西进去了
实际上,代码是这样:
class ClientActivity:Activity(){
/**
* -1 未初始化状态
* 0 初始化状态
* 1 添加状态
* 2 干净状态
*/
val status = -1
fun initView() {
btCommit.setOnClickListener {
if (status == 1) {
val receiver = LocalReceiver()
print("提交前的操作")
receiver.commit()
print("提交后的操作")
} else {
print("请先进行添加操作")
}
}
}
}
- 有没有发现,我们将 逻辑(状态和操作),命令两者都放在了客户端去实现。
甚至有时候,我们将几种命令组合成一种命令,称为组合命令。
class ClientActivity:Activity() {
fun initView()
btIAC.setOnClickListener {
val receiver = LocalReceiver()
receiver.init()
receiver.add()
receiver.commit()
}
}
}
- 组合逻辑留在了客户端。这是不合理的,因为维护的时候,可能会存在不小心删除部分代码等因素。
- 迪米特原则。客户端应该只知道我可以调用一个组合命令,而不应该知道这些细节。
- 复用性低。有可能在其他地方也可能使用到该组合命令,移植性差。
- 参杂逻辑时,会使得这部分代码变得更为复杂和难以维护
这时候命令模式就发挥了它的作用。
1.2 角色介绍
- Receiver:命令的执行者,是真正实现命令操作的。
- Command:定义了具体命令类的抽象接口。
- ConcreteCommand:具体命令类,实现了Command抽象接口。
- Invoker:调用者 控制和负责具体命令类的调用
- Client:客户端
Client,你可以理解为一种使用场景,拿我们上面的代码来说,就相当于一个按钮的回调方法。
Invoker,在客户端使用Command参数化Invoker,拿我们上面的代码来说,调用invoker.setCommand(command),然后去执行对应的方法。它作为客户端对命令的调用者,控制command的调用,内部处理了一些逻辑操作。
Command:规范具体命令类需要实现的方法,比如统一实现execute()方法。
ConcreteCommand:一般会实现setReceiver()方法,实现execute()方法,该方法中可能是一个命令或是多个命令的组合,也可能会包括一些其他操作,我们也可以扩展一些其他方法,比如撤销命令方法。
1.3 命令模式的实现
让我们来看看比较完整的实现方案吧
/**
* 本地Git的实现操作
*/
class LocalReceiver{
fun init() {
println("init:将这个目录变成git可以管理的仓库")
}
fun add(){
println("add:将文件添加到仓库")
}
fun commit(){
println("commit:将文件提交到仓库")
}
fun reset() {
println("reset:版本回退操作")
}
}
/**
* 提交接口
*/
interface PushReceiver{
fun push()
}
/**
* 提交到Github
*/
class PushToGitHubReceiver:PushReceiver{
override fun push(){
println("PushToGitHubReceiver:从配置文件中拿到对应的GitHub仓库地址")
println("PushToGitHubReceiver:绑定GitHub远程仓库")
println("PushToGitHubReceiver:提交到GitHub远程仓库")
}
}
/**
* 提交到码云
*/
class PushToMaYunReceiver:PushReceiver{
override fun push(){
println("PushToMaYunReceiver:从配置文件中拿到对应的码云仓库地址")
println("PushToMaYunReceiver:绑定码云远程仓库")
println("PushToMaYunReceiver:提交到码云远程仓库")
}
}
有没有发现,我这里的LocalReceiver和PushReceiver并没有抽象出一个统一的接口出来,这是为什么?
LocalReceiver类实现了四个方法·,那我可不可以分成四个类,然后跟PushReceiver类统一使用doSomething方法呢?
- 没有必要,在功能不是那么复杂的情况下,分的如此之细,这属于优化过度,不是好的设计。而且每个功能都有自己的含义,我们一般通过方法名去查看,如果以doSomething为方法名,简直就是灾难。
- 抽象一个统一的接口,需要它们有共同的特性。抽象出了PushReceiver接口,是因为它们都实现push的功能。
Command:
interface Command{
fun execute()
}
/**
* 初始化命令
*/
class InitCommnad : Command{
var receiver:LocalReceiver?=null
override fun execute() {
receiver?.init()
}
}
/**
* 添加命令
*/
class AddCommand:Command{
var receiver:LocalReceiver?=null
override fun execute() {
receiver?.add()
}
}
/**
* 提交命令
*/
class CommitCommand:Command{
var receiver:LocalReceiver?=null
override fun execute() {
receiver?.commit()
}
}
/**
* 回退版本命令
*/
class ResetCommand :Command {
var receiver: LocalReceiver? = null
override fun execute() {
receiver?.reset()
}
}
/**
* push到远程仓库
*/
class PushCommand:Command{
var receiver:PushReceiver?=null
override fun execute() {
receiver?.push()
}
}
/**
* 提交到配置文件中所有的远程仓库
*/
class PushToAllCommand:Command{
var receiverList=ArrayList<PushReceiver>();
fun initReceiverList() {
if (receiverList.size == 0) {
receiverList.add(PushToGitHubReceiver())
receiverList.add(PushToMaYunReceiver())
}
}
override fun execute() {
initReceiverList()
receiverList.forEach {
it.push()
}
}
}
Invoker:Git是一个饿汉式单例。
object Git {
/**
* 本来想着使用状态模式的,但为了方便,就简单使用int判断
* 使用该状态,仅仅是为了体现调用者的控制作用
* -1 未初始化状态
* 0 初始化状态
* 1 添加状态
* 2 干净状态(未同步状态)
* 3 同步状态 由于存在多个仓库,避免复杂,不讲究这个
*/
var status = -1
var mResetVersion: Int? = null
var mCommitLog: String? = null
var command: Command? = null
fun init() {
if (status < 0) {
command?.execute()
status = 0
} else {
println("该目录已经是git可以管理的仓库")
}
}
fun add() {
if (status >= 0) {
command?.execute()
status = 1
} else {
println("该目录还不是git可以管理的仓库,需要init操作")
}
}
fun commit(commitLog: String) {
if (status == 1) {
command?.run {
//这里仅仅为了告诉你一种当receiver对象方法参数的获取方法
//具体的使用看你自己
mCommitLog = commitLog
execute()
status = 2
}
} else {
println("请添加已修改部分")
}
}
fun push() {
if (status == 2) {
command?.execute()
} else {
println("该仓库存在问题")
}
}
fun pushToAll() {
if (status == 2) {
command?.execute()
} else {
println("该仓库存在问题")
}
}
fun reset(version: Int) {
if (status > 0) {
command?.run {
mResetVersion = version
execute()
status = 2
}
} else {
println("该目录还不是git可以管理的仓库,需要init操作")
}
}
}
Client
class ClientActivity : Activity() {
fun initView() {
var localReceiver = LocalReceiver()
btInit.setOnClickListener {
val command = InitCommnad()
command.receiver = localReceiver
Git.command = command
Git.init()
}
btAdd.setOnClickListener {
val command = AddCommand()
command.receiver = localReceiver
Git.command = command
Git.add()
}
btCommit.setOnClickListener {
val command = CommitCommand()
command.receiver = localReceiver
Git.command = command
Git.commit("first commit")
}
btReset.setOnClickListener {
val command = ResetCommand()
command.receiver = localReceiver
Git.command = command
Git.reset(1)
}
btPushToGitHub.setOnClickListener {
val command = PushCommand()
command.receiver = PushToGitHubReceiver()
Git.command = command
Git.push()
}
btPushToMaYun.setOnClickListener {
val command = PushCommand()
command.receiver = PushToMaYunReceiver()
Git.command = command
Git.push()
}
btPushToAll.setOnClickListener {
Git.command = PushToAllCommand()
Git.pushToAll()
}
}
}
- 客户端,关于命令的所有逻辑操作都消失了,看起来就跟如下代码一样
class ClientActivity:Activity() {
fun initView()
btIAC.setOnClickListener {
val receiver = LocalReceiver()
receiver.commit()
}
}
}
你是否注意到receiver出现在了客户端上?
- 客户端其实是不应该出现receiver的。
你可以选择封装到invoker中,没有问题,也应该这样做。
- 当存在很多command, 你不要为每个command中创建一个receiver,因为可能会堆积很多receiver的情况,能共用同一个的就尽量共用。
1.4 适用场景
-
命令的发送者和命令执行者有不同的生命周期。命令发送了并不是立即执行。
-
命令需要进行各种管理逻辑。
-
需要支持撤消\重做操作
总结
判断标准
- 命令模式的核心:命令。抓住这个关键字,它是否是一个命令?
- 客户端去做Receiver类的方法外的一些逻辑处理(控制管理,组合命令)等。
这时候我们可以考虑使用命令模式。否则,客户端只是需要单纯的去调用receiver类,就没有这个必要去使用命令模式。
优点
- 一定程度上,提高了代码的复用性。例如封装好的组合命令,可能在其他界面也需要该命令功能。
- 扩展性好。 Command为抽象接口,非常容易扩展具体的命令类。
- 将调用操作的请求对象与接收对象解耦。
缺点
- 命令过多,将会产生大量的Command具体实现类
- Command的抽象方法execute() 不够灵活。 也许我需要传参过去,但是受于限制,只能通过其他途径去完成传参这一行为。