代理模式、命令模式、策略模式和門面模式初探

摘自《輕量級JavaEE企業應用》

代理模式

當客戶端代碼需要調用某一個對象時,客戶端實際上不關心是否準確得到該對象,他只要一個能提供該功能的對象即可,此時就可以返回該對象的代理(Proxy)
有點像無中生有,直到真正使用到對象的方法時候才創建這個對象,之前都是一個對象的假象。
代碼舉例:一個BigImage對象實例化的時候需要花費3秒鐘,但是我們可能并不會真正使用到它或者用到它的時候是比較后面的時候才用到我們就可以這樣設計。

延時加載的BigImage

Image接口

package com.proxy;
/**
 * 大圖所需要實現的接口
 * @author Slience
 *
 */
public interface Image {
    void show();
}

之后BigImage去實現這個接口

package com.proxy;

/**
 * 大圖片實現類
 * 構造函數中暫停了3秒來模擬系統開銷,如果不用代理模式
 * 系統在創建這個BigImage的時候將會有3秒的延時
 * @author Slience
 *
 */
public class BigImage implements Image {

    public BigImage() {
        // TODO Auto-generated constructor stub
        try {
            //模式加載大圖片的時間
            Thread.sleep(3000);
            System.out.println("圖片加載成功");
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
    
    @Override
    public void show() {
        // TODO Auto-generated method stub
        System.out.println("繪制實際的大圖片");
    }

}

之后創建ImageProxy代理類,它來發起Image的show動作

package com.proxy;

public class ImageProxy implements Image {
    //組合一個image實例,作為代理的對象
    private Image image;
    //使用抽象實體來初始化代理的對象
    public ImageProxy(Image image) {
        // TODO Auto-generated constructor stub
        this.image = image;
    } 
    
    /***
     * 重寫Image接口的show()方法
     * 該方法用于控制對被代理對象的訪問
     * 并根據需要負責創建和刪除被代理對象
     */
    @Override
    public void show() {
        // TODO Auto-generated method stub
        //只有當真正需要調用image的show方法是才會創建被代理的對象
        if(image == null) {
            image = new BigImage();
        }
        image.show();
    }
    
}

代理類ImageProxy中的show方法控制了系統真正使用此Image的時候才會實例化對象。

package com.proxy;

/***
 * 獲取到Image很快,因為并沒有真正創建BigImage對象,BigImage對象是在調用show方法的時候創建的
 * 這樣做的好處在于創建BigImage對象的時間推遲到真正需要它的時候,可以減少BigImage在內存中的存活時間
 * 還有可能就是系統可能永遠也用不到BigImage對象(不調用show方法),意為著系統根本不需要實例化BigImage
 * 這是Hibernate延時加載所采用的設計模式
 * @author Slience
 *
 */
public class BigImageTest {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        Image image = new ImageProxy(null);
        System.out.println("系統得到Image對象的時間開銷:" + (System.currentTimeMillis() - start));
        image.show();
        System.out.println("調用show方法時真正創建Image的時間開銷:" + (System.currentTimeMillis() - start));
    }
}

運行效果

可以看見系統可以很快的得到Image對象,只不過在它實際使用的時候才會去new這個對象。這樣做的好處有兩點

  • 把創建BigImage推遲到真正需要它時才創建,這樣能夠保證前面代碼運行的流暢性,而且減少BigImage對象在系統內存的存活時間。
  • 在有些情況下,程序也許永遠不會真正調用BigImage的show方法,這意味著系統根本無需創建BigImage對象。在這種情形下,使用代理模式可以顯著提高性能。

第二個好處就是Hibernate【延時加載】所采用的設計模式——當A實體和B實體存在關聯關系的時候,當系統加載A實體的時候B實體其實并未加載出來,直到A實體真正需要去訪問B實體的時候才會加載B

增強功能的代理

代理模式還可以用到另一種地方,就是當對象功能不足的時候,系統可以創建一個代理對象,而這個代理對象可以增強元目標對象的功能。
這一點很像【攔截器】的功能
舉個例子:有一個獵狗,他會執行info和run方法,但是現在需要在執行這兩個方法之前執行一些處理,我們就可以這樣操作

package com.proxy;
/**
 * 狗接口
 * @author Slience
 *
 */
public interface Dog {
    void info();
    void run();
}

然后一只獵狗去實現這個接口,沒啥特殊的

package com.proxy;

public class GunDog implements Dog {

    @Override
    public void info() {
        // TODO Auto-generated method stub
        System.out.println("我是一只獵狗");
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println("獵狗奔跑很快");
    }

}

現在需要在執行run和info之前【增加事物控制】——在目標方法被調用之前開始事物,在目標方法被調用之后結束事物。
我們在這里創建一個攔截器類TxUtil

package com.proxy;

public class TxUtil {
    public void beginTx() {
        System.out.println("模擬事物開始");
    }
    public void endTx() {
        System.out.println("模擬結束事物");
    }
}

之后借助InvocationHandler來實現代理

package com.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvokationHandler implements InvocationHandler {

    //這是需要被代理的對象
    private Object target;
    
    public void setTarget(Object target) {
        this.target = target;
    }


    /**
     * 當執行動態代理的對象的所有方法是,都會被替換成執行如下的invoke方法
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // TODO Auto-generated method stub
        System.out.println("method = " + method);
        TxUtil tx = new TxUtil();
        //執行攔截器中的方法
        tx.beginTx();
        //執行本該執行的方法
        Object result = method.invoke(target, args);
        //執行攔截器中的方法
        tx.endTx();
        return result;
    }
    
}

之后創建一個動態代理的Factory類

package com.proxy;

import java.lang.reflect.Proxy;

/**
 * 為指定的target生成動態代理實例
 * @author Slience
 *
 */
public class MyProxyFactory {
    public static Object getProxy(Object object) throws Exception {
        //創建一個InvocationHandler對象
        MyInvokationHandler handler = new MyInvokationHandler();
        //設置將要代理的對象
        handler.setTarget(object);
        //創建并返回一個動態代理
        //這個動態代理與target實現了同樣的接口,所以具有相同的public方法
        //所以動態代理對象可以當成target目標對象來使用
        //當程序調用了動態代理對象的指定方法,實際上轉變為
        //執行MyInvokationHandler對象的involke方法
        return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), handler);
    }
}

測試類

package com.proxy;
/**
 * 測試Dog代理
 * @author Slience
 *
 */
public class Test {
    public static void main(String[] args) throws Exception {
        Dog target = new GunDog();
        //制定target來創建動態代理
        Dog dog = (Dog) MyProxyFactory.getProxy(target);
        dog.info();
        dog.run();
    }
}

運行結果

其實這種思想在AOP面向切面編程里被稱為AOP代理,AOP代理可以替代目標對象,AOP代理包含目標對象的全部方法,而且在指向目標方法之前之后插入一些動作

命令模式

一個參數和執行方法體的代碼也可以改變的方法
或者說把一個代碼塊當作參數傳入的方法,Java8的Lambda表達式
當我們需要做一個接收一個數組的方法,這個方法可能對數組然后調用兩次這個方法,第一個調用的時候是將這個數組輸出出來,第二次調用的時候將這個數組的所有元素相加。具體實現如下

package com.command;
/**
 * 有一個each方法用來處理數組,但是具體如何處理數組還不確定
 * @author Slience
 *
 */
public class ProcessArray {
    public void each(int[] target, Command command) {
        command.process(target);
    }
}

package com.command;

public interface Command {
    //接口里定義的process()方法用于封裝“處理行為”
    void process(int[] target);
}

測試類

package com.command;

public class CommentTest {
    public static void main(String[] args) {
        ProcessArray processArray = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        //第一次處理數組,具體的處理行為取決于Comment對象
        processArray.each(target, new Command() {
            public void process(int[] target) {
                for(int temp: target) {
                    System.out.println("迭代輸出目標數組的元素:" + temp);
                }
            }
        });
        
        System.out.println("________________________");
        processArray.each(target, new Command(){
            public void process(int[] target) {
                int sum = 0;
                for(int tmp: target) {
                    sum += tmp;
                }
                System.out.println("數組元素總和是:" + sum);
            }
        });
        
    }
}

挺像javascript的匿名函數的

策略模式

主要用來封裝系列算法,使用策略模式可以輕松的切換不同的算法
問題引入:網上開書店,有多種打折策略,VIP打XX折,舊書打XX折,如何對多種打折策略進行選擇呢?比較不好的方式是使用switch case進行區分,但是如果有一天新增了一種打折策略SVIP,那么需要修改的地方至少修改三個地方:增加一個常量來代表SVIP打折類型;其次需要增加一個case語句;最后實現SVIP的打折邏輯。為了改變這種設計我們可以使用策略模式來實現此功能。
首先定一個一個打折接口,讓所有的打折策略去實現這個接口

package com.strategy;

public interface DiscountStrategy {
    //定義一個用戶計算打折價的方法
    double getDiscount(double originPrice);
}

接下來實現兩種打折算法
舊書打折

package com.strategy;

public class OldDiscount implements DiscountStrategy {

    @Override
    public double getDiscount(double originPrice) {
        // TODO Auto-generated method stub
        System.out.println("使用舊書折扣...");
        return originPrice * 0.7;
    }
    
}

VIP打折

package com.strategy;

public class VipDiscount implements DiscountStrategy {

    @Override
    public double getDiscount(double originPrice) {
        // TODO Auto-generated method stub
        System.out.println("使用VIP折扣...");
        return originPrice * 0.5;
    }

}

之后在寫一個DsicountContext類,用來找到合適的打折策略

package com.strategy;

/**
 * 扮演決策者,決定調用哪個折扣策略來處理打折
 * @author Slience
 *
 */
public class DiscountContext {
    //組合一個DiscountStrategy對象
    private DiscountStrategy strategy;
    public DiscountContext(DiscountStrategy strategy) {
        // TODO Auto-generated constructor stub
        this.strategy = strategy;
    }
    
    //根據實際所使用的DiscountStrategy對象獲取折扣價
    public double getDiscountPrice(double price) {
        //如果strategy為null,系統自動選擇OldDiscount類
        if(strategy == null) {
            strategy = new OldDiscount();
        }
        return this.strategy.getDiscount(price);
    }
    
    public void changeDiscount(DiscountStrategy strategy) {
        this.strategy = strategy;
    }
}

測試類

package com.strategy;

public class StrategyTest {
    public static void main(String[] args) {
        //沒有選擇打折策略類,默認用舊書打折策略
        DiscountContext discountContext = new DiscountContext(null);
        double price = 79;
        System.out.println(price + "元的書默認折價后的價格是: " + discountContext.getDiscountPrice(price));
        //使用VIP打折策略
        discountContext.changeDiscount(new VipDiscount());
        System.out.println(price + "元的書VIP折價后的價格是:" + discountContext.getDiscountPrice(price));
        //如果需要添加一個SVIP的打折策略的話
        //只需要創建一個實現了DiscountStrategy接口的類就好
        discountContext.changeDiscount(new SVipDiscount());
        System.out.println(price + "元的書SVIP折價后的價格是:" + discountContext.getDiscountPrice(price));
    }
}

當我們需要添加一個新策略SVIP的時候就可以直接創建一個打折類

package com.strategy;

public class SVipDiscount implements DiscountStrategy {

    @Override
    public double getDiscount(double originPrice) {
        // TODO Auto-generated method stub
        System.out.println("使用SVIP打折");
        return originPrice * 0.1;
    }

}

之后在測試方法中使用changeDiscount來修改當前的打折策略即可。這樣做的缺點在于客戶端代碼需要和不同的策略類耦合。為了彌補不足可以考慮使用配置文件來指定DiscountContext選擇那種打折策略。

門面模式

它可以將一組復雜的類包裝到一個簡單的外部接口中。
舉例說明:一個顧客Customer有一個haveDinner方法,這個方法需要使用三個接口的方法,這三個接口是收銀部Payment的pay方法,廚師部Cook的cook方法還有服務生部的Waiter的上菜serve方法。代碼如下

package com.facade;
//收銀部
public interface Payment {
    public String pay();
}

package com.facade;
//廚師部
public interface Cook {
    public String cook(String food);
}

package com.facade;
//服務生部
public interface Waiter {
    public void serve(String food);
}

以及分別實現以上接口的類

package com.facade;

public class PaymentImpl implements Payment{

    @Override
    public String pay() {
        // TODO Auto-generated method stub
        String food = "快餐";
        System.out.println("你已經向收銀員支付了費用,你購買的食物是:" + food);
        
        return food;
    }
}

package com.facade;

public class CookImpl implements Cook {

    @Override
    public String cook(String food) {
        // TODO Auto-generated method stub
        System.out.println("廚師正在烹調:" + food);
        return food;
    }
    
}

package com.facade;

public class WaiterImpl implements Waiter {

    @Override
    public void serve(String food) {
        // TODO Auto-generated method stub
        System.out.println("服務生已經將" + food + "端過來了,請慢用");
        
    }
    
}

那么我們在實現Customer的haveDinner方法時就可以這樣:

    public void haveDinner() {
        //依次創建三個部門的實例
        Payment payment = new PaymentImpl();
        Cook cook = new CookImpl();
        Waiter waiter = new WaiterImpl();
        //依次調用三個部門的實例的方法來實現用餐的功能
        String food = payment.pay();
        food = cook.cook(food);
        waiter.serve(food);
    }

如果飯店有很多部門的話,就要一個一個去調用他們的方法圍欄解決這個問題我們可以創建一個門面類Facade來包裝這些類,對外提供一個簡單的訪問方法。
門面類Facade

package com.facade;

public class Facade {
    //定義三個部門
    Payment payment;
    Cook cook;
    Waiter waiter;
    public Facade() {
        // TODO Auto-generated constructor stub
        this.payment = new PaymentImpl();
        this.cook = new CookImpl();
        this.waiter = new WaiterImpl();
    }
    
    public void serveFood() {
        //依次調用三個部門的方法,封裝成一個serveFood()方法
        String food = payment.pay();
        food = cook.cook(food);
        waiter.serve(food);
         
    }
}

之后Customer去實現haveDinner就可以變成這樣

    public void haveDinner() {
        Facade facade = new Facade();
        facade.serveFood();
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,082評論 6 13
  • 設計模式匯總 一、基礎知識 1. 設計模式概述 定義:設計模式(Design Pattern)是一套被反復使用、多...
    MinoyJet閱讀 3,961評論 1 15
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,341評論 11 349
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 【連載】鼬真傳(3) 這個是我之前的作品,雖然是連載的,但是有很長時間沒有更新了,所以風格和連貫性上可能會有所差別...
    遠坂宗政閱讀 959評論 0 2