用遙控器的例子逐步介紹命令模式
摘要
這一篇文章用一個非常非常詳細的用遙控器控制家電的例子來解釋命令模式,用命令模式和不用命令模式的情況都有了具體的代碼,方便大家做出比較,進而得到命令模式的優缺點。一開始我不會直接給出命令模式的定義,在例子中適合的時候我纔會給出定義,方便讀者循序漸進地理解命令模式的設計動機。
這篇文章寫了很久,各位看官走過路過不要錯過,都進來看一眼吧。
模式動機
在軟件設計中,我們經常需要向某些對象發送請求,但是並不知道請求的接收者是誰,也不知道被請求的操作是哪個,我們只需在程序運行時指定具體的請求接收者即可,此時,可以使用命令模式來進行設計,使得請求發送者與請求接收者消除彼此之間的耦合,讓對象之間的調用關係更加靈活。命令模式可以對發送者和接收者完全解耦,發送者與接收者之間沒有直接引用關係,發送請求的對象只需要知道如何發送請求,而不必知道如何完成請求。這就是命令模式的模式動機。
例子
我想了好久用什麼好例子來介紹命令模式,最後發現還是《Head First設計模式》中的例子最恰當。
下面是例子介紹:
你所在的公司從家電公司處接到了一個任務,家電公司給你提供各種傢俱如風扇、窗簾、電燈等的具體實現類,你需要設計一個遙控器類來控制提供的傢俱,前提是你不能修改提供的家電類的代碼。
注意,不同的家電類中的方法接口不一定一樣,比如開風扇是fan.open() 而開電燈則是light.on()。
家電公司給的第一個任務是實現一個控制風扇窗簾和電燈開關的遙控器。
我們先看一下家電公司提供的家電類的代碼
Furniture.java
package priv.mxz.design_pattern.command_pattern;
class Curtain {
public void turnon(){
System.out.println("[Curtain]:turnon");
}
public void turnoff(){
System.out.println("[Curtain]:turnoff");
}
}
class Fan {
public void open(){
System.out.println("[Fan]:open");
}
public void close(){
System.out.println("[Fan]:close");
}
}
class Light {
public void on(){
System.out.println("[Light]:on");
}
public void off(){
System.out.println("[Light]:off");
}
}
可以看出不同家電類中開關操作的接口都是不一樣的。
不使用命令模式的實現
實現一個控制風扇窗簾和電燈開關的遙控器這個任務落在了你公司的實習生阿呆手上,下面是阿呆的實現
SimpleController.java
package priv.mxz.design_pattern.command_pattern;
enum ControllerButton {
CURTAINON,CURTAINOFF,FANON,FANOFF,LIGHTON,LIGHTOFF;
}
class SimpleController {
private Curtain curtain=null;
private Fan fan=null;
private Light light=null;
public SimpleController(Curtain curtain, Fan fan, Light light){
this.curtain=curtain;
this.fan=fan;
this.light=light;
}
public void pushButton(ControllerButton button){
switch (button){
case CURTAINON:
curtain.turnon();
break;
case CURTAINOFF:
curtain.turnoff();
break;
case FANON:
fan.open();
break;
case FANOFF:
fan.close();
break;
case LIGHTON:
light.on();
break;
case LIGHTOFF:
light.off();
break;
default:
break;
}
}
}
class SimpleControllerTest{
public static void main(String[] args) {
SimpleController simpleController=new SimpleController(new Curtain(),new Fan(),new Light());
for (ControllerButton button:ControllerButton.values()){
simpleController.pushButton(button);
}
}
}
SImpleControllerTest中有程序入口,對SimpleController進行測試,測試後發現這個遙控器的基本功能已經得到實現。
下面是輸出結果
[Curtain]:turnon
[Curtain]:turnoff
[Fan]:open
[Fan]:close
[Light]:on
[Light]:off
把這個遙控器類交給家電公司後,家電公司覺得很“不錯”,於是把其他遙控器的實現也交給了你所在的公司,這些控制器包括:只控制風扇和電燈的遙控器、控制門和空調的遙控器、控制風扇和空調和熱水器的遙控器等等等等。。。
這些遙控器再次交給了實習生阿呆來實現,在用同樣的方式實現了幾個遙控器後,阿呆陷入了思考,因爲他發現不同的遙控器的實現是非常類似的,都是用switch case來判斷按鍵對應的家電類與方法,有沒有辦法可以把類似的行爲抽象出來呢?比如定義一個父類?阿呆思考着能不能夠把最重要的方法pushButton放到父類中?結果發現是不可以的,pushButton裏面需要執行綁定的家電類實例的方法,而這些家電類實例是每一個子類都不一樣的,無法統一放到父類中。
使用命令模式的實現
阿呆把他的這個困惑告訴了他的導師阿強,阿強看完阿呆的代碼後,說出了無法抽象遙控器行爲的原因:遙控器類直接與家電類進行交互了,遙控器類與家電類之間存在強耦合,所以無法很好地把遙控器的行爲從家電類中分離出來。要想把遙控器的行爲抽象出來,必須讓遙控器與家電類實現解耦。
在解釋完後,阿強又給阿呆介紹了命令模式,然後用命令模式做了基礎遙控器類的實現。
命令模式的定義
將請求封裝成對象,這可以讓你使用不同的請求、隊列或者日誌請求來參數化其他對象,命令模式可以支持撤銷操作。
模式結構
命令模式包含以下角色:
- Command 抽象命令類
- ConcreteCommand 具體命令類
- Invoker 調用者
- Receiver 接收者
- Client 客戶類
下面的UML類圖介紹了各個角色之間的關係
從上往下看,Client類表示用戶,用戶通過向Invoker傳入具體的Command以及Command的Receiver(在new Command時傳入),來指定用戶需要執行的指令以及指令接收者,用戶不需要知道指令接收者詳細要調用什麼方法來完成指令要求的行爲,只需要把要指令接收者要做的事情通過命令來傳達。
Invoker類表示調用者,負責與Command交互,Invoker並不清楚用戶傳給自己的Command具體是負責什麼的,它只負責在適當的時候調用Command的execute方法。
Command是抽象接口,定義execute方法來執行自身的命令。
ConcreteCommandA(B)是實現Command接口的具體類,內部有該command的接收者receiver的實例,execute方法的具體實現就是調用receiver的某些方法來實現命令表示的功能。
ReceiverA(B)命令的接收者類,注意,不同的receiver雖然角色類似,但不一定都繼承於相同的類或者有相同的接口,不同的receiver可以是完全無關係的類,具體怎麼使用這些類的方法就交給對應的ConcreteCommand負責。
具體代碼實現
命令模式中的對象與例子中的類對應關係如下
命令模式中 | 例子中 |
---|---|
Receiver | Fan Light Curtain |
Command | Command |
ConcreteCommand | FanOnCommand FanOffCommand LightOnCommand 。。。 |
Invoker | CommandController |
Client | CommandControllerTest |
下面是代碼
Command.java
package priv.mxz.design_pattern.command_pattern;
interface Command {
void execute();
}
class CurtainOnCommand implements Command{
private Curtain curtain;
public CurtainOnCommand(Curtain curtain){
this.curtain=curtain;
}
@Override
public void execute() {
curtain.turnon();
}
}
class CurtainOffCommand implements Command{
private Curtain curtain;
public CurtainOffCommand(Curtain curtain){
this.curtain=curtain;
}
@Override
public void execute() {
curtain.turnon();
}
}
class FanOnCommand implements Command{
private Fan fan;
public FanOnCommand(Fan fan){
this.fan=fan;
}
@Override
public void execute() {
fan.open();
}
}
class FanOffCommand implements Command{
private Fan fan;
public FanOffCommand(Fan fan){
this.fan=fan;
}
@Override
public void execute() {
fan.close();
}
}
class LightOnCommand implements Command{
private Light light;
public LightOnCommand(Light light){
this.light=light;
}
@Override
public void execute() {
light.on();
}
}
class LightOffCommand implements Command{
private Light light;
public LightOffCommand(Light light){
this.light=light;
}
@Override
public void execute() {
light.off();
}
}
class NoCommand implements Command{
@Override
public void execute() {
// do nothing
}
}
Command.java定義了Command接口和其他具體的Command類,每一個對家電的操作對應一個具體的Command類,比如CurtainOnCommand對應拉開窗簾的操作。
特別的一個實現Command接口的類是NoCommand,這是一個空對象,用於遙控器中對命令的初始化設置。
CommandController.java
package priv.mxz.design_pattern.command_pattern;
import java.util.ArrayList;
public class CommandController {
ArrayList<Command> commands;
public CommandController(int buttonSize){
if (buttonSize>0){
commands=new ArrayList<Command>(buttonSize);
for(int i=0; i<buttonSize; i++){
commands.add(i,new NoCommand());
}
}
}
public void setCommand(int buttonIndex, Command command){
if (buttonIndex>=0 && buttonIndex<commands.size()){
commands.set(buttonIndex,command);
}
}
public void pushButton(int buttonIndex){
if (buttonIndex>=0&& buttonIndex<commands.size()) {
commands.get(buttonIndex).execute();
}
}
}
CommandController定義了一個用命令模式實現的遙控器,在內部存儲Command數組,每一個Command實例對應一個按鈕,在構造函數中初始化Command數組大小(即按鈕的數目),然後把每個指令都設置爲NoCommand實例,這麼做的原因是解決下述的一種情況:用戶沒有在對應按鈕上調用setCommand綁定新的指令,就調用pushButton來執行還沒綁定的指令(即執行command.execute())。 這種情況下如果Command數組中對應的command沒有被初始化,就會報異常。NoCommand類裏面execute方法什麼都不幹,即使用戶調用也不會拋異常和對系統造成影響。
CommandControllerTest.java
package priv.mxz.design_pattern.command_pattern;
public class CommandControllerTest {
public static void main(String[] args) {
CommandController commandController=new CommandController(6);
Curtain curtain=new Curtain();
commandController.setCommand(0,new CurtainOnCommand(curtain));
commandController.setCommand(1,new CurtainOffCommand(curtain));
Fan fan=new Fan();
commandController.setCommand(2,new FanOnCommand(fan));
commandController.setCommand(3,new FanOffCommand(fan));
Light light=new Light();
commandController.setCommand(4,new LightOnCommand(light));
commandController.setCommand(5,new LightOffCommand(light));
for(int i=0; i<6; i++){
commandController.pushButton(i);
}
}
}
CommandControllerTest中從客戶的角度定義了一個遙控器,這個遙控器有六個按鈕,功能按順序是開窗簾、關窗簾、開風扇、關風扇、開燈和關燈。我們通過setCommand配置好按鈕對應的command,最後依次按下按鈕,下面是輸出
[Curtain]:turnon
[Curtain]:turnon
[Fan]:open
[Fan]:close
[Light]:on
[Light]:off
例子小結
我們對比一下不使用命令模式的遙控器實現和使用了命令模式的遙控器實現。
- 不使用命令模式的情況下,SimpleController類需要與多個家電類進行交互,在SimpleController類內部需要存儲具體的家電類實例,在按下按鈕判斷執行什麼功能時需要使用大量的if判斷或者switch。如果要控制新的家電或者修改按鍵對應的功能必須修改控制器內部的代碼,不符合開閉原則。SimpleController是遙控器的一個具體實現,只能對應一種具體的遙控器。(如果你問爲什麼不抽象出一個遙控器基類出來,我只能告訴你,你可以去試試,然後會發現無法抽象出很有用的功能出來,最多隻能定義一下接口)
- 使用命令模式的情況下,我們實現了一個CommandController類,它不需要與各種各樣的家電進行交互,而是選擇與具有統一Command接口的類進行交互,在類內部不需要存儲每個家電類的實例,只需要存儲command數組即可,抽象的command接口可以使CommandController類實例具有更豐富的變化,這符合“依賴倒置”的設計原則(依賴抽象類,不要依賴具體類)。我們可以基於這個類實現有不同數目按鈕和控制不同家電的遙控器,這個類本身可以多次複用。需要控制新的家電只需要定義好對應的Command類,然後再使用CommandController類實例時傳入command就好,CommandController類的代碼無需改動,就可以得到一個控制新家電的一個遙控器。我們要改變遙控器上某個按鈕的功能也十分方便,只需要用setCommand重新傳入需要的command就可以。
命令模式的擴展
繼續使用上述遙控器的例子,如果我們需要支持一個按鍵(一個command)就可以實現多個動作,即宏命令,我們可以定義一個實現了Command接口且自身存儲多個command的類,下面是一種實現
MacroCommand.java
class MacroCommand implements Command{
private ArrayList<Command> commands;
public MacroCommand(ArrayList<Command> commands){
this.commands=commands;
}
@Override
public void execute() {
if (commands!=null){
for(Command command:commands){
command.execute();
}
}
}
}
我們也可以用命令模式支持遙控器的撤回操作(undo)具體只需在Command接口新加一個undo方法,然後在對應的類中實現具體的行爲,比如LightOnCommand的undo方法只需要調用light.off()
使用場景
命令模式適合用於
- 系統需要將請求調用者和請求接收者解耦,使得調用者和接收者不直接交互。
- 系統需要在不同的時間指定請求、將請求排隊和執行請求。
- 系統需要支持命令的撤銷(Undo)操作和恢復(Redo)操作。
- 系統需要將一組操作組合在一起,即支持宏命令
優缺點
命令模式的優點
- 降低系統的耦合度。
- 新的命令可以很容易地加入到系統中。
- 可以比較容易地設計一個命令隊列和宏命令(組合命令)。
- 可以方便地實現對請求的Undo和Redo。
命令模式的缺點
- 使用命令模式可能會導致某些系統有過多的具體命令類。因爲針對每一個命令都需要設計一個具體命令類,因此某些系統可能需要大量具體命令類,這將影響命令模式的使用。
總結
- 在命令模式中,將一個請求封裝爲一個對象,從而使我們可用不同的請求對客戶進行參數化;對請求排隊或者記錄請求日誌,以及支持可撤銷的操作。命令模式是一種對象行爲型模式,其別名爲動作模式或事務模式。
- 命令模式包含四個角色:抽象命令類中聲明瞭用於執行請求的execute()等方法,通過這些方法可以調用請求接收者的相關操作;具體命令類是抽象命令類的子類,實現了在抽象命令類中聲明的方法,它對應具體的接收者對象,將接收者對象的動作綁定其中;調用者即請求的發送者,又稱爲請求者,它通過命令對象來執行請求;接收者執行與請求相關的操作,它具體實現對請求的業務處理。
- 命令模式的本質是對命令進行封裝,將發出命令的責任和執行命令的責任分割開。命令模式使請求本身成爲一個對象,這個對象和其他對象一樣可以被存儲和傳遞。
- 命令模式的主要優點在於降低系統的耦合度,增加新的命令很方便,而且可以比較容易地設計一個命令隊列和宏命令,並方便地實現對請求的撤銷和恢復;其主要缺點在於可能會導致某些系統有過多的具體命令類。
- 命令模式適用情況包括:需要將請求調用者和請求接收者解耦,使得調用者和接收者不直接交互;需要在不同的時間指定請求、將請求排隊和執行請求;需要支持命令的撤銷操作和恢復操作,需要將一組操作組合在一起,即支持宏命令
參考
https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/command.html