命令式接口

2003 年 11 月 23 日

与模块交互最常见的风格是使用过程或对象方法。因此,如果您希望模块计算合同的一系列费用,您可能会有一个 BillingService 类,其中包含一个用于执行计算的方法,并像这样调用它

aBillingService.calculateCharges(aContract)

命令式接口将为每个操作创建一个命令类,并像这样调用它

CalculateChargeCommand.new(aContract).run()

本质上,您为方法式接口中的每个方法创建一个命令类。

一个常见的变体是拥有一个单独的命令执行器对象,它实际上执行命令的运行。

aCommandExecutor.run(CalculateChargeCommand.new(aContract))

如果您使用过 Struts 等框架,您会发现操作类遵循这种操作风格。

那么为什么人们喜欢和不喜欢这种方法呢?首先,可以公平地说,命令式接口比方法式接口更复杂。您需要实例化命令并将其传递以执行。这比仅仅调用一个方法更复杂,这就是为什么即使是这种方法的粉丝也只将它们用于重要的接口(服务层、服务器端逻辑、主要子系统的接口)。

命令式接口有很多好处。其中一个主要的好处是能够通过装饰命令执行器轻松地将通用行为添加到命令中。这对于处理事务、日志记录等非常方便。命令可以排队以供以后执行,并且(如果命令及其数据是可序列化的)可以跨网络传递。命令结果可以通过将结果保存到从命令名称和参数合成的键来缓存。

我看到人们对命令的抱怨,最大的问题是命令中存在大量重复的逻辑。当命令是包含大量逻辑的事务脚本时,这种情况往往会发生。这并不一定与使用命令而不是方法有关;这个问题是事务脚本的普遍问题。可能是命令式结构往往会夸大这一点,仅仅是因为许多人认为一个类需要一两页代码才能有价值,因此最终在命令中放入了比应该更多的代码。

您会注意到我在本页中使用了“接口”一词。这反映了使用命令的选择主要与客户端的接口有关,而不是与命令逻辑的实际实现有关。拥有运行方法只是调用另一个方法的单行代码的命令类是完全合理的。这样做可以为您提供命令的所有优势,但允许您将逻辑保留在命令类本身之外。这样的命令类只有很少的代码行。

关于命令的一个常见问题是返回什么。通用运行方法需要一个通用的返回类型,例如 Object 或 CommandResult,但您需要一个更具体的类型来获取运行命令的结果。一种方法是为每个命令类定义一个结果类并使用命名约定,以便 CalculateChargeCommand 将具有 CalculateChargeResult 的返回类型。另一种方法是让命令将结果存储在同一个对象中。在这种情况下,您首先运行命令,然后查询命令对象以获取结果。