1. 程式人生 > >設計模式的征途—19.命令(Command)模式

設計模式的征途—19.命令(Command)模式

在生活中,我們裝修新房的最後幾道工序之一是安裝插座和開關,通過開關可以控制一些電器的開啟和關閉,例如電燈或換氣扇。在購買開關時,使用者並不知道它將來到底用於控制什麼電器,也就是說,開關與電燈、換氣扇並無直接關係,一個開關在安裝之後可能用來控制電燈,也可能用來控制換氣扇或者其他電器裝置。相同的開關可以通過不同的電線來控制不同的電器,如下圖所示。

在軟體開發中也存在很多與開關和電器類似的請求傳送者和接受者物件,例如一個按鈕,它可能是一個“關閉視窗”請求的傳送者,而按鈕點選事件處理類則是該請求的接受者。為了降低系統的耦合度,將請求的傳送者和接收者解耦,可以使用一種被稱為命令模式的設計模式來設計系統。

命令模式(Command) 學習難度:★★★☆☆ 使用頻率:★★★★☆

一、自定義功能按鍵的設計

1.1 需求背景

M公司開發人員為公司內部OA系統開發了一個桌面版應用程式,該應用程式為使用者提供了一系列自定義功能鍵,使用者可以通過這些功能鍵來實現一些快捷操作。M公司開發人員通過分析,發現不同的使用者可能會有不同的使用習慣,在設定功能鍵的時候每個人都有自己的喜好,例如有的人喜歡將第一個功能鍵設定為“開啟幫助文件”,有的人則喜歡將該功能鍵設定為“最小化至托盤”。為了讓使用者能夠靈活地進行功能鍵的設定,開發人員提供了一個“功能鍵設定”視窗,如下圖所示。

通過上圖的介面,使用者就可以將功能鍵和相應功能繫結在一起,還可以根據需求來修改功能鍵的設定,而且系統在未來可能還會增加一些新的功能或功能鍵。

1.2 初始設計

  M公司開發人員打算使用如下code來實現功能鍵與功能處理類之間的呼叫關係:

    public class FunctionButton
    {
        private HelpHandler handler;

        public void OnClick()
        {
            handler = new HelpHandler();
            handler.Display();
        }
    }

  在上述程式碼中,功能按鍵類FunctionButton充當請求的傳送者,幫助文件處理類HelpHandler則充當請求的接收者,在傳送者FunctionButton的OnClick()方法中將呼叫接收者HelpHandler的Display()方法。顯然,如果直接使用上述程式碼,將會有以下幾個問題:

  (1)請求傳送者和請求接收者存在直接呼叫 => 耦合度太高!更換請求接收者必須修改傳送者的原始碼!

  (2)FunctionButton類在設計和實現時功能已被固定 => 增加新的請求接收者要麼修改FunctionButton類要麼新增一個新的請求接收者類

  (3)使用者無法按照自己的需要來設定某個功能鍵的功能 => 無法在修改原始碼情況下更換功能,缺乏靈活性!

二、命令模式概述

2.1 命令模式簡介

命令(Command)模式:將一個請求封裝為一個物件,從而可以用不同的請求對客戶進行引數化;對請求排隊或者記錄請求日誌,以及支援可撤銷的操作。命令模式是一種物件行為型模式,其別名為動作(Action)模式或事物(Transaction)模式。

2.2 命令模式結構

  命令模式的核心在於引入了命令類,通過命令類來降低請求傳送者和接收者的耦合度,請求傳送者只需要指定一個命令物件,再通過命令物件來呼叫請求接收者的處理方法,其結構如下圖所示。

  其中,包含以下幾個角色:

  (1)Command(抽象命令類):一個抽象類或介面,聲明瞭執行請求的Execute()方法,通過這些方法可以呼叫請求接收者的相關操作。

  (2)ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了抽象命令類中宣告的方法。在實現Execute()方法時,將呼叫接收者物件的相關操作(Action)。

  (3)Invoker(呼叫者):請求傳送者,通過命令物件來執行請求。

  (4)Receiver(接收者):接收者執行與請求相關的操作,它具體實現對請求的業務處理。

  命令模式的本質在於:對請求進行封裝,一個請求對應一個命令,將發出命令的責任和執行命令的責任分割開,使得請求的一方不必瞭解接收請求的一方的介面,更不必知道請求如何被接收、操作是否被執行、何時被執行,以及是怎麼被執行的

三、重構自定義功能鍵的設計

3.1 重構後的設計

  其中,FBSettingWindow是“功能鍵設定”介面類,FunctionButton充當呼叫者,Command充當抽象命令類,MinimizeCommand、HelpCommand充當具體命令類,WindowHandler和HelpHandler充當請求接收者。

3.2 具體程式碼實現

  (1)功能鍵設定視窗

    /// <summary>
    /// 功能鍵設定視窗類
    /// </summary>
    public class FBSettingWindow
    {
        // 視窗標題
        public string Title { get; set; }
        // 所有功能鍵集合
        private IList<FunctionButton> functionButtonList = new List<FunctionButton>();

        public FBSettingWindow(string title)
        {
            this.Title = title;
        }

        public void AddFunctionButton(FunctionButton fb)
        {
            functionButtonList.Add(fb);
        }

        public void RemoveFunctionButton(FunctionButton fb)
        {
            functionButtonList.Remove(fb);
        }

        // 顯示視窗及功能鍵
        public void Display()
        {
            Console.WriteLine("顯示視窗:{0}", this.Title);
            Console.WriteLine("顯示功能鍵:");

            foreach (var fb in functionButtonList)
            {
                Console.WriteLine(fb.Name);
            }

            Console.WriteLine("------------------------------------------");
        }
    }

  (2)請求傳送者:FunctionButton

    /// <summary>
    /// 請求傳送者:功能鍵
    /// </summary>
    public class FunctionButton
    {
        // 功能鍵名稱
        public string Name { get; set; }
        // 維持一個抽象命令物件的引用
        private Command command;

        public FunctionButton(string name)
        {
            this.Name = name;
        }

        // 為功能鍵注入命令
        public void SetCommand(Command command)
        {
            this.command = command;
        }

        // 傳送請求的方法
        public void OnClick()
        {
            Console.WriteLine("點選功能鍵:");
            if (command != null)
            {
                command.Execute();
            }
        }
    }

  (3)抽象命令類:Command

    /// <summary>
    /// 抽象命令類
    /// </summary>
    public abstract class Command
    {
        public abstract void Execute();
    }

  (4)具體命令類:HelpCommand與MinimizeCommand

    /// <summary>
    /// 具體命令類:幫助命令
    /// </summary>
    public class HelpCommand : Command
    {
        private HelpHandler hander;

        public HelpCommand()
        {
            hander = new HelpHandler();
        }

        // 命令執行方法,將呼叫請求接受者的業務方法
        public override void Execute()
        {
            if (hander != null)
            {
                hander.Display();
            }
        }
    }

    /// <summary>
    /// 具體命令類:最小化命令
    /// </summary>
    public class MinimizeCommand : Command
    {
        private WindowHandler handler;

        public MinimizeCommand()
        {
            handler = new WindowHandler();
        }

        // 命令執行方法,將呼叫請求接受者的業務方法
        public override void Execute()
        {
            if (handler != null)
            {
                handler.Minimize();
            }
        }
    }

  (5)請求接收者:WindowHandler和HelpHandler

    /// <summary>
    /// 請求接受者:幫助文件處理類
    /// </summary>
    public class WindowHandler
    {
        public void Minimize()
        {
            Console.WriteLine("正在最小化視窗至托盤...");
        }
    }

    /// <summary>
    /// 請求接受者:幫助文件處理類
    /// </summary>
    public class HelpHandler
    {
        public void Display()
        {
            Console.WriteLine("正在顯示幫助文件...");
        }
    }

  (6)客戶端測試

    public class Program
    {
        public static void Main(string[] args)
        {
            // Step1.模擬顯示功能鍵設定視窗
            FBSettingWindow window = new FBSettingWindow("功能鍵設定視窗");

            // Step2.假如目前要設定兩個功能鍵
            FunctionButton buttonA = new FunctionButton("功能鍵A");
            FunctionButton buttonB = new FunctionButton("功能鍵B");

            // Step3.讀取配置檔案和反射生成具體命令物件
            Command commandA = (Command)AppConfigHelper.GetCommandAInstance();
            Command commandB = (Command)AppConfigHelper.GetCommandBInstance();

            // Step4.將命令注入功能鍵
            buttonA.SetCommand(commandA);
            buttonB.SetCommand(commandB);

            window.AddFunctionButton(buttonA);
            window.AddFunctionButton(buttonB);
            window.Display();

            // Step5.呼叫功能鍵的業務方法
            buttonA.OnClick();
            buttonB.OnClick();

            Console.ReadKey();
        }
    }

  這裡為了提高系統的靈活性,將具體命令類配置在了配置檔案中,並通過幫助類AppConfigHelper來讀取配置並反射生成物件。其中配置檔案設定如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="HelpCommand" value="Manulife.ChengDu.DesignPattern.Command.HelpCommand, Manulife.ChengDu.DesignPattern.Command" />
    <add key="MinimizeCommand" value="Manulife.ChengDu.DesignPattern.Command.MinimizeCommand, Manulife.ChengDu.DesignPattern.Command" />
  </appSettings>
</configuration>

  AppConfigHelper類的實現如下,這裡不再詳述。

    public class AppConfigHelper
    {
        public static string GetCommandAName()
        {
            string factoryName = null;
            try
            {
                factoryName = System.Configuration.ConfigurationManager.AppSettings["HelpCommand"];
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            return factoryName;
        }

        public static object GetCommandAInstance()
        {
            string assemblyName = AppConfigHelper.GetCommandAName();
            Type type = Type.GetType(assemblyName);

            var instance = Activator.CreateInstance(type);
            return instance;
        }

        public static string GetCommandBName()
        {
            string factoryName = null;
            try
            {
                factoryName = System.Configuration.ConfigurationManager.AppSettings["MinimizeCommand"];
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            return factoryName;
        }

        public static object GetCommandBInstance()
        {
            string assemblyName = AppConfigHelper.GetCommandBName();
            Type type = Type.GetType(assemblyName);

            var instance = Activator.CreateInstance(type);
            return instance;
        }
    }
View Code

  編譯後執行,輸出結果如下圖所示:

  

  此時,如果需要修改功能鍵,例如某個功能鍵可以實現“自動截圖”,只需要增加一個新的具體命令類,在該命令類與螢幕處理者(ScreenHandler)之間建立一個關聯關係,然後將該具體命令類的物件通過配置檔案注入到某個功能鍵即可,原有程式碼無需修改,符合開閉原則。

四、命令模式總結

4.1 主要優點

  (1)降低了系統的耦合度 => 請求傳送者與接受者不存在直接引用

  (2)方便地增加新的命令到系統中 => 無須修改原始碼,從而符合開閉原則

4.2 主要缺點

  使用命令模式可能會導致某些系統有過多的具體命令類。 => 因為針對每一個對請求接收者的呼叫操作都需要設計一個具體命令,因此在某些系統中可能需要提供大量的具體命令類。

4.3 應用場景

  系統需要將請求呼叫者和請求接收者解耦 => 那就快用命令模式吧騷年!

參考資料

  DesignPattern

  劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。

相關推薦

設計模式征途19.命令Command模式

在生活中,我們裝修新房的最後幾道工序之一是安裝插座和開關,通過開關可以控制一些電器的開啟和關閉,例如電燈或換氣扇。在購買開關時,使用者並不知道它將來到底用於控制什麼電器,也就是說,開關與電燈、換氣扇並無直接關係,一個開關在安裝之後可能用來控制電燈,也可能用來控制換氣扇或者其他電器裝置。相同的開關可以通過不同的

設計模式命令Command模式

box exe see 再次 clas sed hist 本質 private 設計模式:命令(Command)模式 一、前言 命令也是類,將命令作為一個類來保存,當要使用的時候可以直接拿來使用,比如腳本語言寫出的腳本,只需要一個命令就能執行得到我們想要的需要操作很長時

設計模式命令Command模式

     Command定義如下: 將來自客戶端的請求傳入一個物件,無需瞭解這個請求啟用的動作或有關接受這個請求的處理細節。是不是有點迷糊。不知其說的是啥。哈哈。彆著急下面聽我慢慢到來。本人覺得,命令模式就是把一些具體的命令封裝成一此具體的類,這此類實現同一個介面或者是抽象類

C# 設計模式-命令Command

將一個請求封裝為一個物件,達到用不同請求對客戶進行引數化。呼叫者通過某個命令來呼叫接收者執行相關操作,減弱呼叫者與接收者的耦合度。 主要組成: Command-命令父類或介面 ConcreteComm

設計模式征途—5.原型Prototype模式

pla width 共享 太多的 isp text 一模一樣 軟件 集合 相信大多數的人都看過《西遊記》,對孫悟空拔毛變出小猴子的故事情節應該都很熟悉。孫悟空可以用猴毛根據自己的形象復制出很多跟自己一模一樣的小猴兵出來,其實在設計模式中也有一個類似的模式,我們可以通過一個原

設計模式征途—16.訪問者Visitor模式

lose mar rtm image 3.1 conf 系統 .get 封裝性 在患者就醫時,醫生會根據病情開具處方單,很多醫院都會存在以下這個流程:劃價人員拿到處方單之後根據藥品名稱和數量計算總價,而藥房工作人員根據藥品名稱和數量準備藥品,如下圖所示。 在軟件開發中

設計模式征途—18.策略Strategy模式

滿足 應用 基礎 blog title pla 生成 display 多個 俗話說條條大路通羅馬,很多情況下實現某個目標地途徑都不只一條。在軟件開發中,也會時常遇到這樣的情況,實現某一個功能有多條途徑,每一條途徑都對應一種算法。此時,可以使用一種設計模式來實現靈活地選擇解決

設計模式征途—20.備忘錄Memento模式

行為 修煉之道 mda 3.2 ima 位置 pri 捕獲 spl 相信每個人都有後悔的時候,但是人生並無後悔藥,有些錯誤一旦發生就無法再挽回,有些事一旦錯過就不會再重來,有些話一旦說出口也就不可能再收回,這就是人生。為了不讓自己後悔,我們總是需要三思而後行。這裏我們要學習

設計模式征途—10.裝飾Decorator模式

雖然目前房價依舊很高,就連我所在的成都郊區(非中心城區)的房價均價都早已破萬,但卻還是阻擋不了大家對新房的渴望和買房的熱情。如果大家買的是清水房,那麼無疑還有一項艱鉅的任務在等著大家,那就是裝修。對新房的裝修並沒有改變房屋用於居住的本質,但它可以讓房子變得更加漂亮和溫馨以及更加實用。在軟體設計中,也有一種類似

設計模式征途—11.外觀Facade模式

在軟體開發中,有時候為了完成一項較為複雜的功能,一個類需要和多個其他業務類互動,而這些需要互動的業務類經常會作為一個完整的整體出現,由於涉及的類比較多,導致使用時程式碼較為複雜,此時,特別需要一個類似服務員一樣的角色,由他來負責和多個業務類進行互動,而使用這些業務類的類只需要和該類進行互動即可。外觀模式通過引

設計模式征途—13.代理Proxy模式

所謂代購,簡單說來就是找人幫忙購買所需要的商品。代購分為兩種型別,一種是因為在當地買不到某件商品,又或者是因為當地這件商品的價格比其他地區的貴,因此託人在其他地區甚至國外購買該商品,然後通過快遞發貨或直接攜帶回來。另一種則是消費者對想要購買的商品相關資訊的缺乏,自己無法確定其實際價值,因此只好委託中介講價或購

設計模式征途—23.直譯器Interpreter模式

雖然目前計算機程式語言有好幾百種,但有時人們還是希望用一些簡單的語言來實現特定的操作,只需要向計算機輸入一個句子或檔案,就能按照預定的文法規則來對句子或檔案進行解釋。例如,我們想要只輸入一個加法/減法表示式,它就能夠計算出表示式結果。例如輸入“1+2+3-4+1”時,將輸出計算結果為3。像C++,Java或C

設計模式征途—7.介面卡Adapter模式

在現實生活中,我們的膝上型電腦的工作電壓大多數都是20V,而我國的家庭用電是220V,如何讓20V的膝上型電腦能夠工作在220V的電壓下工作?答案:引入一個電源介面卡,俗稱變壓器,有了這個電源介面卡,生活用電和膝上型電腦即可相容。 在軟體開發中,有時候也會存在這種不相容的情況,我們也可以像電源介面卡一樣引入

設計模式:裝飾器Decorator模式

讓我 分享圖片 底部 .com 一件事 輸出 PE 新的 int 設計模式:裝飾器(Decorator)模式 一、前言 裝飾器模式也是一種非常重要的模式,在Java以及程序設計中占據著重要的地位。比如Java的數據流處理,我們可能看到數據流經過不同的類的包裝和包裹,最

設計模式:觀察者Observer模式

image 強制轉換 trace vat PE sta obs observer -a 設計模式:觀察者(Observer)模式 一、前言 觀察者模式其實最好的名稱應該是“發布訂閱”模式,和我們現在大數據之中的發布訂閱方式比較類似,但是也有區別的地方,在上一個設計模式,

設計模式:享元FlyWeight模式

例子 清理 什麽 public == lean http 變量 -- 設計模式:享元(FlyWeight)模式 一、前言 享元(FlyWeight)模式顧名思義,既是輕量級的,原因就是享元,共享元素,這裏的元素指的是對象。如何共享對象,那就是在檢測對象產生的時候,如

Koffee設計模式學習之路 —— 模式學習總結思路

    這篇部落格沒有相關技術細節,僅作為自己對設計模式這個東西的一點感悟和以後設計模式系列部落格的一個寫作思路。     作為非科班出身,誤打誤撞進入程式設計的人,在上研究生期間對於程式的唯一要求就是:能用。彼時,不知道有面向物件,記憶體管理,多執行緒,

設計模式之裝飾者Decorator模式

首先來看一個場景,如圖: 工人分為很多種類,比如電工,管道工等等,同時又有A公司的電工,B公司的電工,A公司的管道工,B公司的管道工等等,那麼當有M個工種和N個公司的時候,就會有 M * N 個子類,這個繼承體系就會變得很龐大和複雜。那麼如何簡化呢,那麼

【java設計模式】之 代理Proxy模式

代理模式的核心作用就是通過代理,控制對物件的訪問。這跟實際中是一樣的,比如說明星都有經紀人,這就是一個代理,比如有人要找某明星拍戲,那麼首先處理這事的是他的經紀人,雖然拍戲需要自己拍,但是拍戲前後的一些必須要做的事等等,都由這個經紀人來處理。    在程式中也是如此,通過

有關模式窗體和無模式窗體的區別

最小化 調用方法 關閉 對話 一起 兩種模式 獲得 windows new   在客戶端和網頁的開發過程中,我們都會或多或少遇到過模式窗體和無(非)模式窗體(以下簡稱無模式窗體),   在傳統的Windows窗體開發過程中,對話框有兩種模式:模式窗體和無模式窗體。   模式