命令模式 Command 行為型 設計模式(十八)

来源:https://www.cnblogs.com/noteless/archive/2018/12/11/10102153.html
-Advertisement-
Play Games

命令模式是行為型設計模式,本文對命令模式Command進行了簡單介紹,深入的分析了命令模式的意圖,以及演化邏輯,並且給出了命令模式的Java版示例,理解命令模式有利於理解面向對象的編程思想,一切皆是對象,方法調用也是一種對象。 ...


命令模式(Command) image_5c0f5d4c_1248 請分析上圖中這條命令的涉及到的角色以及執行過程,一種可能的理解方式是這樣子的: 涉及角色為:大狗子和大狗子他媽 過程為:大狗子他媽角色 調用 大狗子的“回家吃飯”方法

引子

package command.origin;
public class BigDog {
public void goHomeForDinner() {
System.out.println("回家吃飯");
}
}
package command.origin;

public class BigDogMother {
public static void main(String[] args) {
BigDog bigDog = new BigDog();
bigDog.goHomeForDinner();
}
}
BigDog類擁有回家吃飯方法goHomeForDinner BigDogMother作為客戶端調用BigDog的回家吃飯方法,完成了“大狗子回家吃飯”這個請求 上面的示例中,通過對命令執行者的方法調用,完成了命令的下發,命令調用者與命令執行者之間是緊密耦合的 我們是否可以考慮換一種思維方式,將“你媽喊你回家吃飯”這一命令封裝成為一個對象? 不再是大狗子他媽調用大狗子的回家吃飯方法 而是大狗子他媽下發了一個命令,命令的內容是“大狗子回家吃飯” 接下來是命令的執行 這樣的話,“命令”就不再是一種方法調用了,在大狗子媽和大狗子之間多了一個環節---“命令”   看下代碼演變 BigDog 沒有變化 新增加了命令類Command  使用對象的接受者BigDog 進行初始化 命令的execute方法內部調用接受者BigDog的方法 BigDogMother中下發了三個命令 然後逐個執行這三個命令
package command.origin;
public class BigDog {
public void goHomeForDinner() {
System.out.println("回家吃飯");
}
}
package command.origin;
public class Command {
private BigDog bigDog;
Command(BigDog bigDog) {
this.bigDog = bigDog;
}
public void execute() {
bigDog.goHomeForDinner();
}
}
package command.origin;
public class BigDogMother {
public static void main(String[] args) {
BigDog bigDog = new BigDog();
Command command1 = new Command(bigDog);
Command command2 = new Command(bigDog);
Command command3 = new Command(bigDog);

command1.execute();
command2.execute();
command3.execute();
}
}
從上面的代碼示例中看到,通過對“請求”也就是“方法調用”的封裝,將請求轉變成了一個個的命令對象  命令對象本身內部封裝了一個命令的執行者 好處是:命令可以進行保存傳遞了,命令發出者與命令執行者之間完成瞭解耦,命令發出者甚至不知道具體的執行者到底是誰 而且執行的過程也更加清晰了

意圖

將一個請求封裝為一個對象,從而使可用不同的請求對客戶進行參數化; 對請求排隊或者記錄請求日誌,以及支持可撤銷的操作。 別名 行為Action或者事物Transaction 命令模式就是將方法調用這種命令行為或者說請求 進一步的抽象,封裝為一個對象

結構

上面的“大狗子你媽喊你回家吃飯”的例子只是展示了對於“命令”的一個封裝。只是命令模式的一部分。 下麵看下命令模式完整的結構 image_5c0f5d4c_60a2 命令角色Command 聲明瞭一個給所有具體命令類的抽象介面 做為抽象角色,通常是介面或者實現類 具體命令角色ConcreteCommand
定義一個接受者和行為之間的弱耦合關係,實現execute()方法
負責調用命令接受者的響相應操作
請求者角色Invoker 負責調用命令對象執行命令,相關的方法叫做行動action方法 接受者角色Receiver 負責具體實施和執行一個請求,任何一個類都可以成為接收者   Command角色封裝了命令接收者並且內部的執行方法調用命令接收者的方法 也就是一般形如: Command(Receiver receiver){ ...... execute(){ receiver.action(); ...   而Invoker角色接收Command,調用Command的execute方法   通過將“命令”這一行為抽象封裝,命令的執行不再是請求者調用被請求者的方法這種強關聯 ,而是可以進行分離 分離後,這一命令就可以像普通的對象一樣進行參數傳遞等

結構代碼示例

command角色
package command;
public interface Command {
void execute();
}

ConcreateCommand角色
內部擁有命令接收者,內部擁有execute方法

package command;
public class ConcreateCommand implements Command {
private Receiver receiver;
ConcreateCommand(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.action();
}
}
  Receiver命令接收者,實際執行命令的角色
package command;

public class Receiver {
public void action(){
  System.out.println("command receiver do sth....");
 }
}
命令請求角色Invoker 用於處理命令,調用命令角色執行命令
package command;
public class Invoker {
private Command command;
Invoker(Command command){
this.command = command;
}
void action(){
command.execute();
}
}
客戶端角色
package command;
public class Client {
public static void main(String[] args){
Receiver receiver = new Receiver();
Command command = new ConcreateCommand(receiver);
Invoker invoker = new Invoker(command);
invoker.action();
}
}
image_5c0f5d4c_13f0   在客戶端角色的測試代碼中,我們創建了一個命令,指定了接收者(實際執行者) 然後將命令傳遞給命令請求調用者 雖然最終命令的接收者為receiver,但是很明顯如果這個Command是作為參數傳遞進來的 Client照樣能夠運行,他只需要藉助於Invoker執行命令即可   命令模式關鍵在於:引入命令類對方法調用這一行為進行封裝 命令類使的命令發送者與接收者解耦,命令請求者通過命令類來執行命令接收者的方法 而不在是直接請求命名接收者

代碼示例

假設電視機只有三個操作:開機open 關機close和換台change channel。 用戶通過遙控器對電視機進行操作。   電視機本身是命令接收者 Receiver 遙控器是請求者角色Invoker 用戶是客戶端角色Client   需要將用戶通過遙控器下發命令的行為抽象為命令類Command Command有開機命令 關機命令和換台命令 命令的執行需要藉助於命令接收者 Invoker 調用Command的開機命令 關機命令和換台命令   電視類  Tv
package command.tv;

public class Tv {
public void turnOn(){
System.out.println("打開電視");
}

public void turnOff(){
System.out.println("關閉電視");
}
public void changeChannel(){
System.out.println("換台了");
}
}
Command介面
package command.tv;
public interface Command {
void execute();
}
三個具體的命令類 內部都保留著執行者,execute方法調用他們的對應方法
package command.tv;
 
public class OpenCommand implements Command {
 
private Tv myTv;
 
OpenCommand(Tv myTv) {
this.myTv = myTv;
}
 
@Override
public void execute() {
myTv.turnOn();
}
}
package command.tv;
 
public class CloseCommand implements Command {
 
private Tv myTv;
 
CloseCommand(Tv myTv) {
this.myTv = myTv;
}
 
@Override
public void execute() {
myTv.turnOff();
}
}
package command.tv;
 
public class ChangeChannelCommand implements Command {
 
private Tv myTv;
 
ChangeChannelCommand(Tv myTv) {
this.myTv = myTv;
}
 
@Override
public void execute() {
myTv.changeChannel();
}
}
遙控器Controller 擁有三個命令
package command.tv;
public class Controller {
private Command openCommand = null;
private Command closeCommand = null;
private Command changeChannelCommand = null;

public Controller(Command on, Command off, Command change) {
openCommand = on;
closeCommand = off;
changeChannelCommand = change;
}
 
public void turnOn() {
openCommand.execute();
}
 
public void turnOff() {
closeCommand.execute();
}
 
public void changeChannel() {
changeChannelCommand.execute();
}
}
用戶類User 
package command.tv;
public class User {
public static void main(String[] args) {
Tv myTv = new Tv();
OpenCommand openCommand = new OpenCommand(myTv);
CloseCommand closeCommand = new CloseCommand(myTv);
ChangeChannelCommand changeChannelCommand = new ChangeChannelCommand(myTv);
Controller controller = new Controller(openCommand, closeCommand, changeChannelCommand);
controller.turnOn();
controller.turnOff();
controller.changeChannel();
}
}

 

image_5c0f5d4c_25a0 以上示例將電視機的三種功能開機、關機、換台 抽象為三種命令 一個遙控器在初始化之後,就可以擁有開機、關機、換台的功能,但是卻完全不知道底層的實際工作的電視。  

命令請求記錄

一旦將“發起請求”這一行為進行抽象封裝為命令對象 那麼“命令”也就具有了一般對象的基本特性,比如,作為參數傳遞 比如使用容器存放進行存放 比如定義一個ArrayList  用於保存命令 ArrayList<Command> commands = new ArrayList<Command>(); 這就形成了一個隊列 你可以動態的向隊列中增加命令,也可以從隊列中移除命令 你還可以將這個隊列保存起來,批處理的執行或者定時每天的去執行 你還可以將這些命令請求持久化到文件中,因為這些命令、請求 也不過就是一個個的對象而已

請求命令隊列

既然可以使用容器存放命令對象,我們可以實現一個命令隊列,對命令進行批處理 新增加一個CommandQueue類,內部使用ArrayList存儲命令 execute()方法,將內部的請求命令隊列全部執行
package command;
import java.util.ArrayList;
 
public class CommandQueue {
 
private ArrayList<Command> commands = new ArrayList<Command>();
 
public void addCommand(Command command) {
commands.add(command);
}
 
public void removeCommand(Command command) {
commands.remove(command);
}
 
//執行隊列內所有命令
public void execute() {
for (Object command : commands) {
((Command) command).execute();
}
}
}
同時調整Invoker角色,使之可以獲得請求命令隊列,並且執行命令請求隊列的方法
package command;
public class Invoker {
private Command command;
Invoker(Command command) {
this.command = command;
}
void action() {
command.execute();
}
//新增加命令隊列
private CommandQueue commandQueue;
public Invoker(CommandQueue commandQueue) {
this.commandQueue = commandQueue;
}
/*
* 新增加隊列批處理方法*/
public void batchAction() {
commandQueue.execute();
}
}
從上面的示意代碼可以看得出來,請求隊列的關鍵就是命令類 一旦創建了命令類,就解除了命令請求者與命令接收者之間耦合,就可以把命令當做一個普通對象進行處理,調用他們的execute()執行方法   所謂請求隊列不就是使用容器把命令對象保存起來,然後調用他們的execute方法嘛 所以說,命令請求的對象化,可以實現對請求排隊或者記錄請求日誌的目的,就是命令對象的隊列

巨集命令

電腦科學里的巨集(Macro),是一種批量批處理的稱謂 一旦請求命令"對象化",就可以進行保存 上面的請求隊列就是如此,保存起來就可以實現批處理的功能,這就是命令模式的巨集命令

撤銷操作

在上面的例子中,我們沒有涉及到撤銷操作 命令模式如何完成“撤銷”這一行為呢? 命令是對於請求這一行為的封裝抽象,每種ConcreteCommand都對應者接收者一種具體的行為方式 所以想要能夠有撤銷的行為,命令接收者(最終的執行者)必然需要有這樣一個功能 如果Receiver提供了一個rollback方法 也就是說如果一個receiver有兩個方法,action()和rollback() 當執行action方法後,調用rollback可以將操作進行回滾 那麼,我們就可以給Command增加一個方法,recover() 用於調用receiver 的rollback方法 這樣一個命令對象就有了兩種行為,執行execute和恢復recover 如果我們在每次的命令執行後,將所有的 執行過的 命令保存起來 當需要回滾時,只需要逐個(或者按照執行的相反順序)執行命令對象的recover方法即可 這就很自然的完成了命令的撤銷行為,而且還可以批量進行撤銷 命令模式的撤銷操作依賴於命令接收者本身的撤銷行為,如果命令接收者本身不具備此類方法顯然沒辦法撤銷 另外就是依賴對執行過的命令的記錄

使用場景

對於“大狗子你媽喊你回家吃飯”的例子,我想你也會覺得大狗子媽直接調用大狗子的方法就好了 脫褲子放屁,抽象出來一個命令對象有什麼用呢?   對於簡單的方法調用,個人也認為是自找麻煩 命令模式是有其使用場景以及特點的,並不是說不分青紅皂白的將請求處理都轉換為命令對象   到底什麼情況需要使用命令模式? 通過上面的分析,如果你希望將請求進行排隊處理,或者請求日誌的記錄 那麼你就很可能需要命令模式,只有將請求轉換為命令對象,這些行為才更易於實現   如果系統希望支持撤銷操作 通過請求的對象化可以方便的將命令的執行過程記錄下來,就下來之後,就形成了“操作記錄” 擁有了操作記錄,如果有撤銷方法,就能夠執行回滾撤銷   如果希望命令能夠被保存起來組成巨集命令,重覆執行或者定時執行等,就可以使用命令模式   如果希望將請求的調用者和請求的執行者進行解耦,使得請求的調用者和執行者並不直接接觸 命令對象封裝了命令的接收者,請求者只關註命令對象,根本不知道命令的接收者   如果希望請求具有更長的生命周期,普通方法調用,命令發出者和命令執行者具有同樣的生命周期 命令模式下,命令對象封裝了請求,完成了命令發出者與命令接收者的解耦 命令對象創建後,只依賴命令接收者的執行,只要命令接收者存在,就仍舊可以執行,但是命令發出者可以消亡   總之命令模式的特點以及解決的問題,也正是他適用的場景 這一點在其他模式上也一樣 特點以及解決的問題,也正是他適用的場景,適用場景也正是它能解決的問題

總結

命令模式中對於場景中命令的提取,始終要註意它的核心“對接收者行為的命令抽象” 比如,電視作為命令接收者,開機,關機,換台是他自身固有的方法屬性,你的命令也就只能是與之對應的開機、關機、換台 你不能打游戲,即使你能打游戲,電視也不會讓你打游戲 這是具體的命令對象ConcreteCommand的設計思路   Command提供抽象的execute方法,所有的命令都是這個方法 調用者只需要執行Command的execute方法即可,不關註到底是什麼命令,命令接收者是誰 如果命令的接收者有撤銷的功能,命令對象就可以也同樣支持撤銷操作 關於如何抽取命令只需要記住: 命令模式中的命令對象是請求的封裝,請求基本就是方法調用,方法調用就是需要方法的執行者,也就是命令的接收者有對應行為的方法   請求者和接收者通過命令對象進行解耦,降低了系統的耦合度 命令的請求者Invoker與命令的接收者Receiver通過中間的Command進行連接,Command中的協議都是execute方法 所以,如果新增加命令,命令的請求者Invoker完全不需要做任何更改,他仍舊是接收一個Command,然後調用他的execute方法 具有良好的擴展性,滿足開閉原則 image_5c0f5d4c_5994   回到剛纔說的,具體的命令對象ConcreteCommand的設計思路 需要與命令接收者的行為進行對應 也就是針對每一個對請求接收者的調用操作,都需要設計一個具體命令類,可能會出現大量的命令類 有一句話說得好,“殺雞焉用宰牛刀”,所以使用命令模式一定要註意場景 以免被別人說脫褲子放屁,為了用設計模式而用設計模式....  原文地址:命令模式 Command 行為型 設計模式(十八)
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 利用canvas將網頁元素生成圖片並保存在本地 首先引入三個文件: > 註:createElementNS() 方法可創建帶有指定命名空間的元素節點。 createElementNS(ns,name)> createElementNS() 方法與 createElement() 方法相似,只是它創建 ...
  • 最近為了準備新工作重新摸出了SSM框架,同時從0學習了JQuery,終於用一周做完了一個包括增刪改查的模塊(主要是屬性太多了,其中一個類50+,複製粘貼耗時)。 從中特意記下了幾個遇到的問題,總結一下。 1. 執行網頁js代碼時, 出現$ is not defined錯誤。 原因:未引入JQuery ...
  • main.js main.vue pythod <! 自定義圖標存在時顯示 <! 自定義圖標不存在時根據type顯示圖標 <! 用戶設置的message的參數為字元串時,顯示字元串 {{ message }} <! 用戶設置的message的參數為VNode時,在此處顯示 <! 當用戶設置的關閉按鈕 ...
  • js實現獲取當前時間是本月第幾周和年的第幾周的方法 獲取本月第幾周的方法: 結果: 獲取年的第幾周的方法: ...
  • 數據的重要性我們大家都知道,就算再小的項目中都可能使用幾個圖表展示,我最近在做項目的過程中也是需要用到圖表,最後選擇了echarts 圖表庫,為什麼選擇 echarts,第一:簡單上手容易,第二:它幾乎可以滿足我們所有的開發需要,第三:echarts 應該是國內做的最好的可視化庫之一了。 廢話不多說 ...
  • $(function(){ //適配本地和測試環境 var host = "http://api.bch.xuemao.com", xmhost = 'http://dev.www.xuemao.com'; var winurl = (window.location.href).split('/')... ...
  • HTML的Encode(轉碼)和解碼(Decode)在平時的開發中也是經常要處理的,在這裡總結了使用javascript處理HTML的Encode(轉碼)和解碼(Decode)的常用方式 一、用瀏覽器內部轉換器實現轉換 1.1.用瀏覽器內部轉換器實現html轉碼 首先動態創建一個容器標簽元素,如DI ...
  • 前端開發過程中有時需自己手寫排序方法 一般想到數字的字元串排序方法 我們會用到 var newArr = arr. sort(function(a,b){return a - b})來進行排序 但除此之外我們會需要進行一些字元串的排序 此時我們可以用到 var newArr = arr.sort(f ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...