設計模式(五)~行爲模式(1)

行爲模式簡介

行爲模式用於描述程序在運行時複雜的流程控制,即描述多個類或對象之間怎樣相互協作共同完成單個對象都無法獨立完成的任務,涉及算法與對象間職責的分配。

行爲模式分爲類行爲模式對象行爲模式,前者採用繼承機制來在類間分派行爲,後者採用組合或聚合在對象間分配行爲。由於組合關係或聚合關係比繼承關係耦合度更低,滿足"合成複用原則",所以對象行爲模式比類行爲模式具有更大的靈活性。

行爲模式是GoF中最爲龐大的一類,它包含模版方法模式策略模式命令模式職責鏈模式狀態模式觀察者模式中介者模式迭代器模式訪問者模式備忘錄模式解釋器模式具體使用見下文。

模版方法模式(定義算法骨架和流程,子類實現可變部分)

定義

模版方法模式:定義一個操作中的算法骨架,而將算法的一些步驟延遲到子類中,使得子類可以不改變該算法結構的前提下重定義該算法的某些特定的步驟。它是一種類行爲模式。

特點

優點

  • 封裝不變部分,擴展可變部分。它把認爲是不變部分的算法封裝到父類中實現,而把可變部分算法由子類繼承實現,便於子類繼續擴展。
  • 它在父類中提取了公共的代碼部分,便於代碼的複用。
  • 部分方法由子類實現,因此子類可以通過擴展方式增加相應的功能,符合開閉原則

缺點

  • 對每個不同的實現都需要定義一個子類,會導致類的個數增加,系統更加龐大,設計也更加抽象。
  • 父類中的抽象方法由子類實現,子類執行的結果會影響到父類的結果,這導致一種反向的控制結構,它提高了代碼閱讀的難度。

結構

模版方法模式主要包含以下主要角色

  • 抽象類(Abstract Class):負責定義算法的輪廓和骨架。它由一個模版方法和若干個基本方法構成,這些方法定義如下:

    模版方法:定義了算法的骨架,按某種順序調用其包含的基本方法。

    • 基本方法:整個算法中的一個步驟,包含以下幾種:
      • 抽象方法:在抽象類中聲明,由具體子類實現。
      • 具體方法:在抽象類中已經實現,在具體子類中可以繼承或重寫它。
      • 鉤子方法:在抽象類中已經實現,包括用於判斷的邏輯方法和需要子類重寫的空方法兩種。
  • 具體子類(Concreate Class):實現抽象類中所定義的抽象方法和鉤子方法,它們是一個頂級邏輯的一個組成步驟。
    結構如下:
    在這裏插入圖片描述

實現

例如:我們一家人一天中包含:起牀、喫飯、做事情、看電視、睡覺幾個流程,其中起牀、喫飯、看電視、睡覺對每個人都是一樣的,但做事情因人不同而不同(爸爸上班、媽媽收拾家務、孩子上學),因此我們採用模版方法模式來實現

  1. 先定義一個抽象類,包含了一個模版方法,該方法包含了起牀、喫飯、做事情、睡覺幾個基本方法,其中有些方法的處理由於各人都一樣,所以在該抽象類中實現就可以了。

    /**
     *定義抽象類-家庭日常(包含一個模版方法+若干個基本方法)
     */
    public abstract class AbsFamilyDaily {
    
        /**
         * 模版方法(內部按照順序調用基本方法)
         */
        final public void templateMethod() {
            getUp();
            eatBreakfast();
            doSomething();
            watchingTV();
            sleep();
        }
    
        //具體方法-起牀
        protected void getUp() {
            System.out.println("07:00 起牀 ");
        }
        //具體方法-喫飯
        protected void eatBreakfast() {
            System.out.println("08:00 開始喫早餐 ");
        }
    
        //抽象方法-做事情,每個人不一樣由具體子類實現
        protected abstract void doSomething();
        
        //具體方法-看電視
        protected  void watchingTV(){
            System.out.println("20:00 看電視  ");
        }
     
        //具體方法-睡覺
        protected void sleep() {
            System.out.println("21:00 睡覺 ");
        }
    }
    
  2. 做事情每個人是不同的因此必須在具體子類中實現

    /**
     *具體子類-爸爸,繼承抽象類,實現做事方法
     */
    public class Father extends AbsFamilyDaily {
        @Override
        public void doSomething() {
            System.out.println("09:00 爸爸去上班了...");
        }
    }
    
    /**
     *具體子類-媽媽,繼承抽象類,實現做事方法
     */
    public class Mather extends AbsFamilyDaily {
        @Override
        public void doSomething() {
            System.out.println("09:30 媽媽開始做家務...");
        }
    }
    
    /**
     *具體子類-孩子,繼承抽象類,實現做事方法
     */
    public class Children extends AbsFamilyDaily {
        @Override
        public void doSomething() {
            System.out.println("09:10 寶寶去上學了...");
        }
    }
    
  3. 測試

    AbsFamilyDaily father = new Father();
    father.templateMethod();
    System.out.println("---------------");
    
    AbsFamilyDaily mather = new Mather();
    mather.templateMethod();
    System.out.println("---------------");
    
    AbsFamilyDaily children = new Children();
    children.templateMethod();
    
    //輸出結果
    07:00 起牀 
    08:00 開始喫早餐 
    09:00 爸爸去上班了...
    20:00 看電視  
    21:00 睡覺 
    ---------------
    07:00 起牀 
    08:00 開始喫早餐 
    09:30 媽媽開始做家務...
    20:00 看電視  
    21:00 睡覺 
    ---------------
    07:00 起牀 
    08:00 開始喫早餐 
    09:10 寶寶去上學了...
    20:00 看電視  
    21:00 睡覺 
    

應用場景

模版方法模式,適用於以下場景

  • 算法整體步驟很固定,但其中個別部分易變,這時候可以使用模版方法模式,將容易變動的部分抽象出來,供具體子類實現。
  • 多個子類存在公共行爲時,可以將其提取出來並集中到一個公共父類中以避免代碼重複。首先,要識別現有代碼中的不同之處,並且將不同之處分離爲新的操作。最後,用一個調用這些新的操作的模版方法來替換這些不同的代碼。
  • 當需要控制子類的擴展時,模版方法只在特定點調用鉤子操作,這樣就只允許在這些點進行擴展。

擴展

模版方法模式中,基本方法包含:抽象方法具體方法鉤子方法,正確使用鉤子方法可以使得子類控制父類的行爲。如下例子中,可以通過具體子類中重寫鉤子方法hookMethod()來改變抽象父類中的運行結果,結構如下:
在這裏插入圖片描述
同樣上面的案例,寶寶不想看電視,我們只需要在抽象類中增加一個抽象鉤子方法即可,具體子類實現鉤子方法來控制父類中的運行結果。

/**
 *抽象類-家庭日常(增加鉤子方法)
 */
public abstract class AbsFamilyDaily {

    /**
     * 模版方法(內部按照順序調用基本方法)
     */
    final public void templateMethod() {
        getUp();
        eatBreakfast();
        doSomething();
        if(hookMethod()){
            watchingTV(); 
        }
        sleep();
    }

    //具體方法-起牀
    protected void getUp() {
        System.out.println("07:00 起牀 ");
    }
    //具體方法-喫飯
    protected void eatBreakfast() {
        System.out.println("08:00 開始喫早餐 ");
    }

    //抽象方法-做事情,每個人不一樣由具體子類實現
    protected abstract void doSomething();
    
    //具體方法-看電視
    protected  void watchingTV(){
        System.out.println("20:00 看電視  ");
    }
    
    //具體方法-睡覺
    protected void sleep() {
        System.out.println("21:00 睡覺 ");
    }
    
    //抽象方法-鉤子,通過具體子類實現,改變父類執行邏輯
    protected abstract boolean hookMethod();
    
}
/**
 *具體子類-孩子實現抽象鉤子方法
 */
public class Children extends AbsFamilyDaily {
    @Override
    public void doSomething() {
        System.out.println("09:10 寶寶去上學了...");
    }

    //寶寶實現具體的鉤子方法-返回false不想看電視
    @Override
    protected boolean hookMethod() {
        return false;
    }
}
//同樣爸爸媽媽如看電視其具體子類中也實現該鉤子方法,返回true即可
//輸出結果:
07:00 起牀 
08:00 開始喫早餐 
09:00 爸爸去上班了...
20:00 看電視  
21:00 睡覺 
---------------
07:00 起牀 
08:00 開始喫早餐 
09:30 媽媽開始做家務...
20:00 看電視  
21:00 睡覺 
---------------
07:00 起牀 
08:00 開始喫早餐 
09:10 寶寶去上學了...
21:00 睡覺 

策略模式(定義一系列算法族,每個封裝起來,使其可以相互替換)

定義

策略模式:定義了一系列算法,並將每個算法封裝起來,使它們可以相互替換,且算法的變化並不會影響使用算法的客戶。策略模式屬於對象行爲模式,它通過對算法進行封裝,把使用算法的責任和算法的實現分割開來,並委派給不同的對象對這些算法進行管理。

但我感覺在基本的策略模式中,選擇所用具體實現的職責由客戶端對象承擔,並轉給策略模式的Context對象[DPE]。這本身並沒有解除客戶端需要選擇判斷的壓力,而策略模式與簡單工廠模式結合後,選擇具體實現的職責也可以由Context來承擔,這就最大化地減輕了客戶端的職責。

特點

優點

  • 避免使用多重條件語句,多重條件語句不利於維護。
  • 提供一系列可供重用的算法族,恰當使用繼承可以把算法族的公共代碼轉移到父類裏面,從而避免重複代碼。
  • 可提供相同行爲不同實現,客戶可依據不同時間或空間選擇。
  • 完美支持開閉原則,可以在不修改源代碼情況下,靈活增加新算法。
  • 把算法使用放到環境類中,而算法的實現放到具體策略類中,實現二者分離。

缺點

  • 客戶端必須理解所有策略算法的區別,以便適時選擇恰當的算法類。
  • 造成很多的策略類。

結構

策略模式是準備一組算法,並將這些算法封裝到一系列具體策略類裏面,作爲一個抽象策略類的子類。策略模式的重心不是如何實現算法,而是如何組織這些算法,從而讓程序結構更加靈活,具有更好維護性和擴展性,基本結構如下:

策略模式主要角色如下:

  • 抽象策略(Abstract Strategy)類:定義一個公共接口,各種不同的算法以不同的方式實現這個接口,環境角色使用這個接口調用不同的算法,一般使用接口或抽象類實現。
  • 具體策略(Concrete Strategy)類:實現了抽象策略定義的接口,提供具體的算法實現。
  • 環境(Context)類:持有一個策略類引用,最終給客戶端調用

結構如下:
在這裏插入圖片描述

實現

例如,我們通常在開發過程中,爲了安全性我們通常採用加密算法對我們的敏感字段進行加密處理,這裏加密的方式有很多中(RSA、3DES、AES)等等,因此我們通過使用加密算法來講解策略模式使用

  1. 定義一個抽象策略類,聲明公共接口加密

    public interface AbsEncrypt {
        //聲明公共接口-加密
        void encrypt();
    }
    
  2. 聲明具體策略類實現抽象接口中的方法

    //具體策略類-AES加密
    public class AESEncrypt implements AbsEncrypt {
        @Override
        public void encrypt() {
            System.out.println("您當前選擇的AES對稱加密算法");
        }
    }
    
    //具體策略類-DESede加密
    public class DESedeEncrypt implements AbsEncrypt{
        @Override
        public void encrypt() {
            System.out.println("您當前選擇的3DES對稱加密算法");
        }
    }
    
    //具體策略類-RSA加密
    public class RSAEncrypt implements AbsEncrypt {
        @Override
        public void encrypt() {
          System.out.println("您當前選擇的RSA非對稱加密算法");
        }
    }
    
  3. 定義環境類(持有一個策略類的引用,供客戶端調用)

    public class Context {
        //持有一個策略類的引用,供客戶端調用
        AbsEncrypt encrypt;
    
        public AbsEncrypt getEncrypt() {return encrypt;}
    
        public void setEncrypt(AbsEncrypt encrypt) {this.encrypt = encrypt;}
    
        public void encrypt(){encrypt.encrypt();}
    }
    
  4. 測試:客戶端調用

    Context context = new Context();
    AbsEncrypt aesEncrypt = new AESEncrypt();
    context.setEncrypt(aesEncrypt);
    context.encrypt();
    
    AbsEncrypt desedeEncrypt = new DESedeEncrypt();
    context.setEncrypt(desedeEncrypt);
    context.encrypt();
    
    AbsEncrypt rsaEncrypt = new RSAEncrypt();
    context.setEncrypt(rsaEncrypt);
    context.encrypt();
    //輸出結果
    您當前選擇的AES對稱加密算法
    您當前選擇的3DES對稱加密算法
    您當前選擇的RSA非對稱加密算法
    

應用場景

  • 一個系統需要動態的在幾種算法中選擇一種時,可將每個算法封裝到具體策略類中。
  • 一個類定義了多種行爲,並且這些行爲在這個類操作中以多個條件語句出現,可將每個條件分支移植到它們對應的具體策略類中以代替這些條件語句。
  • 系統中各算法彼此完全獨立,且要求對客戶隱藏具體到算法實現細節。
  • 多個類只區別在表現行爲不同,可以使用策略模式,在運行時動態的選擇要執行的行爲。

擴展

1. 策略工廠模式(策略模式+簡單工廠模式)

在一個使用策略模式的系統中,當存在的策略很多時,客戶端管理所有策略算法將變得很複雜,如果在環境類中使用策略工廠模式來管理這些策略類將大大減少客戶端的工作複雜度,其結構圖如下 所示。

在這裏插入圖片描述

代碼案例:

還是如上案例,加密算法使用策略工廠模式實現。

//抽象策略類
public interface AbsEncrypt {
    //聲明公共接口-加密
    void encrypt();
}
//具體策略類-AES加密
public class AESEncrypt implements AbsEncrypt {
    @Override
    public void encrypt() {
        System.out.println("您當前選擇的AES對稱加密算法");
    }
}
//具體策略類-RSA加密
public class RSAEncrypt implements AbsEncrypt {
    @Override
    public void encrypt() {
      System.out.println("您當前選擇的RSA非對稱加密算法");
    }
}
//具體策略類-DESede加密
public class DESedeEncrypt implements AbsEncrypt{
    @Override
    public void encrypt() {
        System.out.println("您當前選擇的3DES對稱加密算法");
    }
}
//策略工廠
public class StrategyFactory {

    HashMap<String,AbsEncrypt> map = new HashMap<>();
    public void put(String key,AbsEncrypt absEncrypt){
        map.put(key,absEncrypt);
    }
    protected AbsEncrypt get(String key){
       return map.get(key);
    }
    public void encrypt(String key){
        get(key).encrypt();
    }
}

測試:輸出

//擴展,策略工廠模式
StrategyFactory strategyFactory = new StrategyFactory();
strategyFactory.put("RSA",new RSAEncrypt());
strategyFactory.put("AES",new AESEncrypt());
strategyFactory.put("DESede",new DESedeEncrypt());
strategyFactory.encrypt("AES");
//輸出
您當前選擇的AES對稱加密算法

命令模式(將系統中的相關操作抽象成命令,使調用者與實現者相關分離)

定義

將一個請求封裝位一個對象,使發出請求的責任和執行請求的責任分隔開。這樣兩者之間通過命令對象進行溝通,這樣方便命令對象進行存儲、傳遞、調用、增加和管理。

特點

優點

  • 降低系統的耦合度。命令模式能將調用操作的對象與實現該操作的對象解耦。
  • 增加或刪除命令非常方便。採用命令模式增加與刪除命令不會影響其他類,滿足開閉原則,對擴展比較靈活。
  • 可實現宏命令。命令模式可以與組合命令結合,將多個命令裝配成一個組合命令,即宏命令。
  • 方便實現Undo和Redo操作。命令模式可以與後面介紹的備忘錄模式結合,實現命令的撤銷與恢復。

缺點

  • 增加系統的複雜度,可能產生大量具體命令類,因此針對每一個具體的操作都要設計一個具體命令類。

結構

命令模式包含以下主要角色

  • 抽象命令類(Abstract Command)角色:聲明執行命令的接口,擁有執行命令的抽象方法execute()。

  • 具體命令類(Concrete Command)角色:是抽象命令類的具體實現類,擁有接收者對象,並通過調用接收者的功能來完成命令要執行的操作。

  • 實現者/接收者(Receiver)角色:執行命令功能的相關操作,是具體命令對象業務的真正實現者。

  • 調用者/請求者(Invoker)角色:請求的發送者,通常擁有很多的命令對象,並通過訪問命令對象來執行相關請求,它不直接訪問接收者。

    結構如下:在這裏插入圖片描述

實現

案例:顧客去餐廳喫早餐,通過服務員點菜,服務員再通知廚師進行烹飪,整個過程中,顧客與廚師並未產生任何交流,通過點餐命令服務員角色進行傳遞,這裏通過命令模式進行展示。這裏:

  • 點餐—相當於命令(例如:點包子、粥、河粉等)
  • 服務員—相當於調用者
  • 廚師—相當於接收者(包子廚師、粥廚師、河粉廚師)
  1. 定義抽象命令(早餐)
/**
 *抽象命令類-聲明執行命令的接口
 */
public abstract class AbsBreakfast {
    public abstract void cooking();
}
  1. 定義具體命令對象(包子、河粉、油條)
/**
 *具體命令對象-包子
 *抽象命令對象的具體實現者,包含接收者對象,通過調用接收者對象的功能來實現命令要執行的操作
 */
public class BaoZi extends AbsBreakfast {
    //接收者,具體命令對象業務真正實現者
    BaoziChef baoziChef;

    public BaoZi() {baoziChef = new BaoziChef();}

    @Override
    public void cooking() {baoziChef.cooking();}
}
/**
 *具體命令對象-河粉
 */
public class HeFen extends AbsBreakfast {
    //接收者,具體命令對象業務真正實現者
    HeFenChef heFenChef;

    public HeFen() {heFenChef = new HeFenChef();}

    @Override
    public void cooking() {heFenChef.cooking();}
}
/**
 *具體命令對象-油條
 */
public class YouTiao extends AbsBreakfast{
    //接收者,具體命令對象業務真正實現者
    YouTiaoChef youTiaoChef;

    public YouTiao(){ youTiaoChef = new YouTiaoChef();}

    @Override
    public void cooking() {youTiaoChef.cooking();}
}
  1. 定義接收者(廚師)
/**
 *接收者-包子廚師(具體命令對象業務的真正實現者)
 */
public class BaoziChef {
    public void cooking() {
        System.out.println("我是包子廚師,我正在做包子");
    }
}
/**
 *接收者-河粉廚師(具體命令對象業務的真正實現者)
 */
public class HeFenChef {
    public void cooking() {
        System.out.println("我是河粉廚師,我正在做河粉");
    }
}
/**
 *接收者-油條廚師(具體命令對象業務的真正實現者)
 */
public class YouTiaoChef {
    public void cooking() {
        System.out.println("我是油條廚師,我正在炸油條");
    }
}
  1. 定義調用者(服務員)
/**
 *調用者-服務員(請求的發送者,通常擁有多個命令對象,並通過訪問命令對象的來執行相關操作,不直接訪問接收者)
 */
public class Waiter {
    //擁有多個命令對象(抽象命令)
    AbsBreakfast breakfast;

    public void setBreakfast(AbsBreakfast breakfast){this.breakfast = breakfast;}
    //通過調用命令對象來執行相關操作,不直接訪問接收者
    public void cooking(){breakfast.cooking();}
}

測試

//調用者-發送命令-接收者執行
Waiter waiter = new Waiter();
waiter.setBreakfast(new BaoZi());
waiter.cooking();
waiter.setBreakfast(new HeFen());
waiter.cooking();
//輸出
我是包子廚師,我正在做包子
我是河粉廚師,我正在做河粉

使用場景

  • 系統需要將請求調用者請求接收者 解耦時,命令模式使得調用者和接收者不直接交互。
  • 系統需要隨機請求命令或經常增加或刪除命令時,命令模式比較方便實現。
  • 系統需要執行一組操作時,命令模式可以定義宏命令來實現該功能。
  • 當系統需要支持命令的撤銷(Undo)操作和恢復(Redo)操作時,可以將命令對象存儲起來,採用備忘錄來實現。

擴展

1. 宏命令模式

命令模式+組合模式 = 宏命令模式,也叫組合命令模式。宏命令包含一組命令,它充當了具體命令與調用者的雙重角色,執行它時將遞歸調用它所包含的所有命令。

  • 具體命令相當於樹葉構件
  • 調用者相當於樹枝構件

結構如下:
在這裏插入圖片描述
其他不變,主要是調用者(樹枝構件提供管理樹葉構件功能)

//抽象命令
interface AbstractCommand
{
    public abstract void execute();
}
//樹葉構件: 具體命令1
class ConcreteCommand1 implements AbstractCommand
{
    private CompositeReceiver receiver;
    ConcreteCommand1()
    {
        receiver=new CompositeReceiver();
    }
    public void execute()
    {       
        receiver.action1();
    }
}

//樹葉構件: 具體命令2
class ConcreteCommand2 implements AbstractCommand
{
    private CompositeReceiver receiver;
    ConcreteCommand2()
    {
        receiver=new CompositeReceiver();
    }
    public void execute()
    {       
        receiver.action2();
    }
}
//接收者
class CompositeReceiver
{
    public void action1()
    {
        System.out.println("接收者的action1()方法被調用...");
    }
    public void action2()
    {
        System.out.println("接收者的action2()方法被調用...");
    }
}

調用者-樹枝構件

//樹枝構件: 調用者-提供管理樹葉構件的功能
class CompositeInvoker implements AbstractCommand
{
    private ArrayList<AbstractCommand> children = new ArrayList<AbstractCommand>();   
    public void add(AbstractCommand c)
    {
        children.add(c);
    }   
    public void remove(AbstractCommand c)
    {
        children.remove(c);
    }   
    public AbstractCommand getChild(int i)
    {
        return children.get(i);
    }   
    public void execute()
    {
        for(Object obj:children)
        {
            ((AbstractCommand)obj).execute();
        }
    }    
}

測試

AbstractCommand cmd1=new ConcreteCommand1();
AbstractCommand cmd2=new ConcreteCommand2();
CompositeInvoker ir=new CompositeInvoker();
ir.add(cmd1);
ir.add(cmd2);
System.out.println("客戶訪問調用者的execute()方法...");
ir.execute();

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章