c#中命令模式詳解

2023-11-16 09:00:30

基本介紹: 

  命令模式,顧名思義就是將命令抽象化,然後將請求者和接收者通過命令進行繫結。

  而命令的請求者只管下達命令,命令的接收者只管執行命令。

  從而實現瞭解耦,請求者和接受者二者相對獨立。

  單獨理解起來比較困難,咱們還是通過具體範例來說明吧。

舉例說明:

  生活中遙控控制電器就是命令模式,比如智慧開關控制一盞燈。

  首先將燈的開和關封裝成一個命令,然後將這個命令繫結到智慧開關上。

  智慧開關的執行其實就是燈開關的執行,燈和智慧開關是通過命令來進行互動的。

  這個時候如果想控制電視的開關,那就可以將電視的開關封裝成一個命令。

  然後將這個命令繫結到智慧開關上,智慧開關的執行這個時候就變成了電視的開關。

  如果即想控制電視又想控制燈,那就可以將燈的命令和電視的命令都繫結到智慧開關上。

 

  又比如皇帝寫了一道聖旨,命令張三即刻回京。

  聖旨跟張三是繫結關係,也就是說張三是實際執行者,聖旨是命令,起到的作用就是跟張三進行繫結和傳達資訊。

  那想個問題傳達聖旨的宦官是誰有關係嗎,是沒有的,是誰都可以。但又是不可或缺的。它就相當於一個遙控或者說是中間人、傳話人。

  是皇帝將這個宦官和聖旨進行了繫結(使用者端就是那個皇帝)。那在想個問題,是先有聖旨再有張三呢,還是先有張三再有聖旨。

  答案是肯定的,先有張三,也就是說得先有具體執行者。

  然後再有聖旨即命令,通過名字將張三和聖旨繫結到一起。

  如果皇帝想命令李四也回京,那這個聖旨可以命令李四嗎?是不可以的,需要重新寫一份聖旨。

  所以從這個不難看出,命令類和具體的執行者是強繫結關係,當然也可以做成工廠。

  這個時候大家想個問題,兩道聖旨是不是可以讓同一個宦官去傳達就可以。因為宦官和執行者不存在繫結,它只和聖旨繫結。

  所以說宦官就相當於遙控,它只需要宣讀聖旨而不必在意具體是怎麼執行的,就可以讓不同的執行人進行不同的操作。

  這就是命令模式的本質。

基本結構:

  通過上述例子不難看出,命令模式主要核心構件有以下幾個:

  ◊ 命令的執行者(接收者Receiver):它單純的只具體實現了功能。(範例中對應的則是張三和李四)

  ◊ 命令的下達者(呼叫者Invoker)

    就是起到遙控的作用,首先在類中宣告抽象命令類的參照,並通過引數注入等方式進行範例化。

    然後通過呼叫命令物件來告訴執行者執行功能。起到一個傳聲筒的作用。(範例中對應的是宦官)

  ◊ 抽象命令類Command

    主要作用有兩點,其一就是為了規範具體命令類,宣告其執行方法。

    其二就是為了方便呼叫者Invoker呼叫,使其不需要知道具體命令類,只需要執行抽象命令類即可。(範例中對應的是泛指聖旨)

  ◊ 具體命令類ConcreteCommand

    繼承自抽象類,在類中首先宣告了執行者的參照,通過引數注入等方式進行建立執行者物件,將命令類和執行者進行繫結。

    其次具體實現了抽象類中宣告的執行方法,呼叫執行者物件中方法進行執行。(範例中對應的是張三和李四具體的兩道聖旨)

  ◊ 使用者端角色Client

    主要是負責將執行者和命令類進行繫結,其次將命令類和呼叫者進行繫結。

    使其呼叫者可以通過命令類給執行者傳遞訊息進行執行。(範例中對應的是皇帝角色)

優缺點:

  優點

    1.降低了系統的耦合性。將呼叫操作物件和知道如何實現該操作物件的解耦。

    2.通過命令模式,新的命令可以很輕鬆的加入系統中,且不會影響其他類,符合開閉原則。

  缺點

    使用命令模式可能會導致某些系統存在過多的命令類,會影響使用。

  命令模式適用情形

    1.將使用者介面和行為分離。

    2.對請求進行排隊,記錄請求紀錄檔以及支援可復原的操作等。

具體範例:

  就以皇帝下達聖旨作為範例吧,首先先說下順序。

    1.得先有執行者也就是接收聖旨的人,它主要是執行具體的行為和功能,如果是遙控控制燈,那這裡就是那盞燈本身。這裡命名為Receiver_ZhangSan類。

    2.其次就需要建立聖旨,也就是傳達命令的媒介。但每道聖旨都是一樣的材質和規則,所以需要先建立一個抽象類來規範。這裡將抽象類命名為Command類,將具體命令類即聖旨內容命名為ConcreteCommand類它繼承自抽象類。

    3.有了執行人也有了聖旨,那就可以安排一個傳旨的人就可以了(如果是遙控控制燈,這裡相當於遙控)。這裡命名為Invoker類。

    4.執行人有了,聖旨有了,傳旨的人也有了,那皇帝就可以下令執行了。這裡就相當於使用者端呼叫了。

  實現方式說明:

    1.建立Receiver_ZhangSan類,設定具體執行方法,這裡可以是騎馬回京,可以是其他的操作。它只負責具體功能的實現。

    2.建立Command抽象類,宣告Execute方法。

    3.建立ConcreteCommand命令類,繼承自抽象類。在該類中建立Receiver_ZhangSan類的參照,並通過引數注入或屬性賦值等方式範例化該類,並在Execute方法中具體呼叫該類中的具體實現方法。

    4.建立Invoker類,在該類中建立Command抽象類的參照,並通過引數注入或屬性賦值等方式將ConcreteCommand類範例化,並在該類中建立方法來執行抽象類中的Execute方法。

    5.使用者端直接呼叫Invoker類中的方法來執行命令。

    具體的執行過程就是,通過Invoker類中的方法來呼叫抽象類中的Execute方法其實就是呼叫ConcreteCommand中的Execute方法,

    然後ConcreteCommand中的Execute呼叫的又是Receiver_ZhangSan類中的具體實現方法。

    這樣就相當於通過Invoker類呼叫了Receiver_ZhangSan類中的方法,只不過是中間加了一個傳話人即命令類。

  為什麼這麼做呢?這樣做有什麼好處呢?

    好處也很好理解,那就是Invoker類不必知道具體執行人是誰,也不必知道具體怎麼執行,只需要將命令注入給該類就好了。

    這樣如果命令改變了,那直接改變注入的命令類就可以了,方便快捷,而且不需要修改現有的類,符合開閉原則。

    而且還可以按順序批次執行命令,只需要將命令依次注入給Invoker類進行呼叫就可以了。即任務佇列。

    也可以一次性注入多條命令,同時執行。

 1     /// <summary>
 2     /// 張三類,它是實際執行者,也就是命令接收者
 3     /// </summary>
 4     public class Receiver_ZhangSan
 5     {
 6         private string strName = "張三";
 7         public void Eat()
 8         {
 9             Console.WriteLine(strName + ":吃飯。");
10         }
11         public void Behavior1()
12         {
13             Console.WriteLine(strName + ":騎馬回京。");
14         }
15         public void Behavior2()
16         {
17             Console.WriteLine(strName + ":原地待命。");
18         }
19     }
20 
21     /// <summary>
22     /// 命令抽象類,規範命令類,並提供執行方法。
23     /// </summary>
24     public abstract class Command
25     {
26         //執行方法
27         public abstract void Execute();
28     }
29 
30     /// <summary>
31     /// 具體命令類,也就是聖旨書寫的具體內容。
32     /// </summary>
33     public class ConcreteCommand1 : Command
34     {
35         //建立執行人的參照,這裡使用readonly,規定只可以在建構函式中進行賦值。
36         private readonly Receiver_ZhangSan _Receiver_ZhangSan;
37 
38         //建構函式,引數注入方式,將執行人注入到命令類中。好比是將人名寫在聖旨上。
39         public ConcreteCommand1(Receiver_ZhangSan receiver_ZhangSan)
40         {
41             this._Receiver_ZhangSan = receiver_ZhangSan;
42         }
43 
44         //具體命令
45         public override void Execute()
46         {
47             //下達立刻回京的命令
48             this._Receiver_ZhangSan.Behavior1();
49         }
50     }
51 
52     /// <summary>
53     /// 呼叫者類,命令下達者,即宦官。
54     /// </summary>
55     public class Invoker
56     {
57         //建立抽象命令類的參照,這裡不同於具體命令類,方便演示,使用了引數賦值的形式進行注入。
58         public Command command { get; set; }
59 
60         //下達命令,即宣讀聖旨
61         public void Reading()
62         {
63             if (command != null)
64             {
65                 command.Execute();
66             }
67         }
68     }
69 
70     /// <summary>
71     /// 使用者端
72     /// </summary>
73     class Client
74     {
75         static void Main(string[] args)
76         {
77             //首先建立接收人,即張三。
78             Receiver_ZhangSan receiver_ZhangSan = new Receiver_ZhangSan();
79 
80             //然後建立命令類,即聖旨。這裡通過建構函式引數注入的形式將張三和命令進行繫結。
81             Command command = new ConcreteCommand1(receiver_ZhangSan);
82 
83             //其次建立呼叫者,即宦官。
84             Invoker invoker = new Invoker();
85             //為了演示不同注入方式,這裡通過屬性賦值的方式將命令和呼叫者繫結。
86             invoker.command = command;
87 
88             //呼叫,即宣讀聖旨
89             invoker.Reading();
90 
91             Console.WriteLine("\r\n");
92 
93             Console.ReadKey();
94         }
95     }

  如果皇帝想讓張三吃飽飯再回京,同時想讓李四原地待命該如何實現呢。

  兩種方式,如果張三和李四在一起,就可以寫在一個聖旨裡即寫在一個命令類裡。

  如果不在一起,那就需要寫兩道聖旨,即兩個命令類。

新增李四類:

 1     /// <summary>
 2     /// 李四類,它是實際執行者,也就是命令接收者
 3     /// </summary>
 4     public class Receiver_LiSi
 5     {
 6         private string strName = "李四";
 7         public void Eat()
 8         {
 9             Console.WriteLine(strName + ":吃飯。");
10         }
11         public void Behavior1()
12         {
13             Console.WriteLine(strName + ":坐馬車回京。");
14         }
15         public void Behavior2()
16         {
17             Console.WriteLine(strName + ":原地待命。");
18         }
19     }

新增新命令類:

 1     /// <summary>
 2     /// 聖旨,即命令類
 3     /// </summary>
 4     public class ConcreteCommand3 : Command
 5     {
 6         //建立執行人的參照
 7         private readonly Receiver_LiSi _Receiver_LiSi;
 8         private readonly Receiver_ZhangSan _Receiver_ZhangSan;
 9 
10         //將執行人注入到命令類中。
11         public ConcreteCommand3(Receiver_LiSi receiver_LiSi, Receiver_ZhangSan receiver_ZhangSan)
12         {
13             this._Receiver_LiSi = receiver_LiSi;
14             this._Receiver_ZhangSan = receiver_ZhangSan;
15         }
16 
17         //具體命令
18         public override void Execute()
19         {
20             //讓張三吃飽飯再回京
21             this._Receiver_ZhangSan.Eat();
22             this._Receiver_ZhangSan.Behavior1();
23             //讓李四原地待命
24             this._Receiver_LiSi.Behavior2();
25         }
26     }

使用者端呼叫:

 1     /// <summary>
 2     /// 使用者端
 3     /// </summary>
 4     class Client
 5     {
 6         static void Main(string[] args)
 7         {
 8             //首先建立接收人,即張三和李四。
 9             Receiver_ZhangSan receiver_ZhangSan = new Receiver_ZhangSan();
10             Receiver_LiSi receiver_LiSi = new Receiver_LiSi();
11 
12             //然後建立命令類,即聖旨。這裡通過建構函式引數注入的形式將張三和命令進行繫結。
13             Command command = new ConcreteCommand3(receiver_LiSi, receiver_ZhangSan);
14 
15             //其次建立呼叫者,即宦官。
16             Invoker invoker = new Invoker();
17             //為了演示不同注入方式,這裡通過屬性賦值的方式將命令和呼叫者繫結。
18             invoker.command = command;
19 
20             //呼叫,即宣讀聖旨
21             invoker.Reading();
22 
23             Console.WriteLine("\r\n");
24 
25             Console.ReadKey();
26         }
27     }

   以上範例則是張三和李四在一起,寫在一個聖旨裡即寫在一個命令類裡。如果不在一起呢?

建立新命令類:

 1     /// <summary>
 2     /// 讓李四原地待命的聖旨
 3     /// </summary>
 4     public class ConcreteCommand2 : Command
 5     {
 6         //建立執行人的參照
 7         private readonly Receiver_LiSi _Receiver_LiSi;
 8 
 9         //將執行人注入到命令類中。
10         public ConcreteCommand2(Receiver_LiSi receiver_LiSi)
11         {
12             this._Receiver_LiSi = receiver_LiSi;
13         }
14 
15         //具體命令
16         public override void Execute()
17         {
18             //讓李四原地待命
19             this._Receiver_LiSi.Behavior2();
20         }
21     }

使用者端呼叫:

 1     /// <summary>
 2     /// 使用者端
 3     /// </summary>
 4     class Client
 5     {
 6         static void Main(string[] args)
 7         {
 8             //首先建立接收人,即張三和李四。
 9             Receiver_ZhangSan receiver_ZhangSan = new Receiver_ZhangSan();
10             Receiver_LiSi receiver_LiSi = new Receiver_LiSi();
11 
12             //然後建立命令類,即聖旨1
13             Command command1 = new ConcreteCommand1(receiver_ZhangSan);
14             //然後建立命令類,即聖旨2
15             Command command2 = new ConcreteCommand2(receiver_LiSi);
16 
17             //其次建立呼叫者,即宦官。
18             Invoker invoker = new Invoker();
19             invoker.command = command1;
20             invoker.Reading();
21 
22             invoker.command = command2;
23             invoker.Reading();
24 
25             Console.WriteLine("\r\n");
26 
27             Console.ReadKey();
28         }
29     }

   另一種方式,可以在Invoker裡申明陣列儲存命令依次執行。

 1     /// <summary>
 2     /// 呼叫者類,命令下達者,即宦官。
 3     /// </summary>
 4     public class InvokerList
 5     {
 6         public List<Command> commands;
 7         public void AddCommand(Command command)
 8         {
 9             if (commands == null)
10             {
11                 commands = new List<Command>();
12             }
13             commands.Add(command);
14         }
15 
16         //下達命令,即宣讀聖旨
17         public void Reading()
18         {
19             if (commands != null)
20             {
21                 foreach (Command item in commands)
22                 {
23                     item.Execute();
24                 }
25             }
26         }
27     }

使用者端呼叫:

 1     /// <summary>
 2     /// 使用者端
 3     /// </summary>
 4     class Client
 5     {
 6         static void Main(string[] args)
 7         {
 8             //首先建立接收人,即張三和李四。
 9             Receiver_ZhangSan receiver_ZhangSan = new Receiver_ZhangSan();
10             Receiver_LiSi receiver_LiSi = new Receiver_LiSi();
11 
12             //然後建立命令類,即聖旨1
13             Command command1 = new ConcreteCommand1(receiver_ZhangSan);
14             //然後建立命令類,即聖旨2
15             Command command2 = new ConcreteCommand2(receiver_LiSi);
16 
17             //其次建立呼叫者,即宦官。
18             InvokerList invoker = new InvokerList();
19             invoker.AddCommand(command1);
20             invoker.AddCommand(command2);
21             invoker.Reading();
22 
23             Console.WriteLine("\r\n");
24 
25             Console.ReadKey();
26         }
27     }

從以上事例中不難看出,命令的呼叫者即Invoker類無需知道執行人和具體執行什麼內容,更換命令也很方便,只需要改變注入的命令類就可以了。

但也可以從範例中看出,新增一個命令就需要新增一個命令類,這可能會導致某些系統存在過多的命令類,會影響使用。

  讀後思考:書讀百遍其義自見,讀不夠百遍,那不如自己動手寫寫,不如就拿遙控控制不同電器為例子。

  友情提示:
      建立一盞燈
      建立一個命令,該命令控制繼承自抽象類,執行燈的一些操作,所以需要將建立好的燈注入到命令物件裡。
      建立一個遙控,將遙控的某一個按鈕繫結上該命令。可以是引數注入,也可以是屬性賦值。
      具體執行就是執行遙控的函數,然後傳遞到命令類 最後才是具體燈的操作。

      關鍵點就是燈跟命令進行繫結,方式就是在命令裡宣告一個燈的參照,然後引數注入等方式進行範例化。
      然後命令再跟遙控進行繫結,方式相同,在遙控裡宣告一個命令的參照,然後引數注入等方式進行範例化。
      最後實行執行操作的是遙控。

      為什麼要將命令抽象化,因為要跟遙控繫結,遙控只是執行命令,具體是什麼樣的命令,就交給命令本身去決定了。
      還有一個問題就是,有多少個電器 就需要多少個命令 。那其實可以合併成一個。大家可以動手試試。

總結:

  將一個請求封裝為一個物件,從而使你可用不同的請求對客戶進行引數化。適用於需要將請求呼叫者和請求接收者解耦,使得呼叫者和接收者不直接互動。

  命令模式的實現要點在於把某個具體的命令抽象化為具體的命令類,並通過加入命令請求者角色來實現將命令傳送者對命令執行者的依賴分割開。

  命令模式的目的是解除命令發出者和接收者之間的緊密耦合關係,使二者相對獨立,有利於程式的並行開發和程式碼的維護。

  命令模式的核心思想是將請求封裝為一個物件,將其作為命令發起者和接收者的中介,而抽象出來的命令物件又使得能夠對一系列請求進行操作,如對請求進行排隊,記錄請求紀錄檔以及支援可復原的操作等。