天天看點

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

本文的概念内容來自深入淺出設計模式一書.

項目需求

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

有這樣一個可程式設計的新型遙控器, 它有7個可程式設計插槽, 每個插槽可連接配接不同的家用電器裝置. 每個插槽對應兩個按鈕: 開, 關(ON, OFF). 此外還有一個全局的取消按鈕(UNDO).

現在客戶想使用這個遙控器來控制不同廠家的家用電器, 例如電燈, 熱水器, 風扇, 音響等等.

客戶提出讓我編寫一個接口, 可以讓這個遙控器控制插在插槽上的一個或一組裝置.

看一下目前各家廠商都有哪些家用電器:

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

問題來了, 這些家用電器并沒有共同的标準....幾乎各自都有自己的一套控制方法.. 而且以後還要添加很多種家用電器.

設計思路

那就需要考慮一下設計方案了:

首先要考慮分離關注點(Separation of concerns),  遙控器應該可以解釋按鈕動作并可以發送請求, 但是它不應該了解家用電器和如何開關家用電器等.

但是目前遙控器隻能做開關功能, 那麼怎麼讓它去控制電燈或者音響呢? 我們不想讓遙控器知道這些具體的家用電器, 更不想寫出下面的代碼:

if slot1 == Light then Light.On()
else if slot1 == Hub....      

說到這就不得不提到指令模式(Command Pattern)了.

指令模式允許你把動作的請求者和動作的實際執行者解耦. 這裡, 動作的請求者就是遙控器, 而執行動作的對象就是某個家用電器.

這是怎麼解耦的呢? 怎麼可能實作呢?

這就需要引進"指令對象(command object)"了. 指令對象會封裝在某個對象上(例如卧室的燈)執行某個動作的請求(例如開燈). 是以, 如果我們為每一個按鈕都準備一個指令對象, 那麼當按鈕被按下的時候, 我們就會調用這個指令對象去執行某些動作. 遙控器本身并不知道具體執行的動作是什麼, 它隻是有一個指令對象, 這個指令對象知道去對哪些電器去做什麼樣的操作. 就這樣, 遙控器和電燈解耦了.

一個指令模式的實際例子

一個快餐廳:

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

客戶給服務員訂單, 服務員把訂單放到櫃台并說: "有新訂單了", 然後廚師按照訂單準備飯菜.

讓我們仔細分析一下它們是怎麼互動的:

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

客戶來了, 說我想要漢堡, 奶酪....就是建立了一個訂單 (createOrder()).

訂單上面寫着客戶想要的飯菜. 

服務員取得訂單 takeOrder(), 把訂單拿到櫃台喊道: "有新訂單了" (調用orderUp())

廚師按照訂單的訓示把飯菜做好 (orderUp()裡面的動作). 

分析一下這個例子的角色和職責:

  • 訂單裡封裝了做飯菜的請求. 可以把訂單想象成一個對象, 這個對象就像是對做飯這個動作的請求. 并且它可以來回傳遞. 訂單實作了一個隻有orderUp()方法的接口, 這個方法裡面封裝了做飯的操作流程. 訂單同時對動作實施者的引用(廚師). 因為都封裝了, 是以服務員不知道訂單裡面有啥也不知道廚師是誰. 服務員隻傳遞訂單, 并調用orderUp().
  • 是以, 服務員的工作就是傳遞訂單并且調用orderUp(). 服務員的取訂單takeOrder()方法會傳進來不同的參數(不同客戶的不同訂單), 但是這不是問題, 因為她知道所有的訂單都支援orderUp()方法.
  • 廚師知道如何把飯做好. 一旦服務員調用了orderUp(), 廚師就接管了整個工作把飯菜做好. 但是服務員和廚師是解耦的: 服務員隻有訂單, 訂單裡封裝着飯菜, 服務員隻是調用訂單上的一個方法而已. 同樣的, 廚師隻是從訂單上收到指令, 他從來不和服務員直接接觸.

項目設計圖

回到我們的需求, 參考快餐店的例子, 使用指令模式做一下設計:

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

客戶Client建立了一個指令(Command)對象. 相當于客人拿起了一個訂單(點菜)準備開始點菜, 我在琢磨遙控器的槽需要插哪些家用電器. 指令對象和接收者是綁定在一起的. 相當于菜單和廚師, 遙控器的插槽和目标家用電器.

指令對象隻有一個方法execute(), 裡面封裝了調用接收者實際控制操作的動作. 相當于飯店訂單的orderUp().

客戶調用setCommand()方法. 相當于客戶想好點什麼菜了, 就寫在訂單上面了. 我也想好遙控器要控制哪些家電了, 列好清單了. 

調用者拿着已經setCommand的指令對象, 在未來某個時間點調用指令對象上面的execute()方法. 相當于服務員拿起訂單走到櫃台前, 大喊一聲: "有訂單來了, 開始做菜吧". 相當于我把遙控器和裝置的接口連接配接上了, 準備開始控制.

最後接收者執行動作. 相當于廚師做飯. 家用電器使用自己獨有的控制方法進行動作.

這裡面:

客戶 --- 飯店客人, 我

指令 --- 訂單, 插槽

調用者 --- 服務員, 遙控器

setCommand()設定指令 --- takeOrder() 取訂單, 插上需要控制的電器

execute() 執行 ---  orderUp() 告訴櫃台做飯, 按按鈕

接收者 --- 廚師, 家電

代碼實施

所有指令對象需要實作的接口:

namespace CommandPattern.Abstractions
{
    public interface ICommand
    {
        void Execute();
    }
}      

一盞燈:

using System;

namespace CommandPattern.Devices
{
    public class Light
    {
        public void On()
        {
            Console.WriteLine("Light is on");
        }

        public void Off()
        {
            Console.WriteLine("Light is off");
        }
    }
}      

控制燈打開的指令:

using CommandPattern.Abstractions;
using CommandPattern.Devices;

namespace CommandPattern.Commands
{
    public class LightOnCommand : ICommand
    {
        private readonly Light light;

        public LightOnCommand(Light light)
        {
            this.light = light;
        }

        public void Execute()
        {
            this.light.On();
        }
    }
}      

車庫門: 

using System;

namespace CommandPattern.Devices
{
    public class GarageDoor
    {
        public void Up()
        {
            Console.WriteLine("GarageDoor is opened.");
        }

        public void Down()
        {
            Console.WriteLine("GarageDoor is closed.");
        }
    }
}      

收起車庫門指令:

using CommandPattern.Abstractions;
using CommandPattern.Devices;

namespace CommandPattern.Commands
{
    public class GarageDoorOpen : ICommand
    {
        private readonly GarageDoor garageDoor;

        public GarageDoorOpen(GarageDoor garageDoor)
        {
            this.garageDoor = garageDoor;
        }

        public void Execute()
        {
            garageDoor.Up();
        }
    }
}      

簡易的遙控器:

using CommandPattern.Abstractions;

namespace CommandPattern.RemoteControls
{
    public class SimpleRemoteControl
    {
        public ICommand Slot { get; set; }
public void ButtonWasPressed()
        {
            Slot.Execute();
        }
    }
}      

運作測試:

using System;
using CommandPattern.Commands;
using CommandPattern.Devices;
using CommandPattern.RemoteControls;

namespace CommandPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var remote = new SimpleRemoteControl();
            var light = new Light();
            var lightOn = new LightOnCommand(light);

            remote.Slot = lightOn;
            remote.ButtonWasPressed();

            var garageDoor = new GarageDoor();
            var garageDoorOpen = new GarageDoorOpenCommand(garageDoor);

            remote.Slot = garageDoorOpen;
            remote.ButtonWasPressed();
        }
    }
}      
使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

指令模式定義

指令模式把請求封裝成一個對象, 進而可以使用不同的請求對其它對象進行參數化, 對請求排隊, 記錄請求的曆史, 并支援取消操作.

類圖:

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

效果圖:

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

全功能代碼的實施

遙控器:

using System.Text;
using CommandPattern.Abstractions;
using CommandPattern.Commands;

namespace CommandPattern.RemoteControls
{
    public class RemoteControl
    {
        private ICommand[] onCommands;
        private ICommand[] offCommands;

        public RemoteControl()
        {
            onCommands = new ICommand[7];
            offCommands = new ICommand[7];

            var noCommand = new NoCommand();
            for (int i = 0; i < 7; i++)
            {
                onCommands[i] = noCommand;
                offCommands[i] = noCommand;
            }
        }

        public void SetCommand(int slot, ICommand onCommand, ICommand offCommand)
        {
            onCommands[slot] = onCommand;
            offCommands[slot] = offCommand;
        }

        public void OnButtonWasPressed(int slot)
        {
            onCommands[slot].Execute();
        }
        public void OffButtonWasPressed(int slot)
        {
            offCommands[slot].Execute();
        }

        public override string ToString()
        {
            var sb = new StringBuilder("\n------------Remote Control-----------\n");
            for(int i =0; i< onCommands.Length; i++){
                sb.Append($"[slot{i}] {onCommands[i].GetType()}\t{offCommands[i].GetType()} \n");
            }
            return sb.ToString();
        }
    }
}      

這裡面有一個NoCommand, 它是一個空的類, 隻是為了初始化command 以便以後不用判斷是否為null.

關燈:

using CommandPattern.Abstractions;
using CommandPattern.Devices;

namespace CommandPattern.Commands
{
    public class LightOffCommand: ICommand
    {
        private readonly Light light;

        public LightOffCommand(Light light)
        {
            this.light = light;
        }

        public void Execute()
        {
            light.Off();
        }
    }
}      

下面試一個有點挑戰性的, 音響:

namespace CommandPattern.Devices
{
    public class Stereo
    {
        public void On()
        {
            System.Console.WriteLine("Stereo is on.");
        }

        public void Off()
        {
            System.Console.WriteLine("Stereo is off.");
        }

        public void SetCD()
        {
            System.Console.WriteLine("Stereo is set for CD input.");
        }

        public void SetVolume(int volume)
        {
            System.Console.WriteLine($"Stereo's volume is set to {volume}");
        }
    }
}      

音響打開指令:

using CommandPattern.Abstractions;

namespace CommandPattern.Devices
{
    public class StereoOnWithCDCommand : ICommand
    {
        private readonly Stereo stereo;

        public StereoOnWithCDCommand(Stereo stereo)
        {
            this.stereo = stereo;
        }

        public void Execute()
        {
            stereo.On();
            stereo.SetCD();
            stereo.SetVolume(10);
        }
    }
}      

測試運作:

using System;
using CommandPattern.Commands;
using CommandPattern.Devices;
using CommandPattern.RemoteControls;

namespace CommandPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var remote = new RemoteControl();
            var light = new Light();
            var lightOn = new LightOnCommand(light);
            var lightOff = new LightOffCommand(light);
            var garageDoor = new GarageDoor();
            var garageDoorOpen = new GarageDoorOpenCommand(garageDoor);
            var garageDoorClose = new GarageDoorCloseCommand(garageDoor);
            var stereo = new Stereo();
            var stereoOnWithCD = new StereoOnWithCDCommand(stereo);
            var stereoOff = new StereoOffCommand(stereo);

            remote.SetCommand(0, lightOn, lightOff);
            remote.SetCommand(1, garageDoorOpen, garageDoorClose);
            remote.SetCommand(2, stereoOnWithCD, stereoOff);

            System.Console.WriteLine(remote);

            remote.OnButtonWasPressed(0);
            remote.OffButtonWasPressed(0);
            remote.OnButtonWasPressed(1);
            remote.OffButtonWasPressed(1);
            remote.OnButtonWasPressed(2);
            remote.OffButtonWasPressed(2);
        }
    }
}      
使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

該需求的設計圖:

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

還有一個問題...取消按鈕呢?

實作取消按鈕

1. 可以在ICommand接口裡面添加一個undo()方法, 然後在裡面執行上一次動作相反的動作即可:

namespace CommandPattern.Abstractions
{
    public interface ICommand
    {
        void Execute();
        void Undo();
    }
}      

例如開燈:

using CommandPattern.Abstractions;
using CommandPattern.Devices;

namespace CommandPattern.Commands
{
    public class LightOnCommand : ICommand
    {
        private readonly Light light;

        public LightOnCommand(Light light)
        {
            this.light = light;
        }

        public void Execute()
        {
            light.On();
        }

        public void Undo()
        {
            light.Off();
        }
    }
}      
using System.Text;
using CommandPattern.Abstractions;
using CommandPattern.Commands;

namespace CommandPattern.RemoteControls
{
    public class RemoteControlWithUndo
    {
        private ICommand[] onCommands;
        private ICommand[] offCommands;
        private ICommand undoCommand;

        public RemoteControlWithUndo()
        {
            onCommands = new ICommand[7];
            offCommands = new ICommand[7];

            var noCommand = new NoCommand();
            for (int i = 0; i < 7; i++)
            {
                onCommands[i] = noCommand;
                offCommands[i] = noCommand;
            }
            undoCommand = noCommand;
        }

        public void SetCommand(int slot, ICommand onCommand, ICommand offCommand)
        {
            onCommands[slot] = onCommand;
            offCommands[slot] = offCommand;
        }

        public void OnButtonWasPressed(int slot)
        {            
            onCommands[slot].Execute();
            undoCommand = onCommands[slot];
        }

        public void OffButtonWasPressed(int slot)
        {
            offCommands[slot].Execute();
            undoCommand = offCommands[slot];
        }

        public void UndoButtonWasPressed()
        {
            undoCommand.Undo();
        }

        public override string ToString()
        {
            var sb = new StringBuilder("\n------------Remote Control-----------\n");
            for(int i =0; i< onCommands.Length; i++){
                sb.Append($"[slot{i}] {onCommands[i].GetType()}\t{offCommands[i].GetType()} \n");
            }
            return sb.ToString();
        }
    }
}      

測試一下:

using System;
using CommandPattern.Commands;
using CommandPattern.Devices;
using CommandPattern.RemoteControls;

namespace CommandPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var remote = new RemoteControl();
            var light = new Light();
            var lightOn = new LightOnCommand(light);
            var lightOff = new LightOffCommand(light);
            var stereo = new Stereo();
            var stereoOnWithCD = new StereoOnWithCDCommand(stereo);
            var stereoOff = new StereoOffCommand(stereo);

            remote.SetCommand(0, lightOn, lightOff);
            remote.SetCommand(1, stereoOnWithCD, stereoOff);

            System.Console.WriteLine(remote);

            remote.OnButtonWasPressed(0);
            remote.OffButtonWasPressed(0);
            remote.OnButtonWasPressed(1);
            remote.OffButtonWasPressed(1);
        }
    }
}      
使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

基本是OK的, 但是有點小問題, 音響的開關狀态倒是取消了, 但是它的音量(也包括播放媒體, 不過這個我就不去實作了)并沒有恢複.

下面就來處理一下這個問題.

修改Stereo:

namespace CommandPattern.Devices
{
    public class Stereo
    {

        public Stereo()
        {
            Volume = 5;
        }

        public void On()
        {
            System.Console.WriteLine("Stereo is on.");
        }

        public void Off()
        {
            System.Console.WriteLine("Stereo is off.");
        }

        public void SetCD()
        {
            System.Console.WriteLine("Stereo is set for CD input.");
        }

        private int volume;
        public int Volume
        {
            get { return volume; }
            set
            {
                volume = value;
                System.Console.WriteLine($"Stereo's volume is set to {volume}");
            }
        }

    }
}      

指令:

using CommandPattern.Abstractions;

namespace CommandPattern.Devices
{
    public class StereoOnWithCDCommand : ICommand
    {
        private int previousVolume;

        private readonly Stereo stereo;
public StereoOnWithCDCommand(Stereo stereo)
        {
            this.stereo = stereo;
       previousVolume = stereo.Volume;
        }

        public void Execute()
        {
            stereo.On();
            stereo.SetCD();
            stereo.Volume = 10;
        }

        public void Undo()
        {
            stereo.Volume = previousVolume;
            stereo.SetCD();
            stereo.Off();
        }
    }
}      

運作:

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

需求變更----一個按鈕控制多個裝置的多個動作

Party Mode (聚會模式):

思路是建立一種指令, 它可以執行多個其它指令

MacroCommand:

using CommandPattern.Abstractions;

namespace CommandPattern.Commands
{
    public class MacroCommand : ICommand
    {
        private ICommand[] commands;

        public MacroCommand(ICommand[] commands)
        {
            this.commands = commands;
        }

        public void Execute()
        {
            for (int i = 0; i < commands.Length; i++)
            {
                commands[i].Execute();
            }
        }

        public void Undo()
        {
            for (int i = 0; i < commands.Length; i++)
            {
                commands[i].Undo();
            }
        }
    }
}      

使用這個MacroCommand:

using System;
using CommandPattern.Abstractions;
using CommandPattern.Commands;
using CommandPattern.Devices;
using CommandPattern.RemoteControls;

namespace CommandPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var light = new Light();
            var lightOn = new LightOnCommand(light);
            var lightOff = new LightOffCommand(light);
            var garageDoor = new GarageDoor();
            var garageDoorOpen = new GarageDoorOpenCommand(garageDoor);
            var garageDoorClose = new GarageDoorCloseCommand(garageDoor);
            var stereo = new Stereo();
            var stereoOnWithCD = new StereoOnWithCDCommand(stereo);
            var stereoOff = new StereoOffCommand(stereo);

            var macroOnCommand = new MacroCommand(new ICommand[] { lightOn, garageDoorOpen, stereoOnWithCD });
            var macroOffCommand = new MacroCommand(new ICommand[] { lightOff, garageDoorClose, stereoOff });

            var remote = new RemoteControl();
            remote.SetCommand(0, macroOnCommand, macroOffCommand);
            System.Console.WriteLine(remote);

            System.Console.WriteLine("--- Pushing Macro on ---");
            remote.OnButtonWasPressed(0);
            System.Console.WriteLine("--- Pushing Macro off ---");
            remote.OffButtonWasPressed(0);
        }
    }
}      
使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

指令模式實際應用舉例

請求隊列

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

這個工作隊列是這樣工作的: 你添加指令到隊列的結尾, 在隊列的另一端有幾個線程. 線程這樣工作: 它們從隊列移除一個指令, 調用它的execute()方法, 然後等待調用結束, 然後丢棄這個指令再擷取一個新的指令.

這樣我們就可以把計算量限制到固定的線程數上面了. 工作隊列和做工作的對象也是解耦的.

記錄請求

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它
使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它

這個例子就是使用指令模式記錄請求動作的曆史, 如果出問題了, 可以按照這個曆史進行恢複.

其它

這個系列的代碼我放在這裡了: 

https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp

下面是我的關于ASP.NET Core Web API相關技術的公衆号--草根專欄:

使用C# (.NET Core) 實作指令設計模式 (Command Pattern)項目需求設計思路一個指令模式的實際例子項目設計圖代碼實施指令模式定義全功能代碼的實施實作取消按鈕需求變更----一個按鈕控制多個裝置的多個動作指令模式實際應用舉例其它