欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

设计模式:(五)代理、模板方法、命令、访问者模式

程序员文章站 2022-04-28 19:31:57
...

一、代理模式

1、概述

代理模式(Proxy)定义:为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,这样做的好处是可以在目标对象实现的基础上,增加额外的功能操作,即扩展目标对象的功能。
代理模式的不同形式:
  • 静态代理
  • 动态代理(JDK代理、接口代理)
  • CgLib代理(可以在内存动态的创建对象,而不需要实现接口,是属于动态代理的范畴)

2、代理模式的主要角色

抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

3、静态代理模式

静态代理在使用的时候,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类。
/**
 * 抽象主题
 */
public interface ITeacher {
    /**
     * 讲课方法
     */
    void teach();
}

/**
 * 真实主题,目标对象
 */
public class ChineseTeacher implements ITeacher {
    @Override
    public void teach() {
        System.out.println("正在授课中...");
    }
}

/**
 * 代理类
 */
public class TeacherProxy implements ITeacher {

    /**
     * 目标对象,通过接口来聚合
     */
    private ITeacher iTeacher;

    public TeacherProxy(ITeacher iTeacher) {
        this.iTeacher = iTeacher;
    }

    @Override
    public void teach() {
        System.out.println("课前准备...");
        iTeacher.teach();
        System.out.println("结束上课...");
    }
}

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        //创建目标对象(被代理对象)
        ChineseTeacher chineseTeacher = new ChineseTeacher();
        //创建代理对象,同时将被代理对象传递给代理对象
        TeacherProxy proxy = new TeacherProxy(chineseTeacher);
		//通过代理对象,调用被代理对象的方法
        proxy.teach();
    }
}

/**
 * 运行结果
 */
课前准备...
正在授课中...
结束上课...
优点:在不修改目标对象的功能前提下,能通过代理对象对目标功能扩展。
缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,一旦接口增加方法,目标对象与代理对象都要维护。

4、动态代理模式

代理类不需要实现接口,但是目标对象要实现接口,否则不能用动态代理,代理对象的生成,是利用JDK的API,动态的在内存中构件代理对象。也叫JDK代理。运用反射机制动态创建。
代理类所在的包:java.lang.reflect.Proxy,JDK实现代理只需要使用newProxyInstance方法
/**
 * 抽象主题
 */
public interface ITeacher {
    /**
     * 讲课方法
     */
    void teach();
}

/**
 * 真实主题
 */
public class ChineseTeacher implements ITeacher {
    @Override
    public void teach() {
        System.out.println("正在授课中...");
    }
}

/**
 * 代理类
 */
public class TeacherProxy {

    /**
     * 目标对象,通过接口来聚合
     */
    private Object target;

    public TeacherProxy(Object target) {
        this.target = target;
    }

    /**
     * 给目标对象生成一个代理对象
     * public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
     * ClassLoader loader:指定当前目标对象使用的类加载器,获取加载器的方法固定
     * Class<?>[] interfaces:目标对象实现的接口类型,使用泛型的方式确认类型
     * InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行的目标对象方法作为参数传入
     * @return
     */
    public Object getProxyInstance() {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("动态代理开始...");
                        //通过反射机制调用目标对象的方法
                        Object result = method.invoke(target, args);
                        return result;
                    }
                });
    }
}

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        //创建目标对象(被代理对象)
        ITeacher iTeacher = new ChineseTeacher();
        //给目标对象,创建代理对象
        ITeacher proxy = (ITeacher) new TeacherProxy(iTeacher).getProxyInstance();
        //proxy = class com.sun.proxy.$Proxy0 可以看出内存中动态生成了代理对象
        System.out.println("proxy = " + proxy.getClass());
        //通过代理对象,调用目标对象的方法
        proxy.teach();
    }
}

/**
 * 运行结果
 */
proxy = class com.sun.proxy.$Proxy0
动态代理开始...
正在授课中...

5、CgLib代理

1、静态代理和JDK代理模式都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候可使用目标对象子类来实现代理,这就是CgLib代理。
2、CgLib代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展,有些书也将Cglib代理归属到动态代理。
3、CgLib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口。它广泛的被许多AOP的框架使用,例如Spring AOP,实现方法拦截。
4、在AOP编程中如何选择代理模式:
  • 目标对象需要实现接口,用JDK代理。
  • 目标对象不需要实现接口,用Cglib代理。
5、CgLib包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的类。

实现步骤说明

1、需要引入CgLib的jar文件。
2、在内存中动态构建子类,注意代理的类不能为final,否则报错java.lang.IllegalArgumentException
3、目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法。
 <!--cglib依赖-->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.4</version>
</dependency>
/**
 * 目标对象(被代理对象)
 */
public class Teacher {
    /**
     * 讲课方法
     */
    public void teach() {
        System.out.println("老师讲课中...");
    }
}

/**
 * 代理类,实现MethodInterceptor接口
 */
public class TeacherProxy implements MethodInterceptor {

    /**
     * 目标对象
     */
    private Object target;

    /**
     * 传入一个被代理的对象
     * @param target
     */
    public TeacherProxy(Object target) {
        this.target = target;
    }

    /**
     * 返回一个代理对象,是target对象的代理对象
     * @return
     */
    public Object getProxyInstance() {
        //1、创建一个工具类
        Enhancer enhancer = new Enhancer();
        //2、设置父类
        enhancer.setSuperclass(target.getClass());
        //3、设置回调函数
        enhancer.setCallback(this);
        //4、创建子类对象,即代理对象
        return enhancer.create();
    }

    /**
     * 重写intercept方法,实现对被代理对象(即目标对象)方法的调用
     * @param o
     * @param method
     * @param objects
     * @param methodProxy
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("cglib代理模式开始");
        Object invoke = method.invoke(target, objects);
        System.out.println("cglib代理模式结束");
        return invoke;
    }
}

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        //创建目标对象(被代理对象)
        Teacher teacher = new Teacher();
        //获取到代理对象,并且将目标对象传递给代理对象
        Teacher proxy = (Teacher) new TeacherProxy(teacher).getProxyInstance();
        //执行代理对象的方法,触发intecept方法,从而实现对目标对象的调用
        proxy.teach();
    }
}

/**
 * 运行结果
 */
cglib代理模式开始
老师讲课中...
cglib代理模式结束

6、优缺点说明

优点:
  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用。
  • 代理对象可以扩展目标对象的功能。
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性。
缺点:
  • 代理模式会造成系统设计中类的数量增加。
  • 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢。
  • 增加了系统的复杂度。

二、模板方法模式

1、概述

模板方法模式(Template Method)定义:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。

2、模板模式的主要角色

1、抽象类/抽象模板(Abstract Class)
抽象模板类,负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下:
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
基本方法:是整个算法中的一个步骤,包含以下几种类型。
  • 抽象方法:在抽象类中声明,由具体子类实现。
  • 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写。
  • 钩子方法:默认不做任何事,在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
2、具体子类/具体实现(Concrete Class)
具体实现类,实现抽象类中所定义的抽象方法和钩子方法,它们是一个*逻辑的一个组成步骤。

3、模板方法模式实现

抽象类

public abstract class AbstractClass {
    /**
     * 模板方法,定义成final,不让子类去覆盖
     */
    public final void templateMethod() {
        specificMethod();
        if (condiction()) {
            abstractMethod();
        }
        clear();
    }

    /**
     * 具体方法
     */
    public void specificMethod() {
        System.out.println("第一步:选择好的大米");
    }

    /**
     * 具体方法
     */
    public void clear() {
        System.out.println("第三步:清洗浸泡大米开始煮粥");
    }

    /**
     * 抽象方法,子类具体实现
     */
    public abstract void abstractMethod();

    /**
     * 钩子方法
     */
    public boolean condiction() {
        return true;
    }
}

具体子类

/**
 * 具体子类1
 */
public class ConcreteClass1 extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("第二步:加入绿豆开始煮稀饭");
    }
}

/**
 * 具体子类2
 */
public class ConcreteClass2 extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("第二步:加入红豆开始煮稀饭");
    }
}

/**
 * 具体子类3
 */
public class ConcreteClass3 extends AbstractClass {
    @Override
    public void abstractMethod() {
        //空实现
    }

    @Override
    public boolean condiction() {
        return false;
    }
}

客户端

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        //制作绿豆粥
        System.out.println("开始制作绿豆粥");
        AbstractClass green = new ConcreteClass1();
        green.templateMethod();

        System.out.println("开始制作红豆粥");
        AbstractClass red = new ConcreteClass2();
        red.templateMethod();
    }
}

/**
 * 运行结果
 */
开始制作绿豆粥
第一步:选择好的大米
第二步:加入绿豆开始煮稀饭
第三步:清洗浸泡大米开始煮粥
开始制作红豆粥
第一步:选择好的大米
第二步:加入红豆开始煮稀饭
第三步:清洗浸泡大米开始煮粥
开始制作白米粥
第一步:选择好的大米
第三步:清洗浸泡大米开始煮粥

4、优缺点说明

优点:
  • 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  • 它在父类中提取了公共的部分代码,便于代码复用。
  • 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。
缺点:
  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
  • 由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。

5、应用场景

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  • 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。

三、命令模式

1、概述

命令(Command)模式定义:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。同时命令模式也支持可撤销的操作。

2、命令模式的主要角色

抽象命令类(Command):声明执行命令的接口,拥有执行命令的抽象方法execute()。
具体命令类(Concreate Command):是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
实现者/接收者(Receiver):执行命令功能的相关操作,是具体命令对象业务的真正实现者。
调用者/请求者(Invoker):是请求的发送者,它通过拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。

3、命令模式实现

抽象命令

public interface Command {
    /**
     * 执行操作
     */
    public void execute();

    /**
     * 撤销操作
     */
    public void undo();
}

接收者

public class Receiver {
    public void on() {
        System.out.println("电灯打开了...开始学习");
    }

    public void off() {
        System.out.println("电灯关闭了...准备睡觉");
    }
}

具体命令

/**
 * 具体命令1
 */
public class ConcreteCommand1 implements Command {
    //聚合接收者
    private Receiver receiver;

    public ConcreteCommand1(Receiver receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        receiver.on();
    }

    @Override
    public void undo() {
        receiver.off();
    }
}

/**
 * 具体命令2
 */
public class ConcreteCommand2 implements Command {
    //聚合接收者
    private Receiver receiver;

    public ConcreteCommand2(Receiver receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        receiver.off();
    }

    @Override
    public void undo() {
        receiver.on();
    }
}

/**
 * 没有任何命令,即空执行,用于初始化,当调用空命令时,对象什么事也不做
 * 其实这也是一种设计模式,可以省略对空判断
 */
public class NoCommand implements Command {
    @Override
    public void execute() {

    }

    @Override
    public void undo() {

    }
}

调用者

public class Invoker {
    //打开命令的数组
    Command[] onCommands;

    //关闭命令的数组
    Command[] offCommands;

    //撤销命令的数组
    Command undoComand;

    /**
     * 完成初始化
     */
    public Invoker() {
        onCommands = new Command[5];
        offCommands = new Command[5];
        for (int i = 0; i < 5; i++) {
            onCommands[i] = new NoCommand();
            offCommands[i] = new NoCommand();
        }
    }

    /**
     * 设置命令
     * @param no
     * @param onCommand
     * @param offCommand
     */
    public void setCommand(int no,Command onCommand,Command offCommand) {
        onCommands[no] = onCommand;
        offCommands[no] = offCommand;
    }

    /**
     * 执行打开命令
     * @param no
     */
    public void executeOn(int no) {
        //找到对应开关,并调用对应方法
        onCommands[no].execute();
        //记录这次的操作,用于撤销
        undoComand = onCommands[no];
    }

    /**
     * 执行关闭命令
     * @param no
     */
    public void executeOff(int no) {
        //找到对应开关,并调用对应方法
        offCommands[no].execute();
        //记录这次的操作,用于撤销
        undoComand = offCommands[no];
    }

    /**
     * 执行撤销命令
     * @param no
     */
    public void executeUndo(int no) {
        undoComand.undo();
    }
}

客户端

/**
 * 使用命令模式,通过遥控器,对电灯操作
 */
public class Client {
    public static void main(String[] args) {
        //创建电灯的对象(调用者)
        Receiver receiver = new Receiver();
        //创建打开命令
        ConcreteCommand1 open = new ConcreteCommand1(receiver);
        //创建关闭命令
        ConcreteCommand2 close  = new ConcreteCommand2(receiver);
        //创建遥控器对象
        Invoker invoker = new Invoker();
        //给遥控器设置相关命令
        invoker.setCommand(0,open,close);
        System.out.println("--------按下打开按钮--------");
        invoker.executeOn(0);
        System.out.println("--------按下关闭按钮--------");
        invoker.executeOff(0);
        System.out.println("--------按下撤销按钮--------");
        invoker.executeUndo(0);
    }
}

/**
 * 运行结果
 */
--------按下打开按钮--------
电灯打开了...开始学习
--------按下关闭按钮--------
电灯关闭了...准备睡觉
--------按下撤销按钮--------
电灯打开了...开始学习

4、优缺点说明

优点:
  • 将发起请求的对象与执行请求的对象解耦。发起请求的对象是调用者,调用者只要调用命令对象的execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁、是如何实现的,命令对象会负责让接收者执行请求的动作,也就是说:“请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到 了纽带桥梁的作用。
  • 容易设计一个命令队列。只要把命令对象放到列队,就可以多线程的执行命令。
  • 扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
  • 可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。
缺点:
  • 可能导致某些系统有过多的具体命令类,增加了系统的复杂度。
  • 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难理解。

5、应用场景

  • 请求调用者需要与请求接收者解耦时,命令模式可以使调用者和接收者不直接交互。
  • 系统随机请求命令或经常增加、删除命令时,命令模式可以方便地实现这些功能。
  • 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录模式来实现。

四、访问者模式

1、概述

访问者(Visitor)模式:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。
工作原理:在被访问的类里面加一个对外提供接待访问者的接口。

2、访问者模式的主要角色

  • 抽象访问者(Visitor):定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。
  • 具体访问者(ConcreteVisitor):实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
  • 抽象元素(Element):声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
  • 具体元素(ConcreteElement):实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
  • 对象结构(Object Structure):是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。

3、访问者模式实现

抽象访问者

public interface Visitor {
    /**
     * 访问具体元素A
     * @param concreteElementA
     */
    void visit(ConcreteElementA concreteElementA);

    /**
     * 访问具体元素B
     * @param concreteElementB
     */
    void visit(ConcreteElementB concreteElementB);
}

具体访问者

/**
 * 具体访问者A
 */
public class ConcreteVisitorA implements Visitor {
    @Override
    public void visit(ConcreteElementA concreteElementA) {
        System.out.println("具体访问者A访问===>" + concreteElementA.operationA());
    }

    @Override
    public void visit(ConcreteElementB concreteElementB) {
        System.out.println("具体访问者A访问===>" + concreteElementB.operationB());
    }
}

/**
 * 具体元素B
 */
public class ConcreteElementB implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public String operationB() {
        return "具体元素B的操作";
    }
}

抽象元素

public interface Element {
    void accept(Visitor visitor);
}

具体元素

/**
 * 具体元素A
 * 1、这里使用了双分派,首先在客户端程序中,将具体状态作为参数传递ConcreteElementA中(第一次分派)
 * 2、然后ConcreteElementA类调用作为参数的“具体方法”中方法visit,同时将自己(即this)作为参数传入,完成第二次分派
 */
public class ConcreteElementA implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public String operationA() {
        return "具体元素A的操作";
    }
}

/**
 * 具体访问者B
 */
public class ConcreteVisitorB implements Visitor {
    @Override
    public void visit(ConcreteElementA concreteElementA) {
        System.out.println("具体访问者B访问===>" + concreteElementA.operationA());
    }

    @Override
    public void visit(ConcreteElementB concreteElementB) {
        System.out.println("具体访问者B访问===>" + concreteElementB.operationB());
    }
}

对象结构

public class ObjectStructure {
    /**
     * 维护一个集合
     */
    private List<Element> list = new LinkedList<>();

    /**
     * 显示数据
     * @param visitor
     */
    public void accept(Visitor visitor) {
        Iterator<Element> i = list.iterator();
        while (i.hasNext()) {
            ((Element) i.next()).accept(visitor);
        }
    }

    /**
     * 添加方法
     * @param element
     */
    public void add(Element element) {
        list.add(element);
    }

    /**
     * 移除方法
     * @param element
     */
    public void remove(Element element) {
        list.remove(element);
    }
}

客户端

public class Client {
    public static void main(String[] args) {
        ObjectStructure objectStructure = new ObjectStructure();

        objectStructure.add(new ConcreteElementA());
        objectStructure.add(new ConcreteElementB());

        Visitor visitorA = new ConcreteVisitorA();
        objectStructure.accept(visitorA);
    }
}

/**
 * 运行结果
 */
具体访问者A访问===>具体元素A的操作
具体访问者A访问===>具体元素B的操作

4、优缺点说明

优点:
  • 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  • 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  • 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对*地演化而不影响系统的数据结构。
  • 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
缺点:
  • 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  • 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  • 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。

5、应用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序。
  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
  • 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。