命令模式的实际运用

28 Aug 2018

前言

本文将记录实际工作中的一个实际场景,来描述为什么以及如何运用命令模式。

命令模式简单介绍

命令模式是一种数据驱动模式,将请求以命令的形式包裹在对象中,并传给调用对象,调用对象会寻找到适合处理该命令的对象,然后把该命令传给相应对象进行处理。

场景描述

实际工作中需要开发一种新的题型,在练习的过程中,先是练习一整个句子,将此次整句打分的结果传递给新的算法服务,该服务会根据此次打分结果返回是否需要进一步的分步练习。所谓分步练习,即将长句拆分为短句,再练习短句,短句练习通过,再重新组合长句练习。整理整个需求之后,我把整个过程分为以下几个较为独立的步骤:

接下来借用图示辅助说明部分步骤。

DOWN

SRChunk 阶段

SRChunk Practice

这是整个流程的第一步,即将整个长句划分成若干个子句。图示中表示当前练习的句子被算法分割成了三个 chunk ,当前第二个 chunk 需要进一步练习。

NEXT

sub chunk 阶段

进行 chunk 练习的时候,如果这一小部分的练习经过算法打分发现还是有部分需要加强练习,那么一个 chunk 会进一步被划分,此时划分的最小单元成为 sub chunk 。类似 DOWN 过程,只不过操作的范围更窄,操作结果具有更小的粒度。

combine 阶段

Chunk Combine 阶段

此时会让当前 chunk 与它前面相邻的 chunk 一起再进行练习。

功能设计

其实在前面描述需求的过程,已经将主要的步骤整理出来了。这所有的 6 个步骤,除了 DOWN 和 END 是开始和结束的标志,其他的步骤都有很多组合的情况,但是这些组合逻辑是算法处理的部分,即下一步做什么动作完全由算法控制,客户端通过数据来响应这种控制。换句话说,这完全是数据驱动的过程,每种步骤都有明确的逻辑需要处理。所以这个场景很适合用命令模式来做,上述 6 个步骤对应就是 6 个命令,每个命令都会有相应的实现去进行相应。而且,日后若有新的步骤,只需要添加一条新的命令和一个新的对象去相应命令即可。

命令抽象

命令的调用者需要操作一个抽象的对象以执行某个具体的命令,该抽象定义为 BaseCommand

abstract class BaseCommand() : Runnable {

    private val subscriptions = arrayListOf<Subscription>()

    open fun onEnter() {
        LMLog.i(this, "enter action: $state")
    }

    protected fun addSubscription(subscription: Subscription) {
        subscriptions.add(subscription)
    }

    open fun onExit() {
        for (subscription in subscriptions) {
            subscription.unsubscribe()
        }
        subscriptions.clear()
    }
}

这里实际没有使用接口而是抽象类,因为在不同的命令中,有些操作比如播放音频、动画等是可以复用的,这些复用的操作可以直接写在父类中。当然也可以使用接口,然后复用的操作按照组合的方式来。BaseCommand 实际也是一个 Runnable ,是为了方便控制方进行操作。添加的 onEnteronExit 方法是为了便于各个命令提前进行初始化及完成命令时的资源释放或者状态恢复。

执行命令

private fun executeNewCommand(chunkAction: ChunkingAction) {
    command?.onExit()
    command = when (chunkAction.srResponse.state) {
        ChunkingAction.State.DOWN.value -> DownCommand(chunkAction.srResponse, this, ChunkingAction.State.DOWN)
        ChunkingAction.State.NEXT.value -> NextCommand(chunkAction.srResponse, this, ChunkingAction.State.NEXT)
        ChunkingAction.State.REPEATED.value -> RepeatedCommand(
            chunkAction.srResponse,
            this,
            ChunkingAction.State.REPEATED
        )
        ChunkingAction.State.COMBINED.value -> CombineCommand(
            chunkAction.srResponse,
            this,
            ChunkingAction.State.COMBINED
        )
        ChunkingAction.State.UP.value -> UpCommand(chunkAction.srResponse, this, ChunkingAction.State.UP)
        ChunkingAction.State.END.value -> EndCommand(chunkAction.srResponse, this, ChunkingAction.State.END)
        else -> {
            throw RuntimeException("unknown state: ${chunkAction.srResponse.state}")
        }
    }
    command?.onEnter()
    command?.run()
}

根据算法返回的指令,选择相应的命令进行响应即可。在具体执行新的命令前,需要先退出之前的命令,让其释放资源,然后进入新的命令并执行。

总结

利用命令模式,能够很好地描述整个行为,并且通过命令的方式将复杂逻辑进行了分离,符合职责单一原则,便于日后的维护和扩展。