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

摘自《轻量级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();
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,846评论 6 13
  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,887评论 1 15
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,166评论 11 349
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • 【连载】鼬真传(3) 这个是我之前的作品,虽然是连载的,但是有很长时间没有更新了,所以风格和连贯性上可能会有所差别...
    远坂宗政阅读 918评论 0 2