命令模式 一个类依赖其它的类进行某种操作时,一般是直接调用其的接口的特定方法。该类称为请求的发送者,依赖的类称为请求的接受者。命令模式在请求发送者和请求接受者之间增加一层抽象层,使请求的接受者能够对请求的发送者不可见,降低耦合性。借由策略模式,我们能够忽略请求接受者的具体的实现类,只关心相应接口;借由命令模式,我们能够忽略请求接受者的接口,只关心具体的命令或命令的接口。如此,请求发送者和请求接受者就完全解耦了。命令模式将请求本身封装成对象(数据 <-> 过程),从而使能够将请求当作对象来处理,完成回滚,日志,排队,异步化,可重做等操作。命令模式也称作事务模式,这是说可以将一系列请求作为一个命令传递,很有趣。
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
我能够想到两个实例——线程池和 js 的事件,它们都不关心请求的接受者究竟姓甚名谁。对前者,命令是Ruunable
,Callable
,对后者,命令是回调函数,对 ui 进行操作时不是直接将该操作传递给什么玩意(这是用户决定的),而是触发一个事件,通过事件监听器间接进行一定的行为。
角色 命令模式包含四个角色——
请求发送者 ——即客户端。
命令 ——客户端发送的请求,为客户端所依赖。命令分为抽象命令和具体命令。
命令执行者(Invoker) ,执行命令的对象。命令执行者只负责处理命令,对具体的请求接受者不关心。命令执行者也可以负责记录命令的历史,重做,回滚,发现请求接受者等功能。
请求接受者 ——在命令中被依赖的对象。
比如,对一个使用了线程池的客户端,客户端本身为请求发送者,命令为客户端提供给命令执行者的 Runnable 或 Callable,命令执行者为线程池,请求接受者为 Task 依赖的对象,如果没有依赖的对象则没有请求接受者。这样,命令本身就将执行请求发送者所需的所有业务逻辑。
对浏览器上的 js,请求发送者为用户的操作,命令为回调函数,命令执行者为 js 的事件驱动机制,请求接受者为回调函数捕获的对象,以及用户操作的 ui。
在 IoC 容器中,借助 DI,客户端即业务代码中只需要关心命令和命令处理者,不需也不应该关心请求接受者究竟是什么东西,而只有命令知晓请求接受者姓甚名谁(不太确定,感觉命令处理者知晓请求接受者在某些时候也是需要的)。
需注意的是,命令模式强调将命令交由命令执行者这样一个特定的地方执行,从而使能够对命令进行统一的操作,如 try-catch,日志,重做等。
栗子 命令模式(的变种)的应用感觉到处都是,实在是找不出一个贴近实际同时又具有典型性的范例,干脆直接放弃思考,在书上(《Scala 和 Clojure 函数式编程模式》)的范例基础上改进一下下。考虑一个收银机,对其的操作包括增加现金,减少现金,显示现金,以及特定命令的集合(时髦的话说,事务),并且在命令发生错误的时候提供回滚操作 。
我们首先定义 Command 接口——
interface Command { void execute () throws Exception; void restore () ; }
我们的请求接受者,即收银机的定义如下,这里为方便,提供简单的复制和回滚操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class CashRegister { private int cash = 0 ; public void addCash (int amount) { this .cash += amount; } public void takeCash (int amount) throws Exception { if (amount > cash) throw new Exception (String.format("请求取出金额 %d 大于当前金额 %d" , amount, cash)); this .cash -= amount; } public int getCash () { return cash; } public CashRegister copy () { CashRegister copy = new CashRegister (); copy.cash = this .cash; return copy; } public void restore (CashRegister copy) { this .cash = copy.cash; } }
然后,我们着手实现四种具体 Command,需要记住——Command 有能力独自执行,这是说它自己就持有自己的所有依赖。
首先是添加现金的命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class AddCashCommand implements Command { private final CashRegister cashRegister; private final int amount; public AddCashCommand (CashRegister cashRegister, int amount) { this .cashRegister = cashRegister; this .amount = amount; } private CashRegister copy; @Override public void execute () throws Exception { copy = cashRegister.copy(); cashRegister.addCash(amount); } @Override public void restore () { cashRegister.restore(copy); } }
然后是取走现金的命令,这里实际上已经能看出某种模式了—— 使用抽象类或者闭包来引用 cashRegister 和 amount 可以减少这里的重复代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class TakeCashCommand implements Command { private final CashRegister cashRegister; private final int amount; public TakeCashCommand (CashRegister cashRegister, int amount) { this .cashRegister = cashRegister; this .amount = amount; } private CashRegister copy; @Override public void execute () throws Exception { copy = cashRegister.copy(); cashRegister.takeCash(amount); } @Override public void restore () { cashRegister.restore(copy); } }
显示现金的命令非常容易。
public class DisplayCashCommand implements Command { private final CashRegister cashRegister; public DisplayCashCommand (CashRegister cashRegister) { this .cashRegister = cashRegister; } @Override public void execute () throws Exception { System.out.println("当前现金:" + cashRegister.getCash()); } @Override public void restore () {} }
最麻烦的是命令集(我们称它为事务),回滚操作需要被小心处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class CashTransactionCommand implements Command { private final List<Command> cashCommands; public CashTransactionCommand (List<Command> cashCommands) { this .cashCommands = cashCommands; } private int lastIndex = 0 ; @Override public void execute () throws Exception { for (Command command : cashCommands) { command.execute(); lastIndex++; } } @Override public void restore () { for (int i = lastIndex; i >= 0 ; i--) { cashCommands.get(i).restore(); } } }
然后是命令执行者,它的代码比较简单——
public class CommandInvoker { void execute (Command command) { try { command.execute(); } catch (Exception e) { e.printStackTrace(); command.restore(); } } }
完成了!我们整个 demo 试试,这里为了简单没有使用依赖注入。工程实践时肯定不是这样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class CashClient { public static void main (String[] args) { CashRegister cashRegister = new CashRegister (); CommandInvoker invoker = new CommandInvoker (); invoker.execute(new AddCashCommand (cashRegister, 100 )); invoker.execute(new DisplayCashCommand (cashRegister)); invoker.execute(new TakeCashCommand (cashRegister, 200 )); invoker.execute(new DisplayCashCommand (cashRegister)); invoker.execute(new CashTransactionCommand (Arrays.asList( new AddCashCommand (cashRegister, 200 ), new TakeCashCommand (cashRegister, 300 ), new DisplayCashCommand (cashRegister) ))); invoker.execute(new DisplayCashCommand (cashRegister)); CashCommandBuilder cashCommandBuilder = ContextUtil.getBean(CashCommandBuilder.class); invoker.execute(cashCommandBuilder.add(200 ).take(300 ).display().build()); CashCommandFactory cashCommandFactory = ContextUtil.getBean(CashCommandFactory.class); invoker.execute(cashCommandFactory.addCommand(100 )); } }
当然,我们也可以让命令调用者来“发现”请求的接收者,在执行命令时对命令注入其依赖,为此我们需要修改命令的接口——
interface Command { void execute (CashRegister cashRegister) throws Exception; void restore (CashRegister cashRegister) ; }CommandInvoker invoker = new CommandInvoker (new CashRegister ()); invoker.execute(new AddCashCommand (300 ));
这样就能够离开 DI 框架也能使用了。
summary 关于命令模式,我们只需要关心它的核心思想——将请求当作对象来操作 ,这完全是在说一等函数!我们可以用Unit -> Unit
或Unit -> a
(需要命令有返回值的情况,Invoker 可以进行进一步的操作,如将其转为异步)之类的来充当 Command 接口。结合柯里化函数,建造者模式等来构造 Command 我认为这对实践是会有帮助的。至于回滚之类的操作…那可太复杂了,“想想数据库的事务日志吧!”
状态模式
不换思想就换人。
状态模式也是个很简单的模式。通常,我们通过更改对象的状态(配置)来变更它的行为,比如对一个密码加密工具(假设它有状态),我们可能会通过字符串或枚举来标识其使用的加密算法,比如 "md5"
,EncryptType.SHA256
等。可以想像,在加密的业务逻辑中,它肯定使用条件语句来选择具体使用的算法,比如可能是这样——
String encrypt (String str) { switch (encryptType) { case MD5: return ; case SHA256: return ; case : return ; } }
这个违反了开闭原则——增加新的算法就需要修改源码了。一个可能的解决方案是使用策略模式和工厂方法模式(或简单工厂模式)来解决这一问题——建立枚举到策略类的映射,通过工厂进行这个映射。
再考虑另一个问题——编写一个电梯的逻辑,现在假设电梯有四个操作——开门,关门,上行,下行,有四个状态——开门,停止,上行,下行。每个操作在每个状态下都会有不同的行为,同时有可能改变自己的状态。
比如,对上行操作,我们可能会这么写——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Lift { private LiftState state; void goUp () { switch (state) { case OPEN: throw new IllegalArgumentException ("开门状态不能移动!" ); case UP: return ; case DOWN: hrow new IllegalArgumentException ("上行状态不能下行!" ); case STOP: System.out.println("停止上行" ); state = LiftState.STOP; } } void open () { switch (state) { case OPEN: return ; case UP: case DOWN: hrow new IllegalArgumentException ("移动状态不能开门!" ); case STOP: System.out.println("关门" ); state = LiftState.STOP; } } }
可以注意到,每个操作的形式都符合一种“模式”——整个方法体里只有一个 switch,对每个状态都有特定操作。
示例 这种情况该如何进行抽象?状态模式给予了我们答案——将状态内化到类(的逻辑)本身,通过切换类的实现来修改行为。这是说,我们可以为每一个特定的状态都定义一个类,来表达这个状态的情况下类应该有的行为,比如我们对开门状态下的实现可能会有这样的逻辑——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 interface LiftState { void goUp () ; void goDown () ; void stop () ; void open () ; }class OpenState implements LiftState { public static final LiftState INSTANCE = new OpenState (); @Override public void goUp () { throw new IllegalArgumentException ("开门状态不能移动!" ); } @Override public void goDown () { throw new IllegalArgumentException ("开门状态不能移动!" ); } @Override public void stop () { System.out.println("关门" ); setThis(StopState.INSTANCE); } @Override public void open () { } }class StopState implements LiftState { public static final LiftState INSTANCE = new StopState (); @Override public void goUp () { System.out.println("上行" ); setThis(GoUpState.INSTANCE); } @Override public void goDown () { System.out.println("下行" ); setThis(GoDownState.INSTANCE); } @Override public void stop () { } @Override public void open () { System.out.println("开门" ); setThis(OpenState.INSTANCE); } }
我们找到解决方法(的一部分)了!只需要对每个状态都定义在这个状态下的各种操作以及状态变化,我们就能够将原本的 switch 逻辑切分到一个个状态类中了 。
可是该代码无法编译——这又不是 C++,类是无法改变自己的引用的,因此setThis(xxx)
是实现不了的!
解决方案是使用一个环境(Context,标准翻译是上下文)来包裹状态,在状态中委托上下文来修改状态。用户则仅使用该上下文。上下文可以和状态具有一样的接口,也可以不一样。
为了让状态中能够使用上下文,我们可以选择将上下文作为函数参数传入,或者作为状态所持有的字段,在构造时传入,我们选择前者,为此需要重写 LiftState 接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 interface Context { void setState (LiftState liftState) ; }interface LiftState { void goUp (Context context) ; void goDown (Context context) ; void stop (Context context) ; void open (Context context) ; }class LiftContext implements Context { private LiftState state = StopState.INSTANCE; @Override public void setState (LiftState liftState) { this .state = liftState; } public void goUp () { state.goUp(this ); } public void goDown () { state.goDown(this ); } public void stop () { state.stop(this ); } public void open () { state.open(this ); } }class OpenState implements LiftState { public static final LiftState INSTANCE = new OpenState (); @Override public void goUp (Context context) { throw new IllegalArgumentException ("开门状态不能移动!" ); } @Override public void goDown (Context context) { throw new IllegalArgumentException ("开门状态不能移动!" ); } @Override public void stop (Context context) { System.out.println("关门" ); context.setState(StopState.INSTANCE); } @Override public void open (Context context) { } }
各个状态和状态之间的转换关系也可以使用一张二维表来进行表示,最典型的例子莫过于有限状态机了。
状态模式亦可用于需要改变引用的情况,比如需要实现CopyOnWrite
的集合的时候。但这是否属于状态模式?我认为是。
class CopyOnWriteMap <K, V> implements Map <K, V> { private Map<K, V> realMap = new HashMap <>(); @Override public synchronized void put (K key, V value) { Map<K, V> copiedMap = new HashMap <>(realMap); copiedMap.put(key, value); realMap = copiedMap; } }
FP 的观点 无论是把上下文当作函数参数传入,还是通过构造函数传入上下文,对状态本身来说实际上增加了额外的复杂度——它需要知晓上下文的存在。我们可以接纳函数式编程中不可变的思想,返回新的状态来替代原本修改上下文中的状态,这样可以消灭 Context 接口,并且让上下文使用和状态一样的接口。
有趣的是,这样的状态模式其形式非常类似装饰器模式和代理模式(甚至可能只能从语义上来区分,在代码层面上无法区分),只不过其侧重点不一样罢了。或许这几种模式都是对于委托(delegate)模式的应用吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 interface LiftState { LiftState goUp () ; LiftState goDown () ; LiftState stop () ; LiftState open () ; }class LiftContext implements LiftState { private LiftState state; @Override public LiftState goUp () { state = state.goUp(); return this ; } @Override public LiftState goDown () { state = state.goDown(); return this ; } @Override public LiftState stop () { state = state.stop(); return this ; } @Override public LiftState open () { state = state.open(); return this ; } }class OpenState implements LiftState { public static final LiftState INSTANCE = new OpenState (); @Override public LiftState goUp () { throw new IllegalArgumentException ("开门状态不能移动!" ); } @Override public LiftState goDown () { throw new IllegalArgumentException ("开门状态不能移动!" ); } @Override public LiftState stop () { System.out.println("关门" ); return StopState.INSTANCE; } @Override public LiftState open () { return this ; } }
状态模式的内涵似乎不止这些,之后还需要继续研究。
访问者模式 对数据类型的扩展有两个维度——在已有的数据类型的实现上添加新的方法;给已有的数据类型添加新的实现 。在 OOP 语言中,实现后者轻而易举,而前者则需要使用静态工具类,而在静态工具类不满足需求的情况下(需要对特定实现添加特定方法,而非对接口添加方法),访问者模式则要走上舞台。
动机 考虑一个场景——我们试图在 Java 的 List 上加入一些新的操作,同时希望该操作对不同的 List 实现(ArrayList,LinkedList 等)有不同的效果,我们马上会想到的一定是创建一个静态工具类并提供相关操作——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public abstract class ListOps { private ListOps () { throw new RuntimeException ("you bad bad" ); } static void op (ArrayList<?> lst) { System.out.println("对 ArrayList 进行操作" ); } static void op (LinkedList<?> lst) { System.out.println("对 ArrayList 进行操作" ); } static void op (List<?> lst) { throw new RuntimeException ("只提供对 ArrayList 和 LinkedList 的操作!" ); } public static void main (String[] args) { List<Object> lst = new LinkedList <>(); ListOps.op(lst); } }
感觉这样就 OK 啦?IDEA 用它的高亮告诉我们,最后被执行的 op 方法是void op(List<?>)
而非我们想要的void op(LinkedList<?>)
!这种情况是我们(我?)对 Java 的重载/多态机制了解不够导致的,这个情况下 Java 选择调用的方法是依据对象的声明类型而非引用类型(静态分派) 。
最简单的方案是仅创建void op(List<?>)
方法,通过 instanceof 和 if-else 来“分发”方法调用——
static void op (List<?> lst) { if (lst instanceof ArrayList) { System.out.println("对 ArrayList 进行操作" ); } else if (lst instanceof LinkedList) { System.out.println("对 ArrayList 进行操作" ); } else { throw new RuntimeException ("只提供对 ArrayList 和 LinkedList 的操作!" ); } }
这既不优雅也不符合开闭原则。我们必须找到方法,让 Java 能够通过引用类型 来调用我们的函数(动态分派),而这时候访问者模式就派上用场了。
介绍 && 栗子 访问者模式简而言之,就是在对象上开一个洞,在洞里把自己暴露给一个称为访问者的对象 ,比如这样——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 interface SomeInterface { <T> T visit (Visitor<T> visitor) ; }class ConcreteClass1 implements SomeInterface { @Override public <T> T visit (Visitor<T> visitor) { return visitor.visit(this ); } }class ConcreteClass2 implements SomeInterface { @Override public <T> T visit (Visitor<T> visitor) { return visitor.visit(this ); } }
两个具体类的实现看上去是一样的,能不能把 visit 方法定为 default 的?不行,因为在每个成员的 visit 方法体中,this 的类型都是已知的,仍旧走的静态分派 ,因此倘若定义在接口中,则必定走的是下面 Visitor 的 default 的 visit 方法。只有用户从外部调用SomeInterface的visit方法的时候才是动态分派,即找到特定的具体类。
而访问者则需要知晓所有具体类 ——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 interface Visitor <T> { T visit (ConcreteClass1 concreteClass1) ; T visit (ConcreteClass2 concreteClass2) ; default T visit (SomeInterface someInterface) { throw new RuntimeException ("需添加访问者接口" +Visitor.class.getName()+"对类" + someInterface.getClass().getName() + "的 visit 方法" ); } }class ConcreteVisitor1 implements Visitor <Void> { @Override public Void visit (ConcreteClass1 concreteClass1) { System.out.println("Hello, Concrete Class 1" ); return null ; } @Override public Void visit (ConcreteClass2 concreteClass2) { System.out.println("Hello, Concrete Class 2" ); return null ; } }
客户端代码则是这样——
public class Hello { public static void main (String[] args) { Visitor<Void> visitor = new ConcreteVisitor1 (); SomeInterface obj1 = new ConcreteClass1 (); SomeInterface obj2 = new ConcreteClass2 (); SomeInterface obj3 = new ConcreteClass3 (); obj1.visit(visitor); obj2.visit(visitor); obj3.visit(visitor); } }
实践证明这个方法可行。为什么呢?可以发现,在进行obj1.visit(visitor)
这样一个方法调用时,Java 通过引用类型来找到obj1
的实际类型——即ConcreteClass1
——中的方法visit
并执行visitor.visit(this)
代码,而这里的this
的类型是可知的,因此调用的visitor
的visit
方法也是可知的,BINGO!
每一个访问者,实际上都是在不改变原有代码的基础上给已有的数据类型添加一个新的方法 。可惜,在 Java 中我们大概只能找到这种方法了。
适用场景 访问者模式的适用范围并不广(我认为一般来说静态工具类已经足以满足要求),在大多数时候我们都可以不去使用它,而且当我们试图访问的类的源代码并不能被我们修改时,应用访问者模式甚至是不可能的(或许通过适配器模式可以做到)。且访问者模式的使用有一个重要的条件——被访问者的类继承结构应当稳定,即用户很少增加被访问的类型的实现。倘若增加了,则所有的 Visitor 都需要新增对应该实现的访问方法,这是访问者对所有实现类都必须知晓的必然结果。
在 Java 中,访问者模式应用在了 Stream 的 collect 方法上,该方法允许用户使用各种方式去折叠流,其甚至模拟了一些类型类才有的操作,比如仅对Stream<String>
才可用的Collector.joining
。
另外,Scala,Kotlin 在对类的方法进行扩展上都有自己的一套方法论,其都不需要修改源代码,对它们的了解是必须的。
来自未来的吐槽 访问者模式中的 Visitor,如果在将其传递给对象时使用匿名实现类的方式,然后使用 lambda 去构造这个匿名实现类,会得到怎样的效果?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 import java.util.function.Function;interface MaybeVisitor <T, A> { default T visit (Maybe<A> maybe) { throw new IllegalStateException ("Impossible" ); } T visit (Just<A> just) ; T visit (Nothing<A> nothing) ; static <T, A> MaybeVisitor<T, A> match ( Function<Just<A>, T> onJust, Function<Nothing<A>, T> onNothing ) { return new MaybeVisitor <T, A>() { @Override public T visit (Just<A> just) { return onJust.apply(just); } @Override public T visit (Nothing<A> nothing) { return onNothing.apply(nothing); } }; } }interface Maybe <A> { <T> T visit (MaybeVisitor<T, A> visitor) ; static <B> Maybe<B> ofNullable (B a) { if (a != null ) return new Just <>(a); return new Nothing <>(); } }class Just <A> implements Maybe <A> { public A value; public Just (A value) { this .value = value; } @Override public <T> T visit (MaybeVisitor<T, A> visitor) { return visitor.visit(this ); } }class Nothing <A> implements Maybe <A> { @Override public <T> T visit (MaybeVisitor<T, A> visitor) { return visitor.visit(this ); } }public class Main { public static void main (String[] args) { Integer res = Maybe.<Integer>ofNullable(null ).visit(MaybeVisitor.match( just -> { System.out.println("not null!" ); return just.value; }, nothing -> 1000000 )); int r = Maybe.ofNullable(1 ).visit(MaybeVisitor.match( just -> { System.out.println("value: " + just.value.floatValue()); return null ; }, nothing -> null )); } }
模式匹配:天呐,这根本就是我!但这也启发了在 OOP 语言中去抽象模式匹配的方式(但我直接定义一个接受两个函数的方法,在Just和Nothing中调用特定一个不就好了吗(全恼)?
了解的越多,越觉得给这些模式下确切的定义是没有必要,和模式创建出来的原意反而背道而驰的了,毕竟其在使用中总是要经过一些修改,抽象和确切的定义就显得多余和无意义了,反而不如直接甩出一堆例子和解决方案,用大家都能听懂的话来描述。从实践出发才是真正的硬道理,而模式的作用反而更多的是提供一些名词来方便描述代码的组织。