设计模式(四)——代理、模板、命令、访问者、迭代器、观察者

iwehdio的博客园:https://www.cnblogs.com/iwehdio/

1、代理模式

  • 代理模式:为一个对象提供一个替身,以控制对这个对象(被代理的对象)的访问。即通过代理对象访问目标对象。

  • 这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

  • 被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象代理模式有不同的形式,主要有三种静态代理、动态代理(JDK代理或接口代理)和cglib代理(不需要实现接口,一种特殊的动态代理)。

  • 静态代理:

    • 静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类。

    • 示例:培训机构代理老师进行教学。对外暴露的是培训机构,但是实际调用的是老师。

    • 类图:

    • 代码:

      //代理对象和被代理对象都要实现teach接口
      public interface Teach {
          void doTeach();
      }
      
      //被代理对象
      public class Teacher implements Teach {
          @Override
          public void doTeach() {
              System.out.println("teacher-teach");
          }
      }
      
      //代理对象,聚合了被代理对象
      public class Train implements Teach {
          private Teach teach;
          public Train(Teach teach) {
              this.teach = teach;
          }
          @Override
          public void doTeach() {
              System.out.println("train-start");
              teach.doTeach();
              System.out.println("train-end");
          }
      }
      
      //使用
      Teacher teacher = new Teacher();
      Train train = new Train(teacher);
      train.doTeach();
      
    • 优点:在不修改目标对象的功能前提下,能通过代理对象对目标功能扩展。

    • 缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类。一旦接口增加方法,目标对象与代理对象都要维护。

  • 动态代理:

    • 代理对象不需要实现接口。但是目标对象(被代理的)要实现接口,否则不能用动态代理。

    • 代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象。

    • JDK中生成代理对象的API:

      • 代理类所在包:java.lang.reflect.Proxy。
      • JDK实现代理只需要使用newProxyInstance方法,该方法需要接收三个参数。
      • 完整的写法是:
        static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h)
      • ClassLoader loader:指定当前目标对象使用的类加载器。
      • Class<?>[] interfaces:目标对象实现的接口类型,使用泛型。
      • InvocationHandler h:创建一个事件处理器。在通过代理对象调用方法时,会触发这个事件处理器方法。
      • method.invoke(被代理对象,被调用方法):调用被代理对象的被调用方法。
    • 代理工厂类中,getProxyInstance方法,根据传入的被代理类,利用反射机制,返回被代理对象并聚合。调用这个对象的方法进行代理。

    • 类图:

    • 代码:

      //Teach和Teacher与静态代理中相同
      
      //动态代理
      public class ProxyFactory {
          private Object object;
          public ProxyFactory(Object object) {
              this.object = object;
          }
          public Object getProxyInstance(){
              return Proxy.newProxyInstance(object.getClass().getClassLoader(),
                      object.getClass().getInterfaces(),
                      new InvocationHandler() {
                          @Override
                          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                              System.out.println("代理开始");
                              Object invoke = method.invoke(object, args);
                              System.out.println("代理结束");
                              return invoke;
                          }
                      });
          }
      }
      
      //使用
      Teach teach = new Teacher();
      Teach proxyInstance = (Teach) new ProxyFactory(teach).getProxyInstance();
      proxyInstance.doTeach();
      
  • Cglib代理:

    • 静态代理和JDK代理模式都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理,这就是cglib代理。

    • Cglib代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展。

    • Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口。

    • 在AOP编程中如何选择代理模式:

      • 目标对象需要实现接口,用JDK代理。
      • 目标对象不需要实现接口,用Cglib代理。
    • Cglib包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的类。

    • 代理的类不能为final,否则报错。目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法。

    • 类图:

    • 代码:

      • 代理工厂需要实现MethodInterceptor接口。
      //被代理的类不再需要实现接口
      public class Teacher {
          public void doTeach() {
              System.out.println("teacher-teach");
          }
      }
      
      //Cglib代理
      public class CglibProxy implements MethodInterceptor {
          private Object object;
          public CglibProxy(Object object) {
              this.object = object;
          }
          //返回一个代理对象
          public Object getProxyInstance() {
              //1、创建工具类
              Enhancer enhancer = new Enhancer();
              //2、设置父类
              enhancer.setSuperclass(object.getClass());
              //3、创建回调
              enhancer.setCallback(this);
              //4、返回子类
              return enhancer.create();
          }
          //拦截器,类似之前的InvocationHandler
          @Override
          public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
              System.out.println("cglib-start");
              Object invoke = method.invoke(object, args);
              System.out.println("cglib-end");
              return invoke;
          }
      }
      
      //使用
      Teacher teacher = new Teacher();
      Teacher cglibProxy = (Teacher) new CglibProxy(teacher).getProxyInstance();
      cglibProxy.doTeach();
      
  • 代理模式的变体:

    • 防火墙代理。
    • 缓存代理。
    • 远程代理。
    • 同步代理。

2、模板模式

  • 示例:

    • 制作豆浆,需要一系列的流程。
    • 不同的材料产出不同的豆浆,但是流程是相同的。
  • 模板方法模式(Template Method Pattern),又叫模板模式(Template Pattern),在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。

  • 模板方法模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤。

  • 流程和共用的部分在抽象类中实现,具体特有的在子类中实现。

  • 角色:

    • AbstractClass,抽象类,确定了方法实现的骨架,具体的需要子类实现。
    • ConcreteClass,继承抽象类的子类,根据子类的特点,分别实现抽象方法。
  • 类图:

  • 代码:

    //抽象类
    public abstract class AbstractClass {
        //保证模板不被子类覆盖
        public final void template(){
            operation1();
            operation2();
            operation3();
        }
        public abstract void operation1();
        public abstract void operation2();
        public void operation3(){
            System.out.println("father-step3");
        }
    }
    
    //子类
    public class ConcreteClass extends AbstractClass {
        @Override
        public void operation1() {
            System.out.println("son-step1");
        }
    
        @Override
        public void operation2() {
            System.out.println("son-step2");
        }
    }
    
    //使用
    AbstractClass temp = new ConcreteClass();
    temp.template();
    
  • 钩子方法:

    • 在模板方法模式的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为“钩子”。
    • 钩子可以在定义时就挂着东西(父类可有实现),可以在后来看情况挂上别的东西(子类可重写),也可以总是不挂任何东西(父类中无实现,并且子类中未重写或重写无实现)。
  • 源码分析:

    • Spring中的IOC容器初始化时用到了模板方法模式。
    • AbstractApplicationContext中的refresh()方法就是一个模板方法。
  • 注意事项:

    • 基本思想是:算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改。
    • 实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用。
    • 既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现。
    • 该模式的不足之处:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大。
    • 一般模板方法都加上final关键字,防止子类重写模板方法。
    • 模板方法模式使用场景:当要完成在某个过程,该过程要执行一系列步骤,这一系列的步骤基本相同,但其个别步骤在实现时可能不同,通常考虑用模板方法模式来处理。

3、命令模式

  • 示例:

    • 有一套智能家电,需要不同厂商的APP进行控制。
    • 希望不同厂家提供接口,用一个APP实现控制。
    • 将动作好请求者和执行者解耦。
  • 命令模式(Command Pattern):在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计。

  • 命令模式使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活,实现解耦。

  • 在命名模式中,会将一个请求封装为一个对象,以便使用不同参数来表示不同的请求(即命令),同时命令模式也支持可撤销的操作。

  • 通俗易懂的理解:将军发布命令,士兵去执行。其中:将军(命令发布者)、士兵(命令的具体执行者)、命令(连接将军和士兵)。

  • 角色:

    • 命令的调用者Invoker,持有具体命令对象。
    • 命令的接收者Receiver,包括接收到命令后的具体行为。
    • 命令接口Command,包括命令的执行和撤销方法。
    • 具体命令ConcreteCommand,实现了命令接口,持有命令的接收者对象。
  • 类图:

  • 代码:

    //命令接口
    public interface Command {
        void execute();
        void undo();
    }
    
    //命令接收者,电灯
    public class Light {
        public void on(){
            System.out.println("light-on");
        }
        public void off(){
            System.out.println("light-off");
        }
    }
    
    //开灯命令
    public class LightOn implements Command {
        private Light light;
        public LightOn(Light light) {
            this.light = light;
        }
        @Override
        public void execute() {
            light.on();
        }
        @Override
        public void undo() {
            light.off();
        }
    }
    
    //关灯命令
    public class LightOff implements Command {
        private Light light;
        public LightOff(Light light) {
            this.light = light;
        }
        @Override
        public void execute() {
            light.off();
        }
        @Override
        public void undo() {
            light.on();
        }
    }
    
    //空命令,可用于初始化等
    public class NoCommand implements Command {
        @Override
        public void execute() {
    
        }
        @Override
        public void undo() {
    
        }
    }
    
    //命令的调用者
    public class Invoker {
        private Command[] onCommands;
        private Command[] offCommands;
        private Command undoCommand;
        public Invoker() {
            this.onCommands = new Command[5];
            this.offCommands = new Command[5];
            this.undoCommand = new NoCommand();
            for (int i = 0; i < 5; i++) {
                onCommands[i] = new NoCommand();
                offCommands[i] = new NoCommand();
            }
        }
        public void setCommands(int no, Command onCommand, Command offCommand) {
            onCommands[no] = onCommand;
            offCommands[no] = offCommand;
        }
        public void pushOn(int no){
            onCommands[no].execute();
            undoCommand = onCommands[no];
        }
        public void pushOff(int no){
            offCommands[no].execute();
            undoCommand = offCommands[no];
        }
        public void undo(){
            undoCommand.undo();
            undoCommand = new NoCommand();
        }
    }
    
    //使用
    Invoker invoker = new Invoker();
    Light light = new Light();
    invoker.setCommands(0, new LightOn(light), new LightOff(light));
    invoker.pushOn(0);
    invoker.pushOff(0);
    invoker.undo();
    
  • 源码分析:

    • Spring中的JdbcTemplate用到了命令模式。
    • StatementCallback类似于命令接口。
    • 内部类QueryStatementCallback类似于具体命令实现和命令接收者。
    • JdbcTemplate是命令的调用者,通过exexcute()方法调用了具体命令实现。
  • 注意事项:

    • 将发起请求的对象与执行请求的对象解耦。发起请求的对象是调用者,调用者只要调用命令对象的execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁、是如何实现的,命令对象会负责让接收者执行请求的动作,也就是说:”请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到了纽带桥梁的作用。
    • 容易设计一个命令队列。只要把命令对象放到列队,就可以多线程的执行命令容易实现对请求的撤销和重做。
    • 命令模式不足:可能导致某些系统有过多的具体命令类,增加了系统的复杂度,这点在在使用的时候要注意。
    • 空命令也是一种设计模式,它为我们省去了判空的操作。
    • 命令模式经典的应用场景:界面的一个按钮都是一条命令、模拟CMD(DOS命令)订单的撤销/恢复、触发-反馈机制。

4、访问者模式

  • 示例:

    • 将观众分为男女,对参赛歌手进行评价,包括成功或失败。
    • 可以将男女都继承于抽象的Person接口。
    • 如果要增加一种评价或者观众种类,代码扩展性差。
  • 访问者模式(Visitor Pattern),封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

  • 主要将数据结构与数据操作分离,解决数据结构和操作耦合性问题。

  • 访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的方法接口。

  • 访问者模式主要应用场景是:需要对一个对象结构中的对象进行很多不同操作(这些操作彼此没有关联),同时需要避免让这些操作"污染"这些对象的类,可以选用访问者模式解决。

  • 角色:

    • 抽象访问者Visitor,其中定义了访问不同被访问者的抽象方法。
    • 具体访问者ConcreteVisitor,实现了进行访问的抽象方法。
    • 抽象被访问者Element,有一个方法用于接收访问者类型。
    • 具体被访问者ConcreteElement,实现具体的接受访问者方法。
    • 对象数据结构ObjectStructure,聚合了被访问者的集合。
  • 类图:

  • 代码:

    • 双分派是指不管类怎么变化,我们都能找到期望的方法运行。双分派意味着得到执行的操作取决于请求的种类和接收者的类型。
    • 双分派可在ObjectStructure中的dsiplay方法中看到。遍历People,比如其中一个对象是Man(即接收者的类型)。Man被调用accpet()接收一种类型的访问者(即请求的类型),比如Success,这是第一次分派。在accpet()方法中,调用访问者的getManResult方法,同时将自己this作为参数传入,是第二次分派。
    • 双分派保证了,如果需要新增一个请求种类(具体访问者),只需要将其传入dsiplay方法即可。不需要对被访问者作出修改,因为双分派导致accept调用的总是其对应的方法。
    //抽象和具体被访问者
    public abstract class Person {
        public abstract void accept(Visitor visitor);
    }
    
    public class Man extends Person {
        @Override
        public void accept(Visitor visitor) {
            visitor.getManResult(this);
        }
    }
    
    //抽象和具体访问者
    public abstract class Visitor {
        public abstract void getManResult(Man man);
        public abstract void getWomanResult(Woman woman);
    }
    
    public class Success extends Visitor {
        @Override
        public void getManResult(Man man) {
            System.out.println("man:success");
        }
        @Override
        public void getWomanResult(Woman woman) {
            System.out.println("woman:success");
        }
    }
    
    //对象数据结构
    public class ObjectStructure {
        private List<Person> people = new LinkedList<>();
        public void attach(Person person) {
            people.add(person);
        }
        public void delete(Person person) {
            people.remove(person);
        }
        public void display(Visitor visitor) {
            for (Person person : people) {
                person.accept(visitor);
            }
        }
    }
    
    //使用
    ObjectStructure os = new ObjectStructure();
    os.attach(new Man());
    os.attach(new Woman());
    os.display(new Success());
    
  • 注意事项:

    • 优点:
      • 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高。
      • 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统。
    • 缺点:
      • 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的,这样造成了具体元素(指被访问者)变更比较困难。
      • 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素。
      • 因此,如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的。

5、迭代器模式

  • 示例:

    • 还是展示学校学院和和系(之前组合模式的例子),如何去遍历。
    • 比如系以集合方式放在学院中,学院用数组方法放在学校中。
  • 迭代器模式( lterator Pattern):提供一种遍历集合元素的统一接口,用一致的方法遍历集合元素,不需要知道集合对象的底层表示,即不暴露其内部的结构。

  • 如果我们的集合元素是用不同的方式实现的,有数组,还有java的集合类,或者还有其他方式,当客户端要遍历这些集合元素的时候就要使用多种遍历方式,而且还会暴露元素的内部结构,可以考虑使用迭代器模式解决。
    迭代器模式。

  • 角色:

    • 迭代器接口Iterator,系统提供,有hasNext、next、remove方法。
    • 具体的迭代器,实现迭代功能。
    • 聚合接口Aggregate。
    • 具体的聚合接口,持有被遍历的集合。提供一个方法,返回具体的迭代器。
  • 类图:

  • 代码:

    //具体的迭代器
    public class CSIterator implements Iterator {
        Department[] departments;
        int position = 0;   //索引
        public CSIterator(Department[] departments) {
            this.departments = departments;
        }
        @Override
        public boolean hasNext() {
            if (position>=departments.length || departments[position]==null){
                return false;
            } else {
                return true;
            }
        }
        @Override
        public Department next() {
            position += 1;
            return departments[position-1];
        }
    }
    
    public class EEIterator implements Iterator {
        List<Department> departments;
        int index = -1;   //索引
        public EEIterator(List<Department> departments) {
            this.departments = departments;
        }
        @Override
        public boolean hasNext() {
            if (index>=departments.size()-1) {
                return false;
            } else {
                index += 1;
                return true;
            }
        }
        @Override
        public Department next() {
            return departments.get(index);
        }
    }
    
    //聚合接口
    public interface College {
        String getName();
        void addDepartment(String name);
        Iterator createIterator();
    }
    
    //聚合实现
    public class CSCollege implements College {
        Department[] departments;
        int num = 0;    //个数
        public CSCollege(int size) {
            this.departments = new Department[size];
        }
        @Override
        public String getName() {
            return "CS";
        }
        @Override
        public void addDepartment(String name) {
            departments[num] = new Department(name);
            num += 1;
        }
        @Override
        public Iterator createIterator() {
            return new CSIterator(departments);
        }
    }
    
    public class EECollege implements College {
        List<Department> departments;
        public EECollege() {
            this.departments = new ArrayList<>();
        }
        @Override
        public String getName() {
            return "EE";
        }
        @Override
        public void addDepartment(String name) {
            departments.add(new Department(name));
        }
        @Override
        public Iterator createIterator() {
            return new EEIterator(departments);
        }
    }
    
    //调用
    public class University {
      List<College> colleges;
      public University(List<College> colleges) {
          this.colleges = colleges;
      }
      public void printCollege() {
          for (College college : colleges) {
              System.out.println(college);
              printDepartment(college);
          }
      }
      public void printDepartment(College college) {
          Iterator iterator = college.createIterator();
          while (iterator.hasNext()) {
              System.out.println(((Department)iterator.next()).getName());
          }
      }
    }
    List<College> colleges = new ArrayList<>();
    CSCollege csCollege = new CSCollege(3);
    csCollege.addDepartment("Java");
    csCollege.addDepartment("Python");
    csCollege.addDepartment("PHP");
    colleges.add(csCollege);
    EECollege eeCollege = new EECollege();
    eeCollege.addDepartment("FPGA");
    eeCollege.addDepartment("Arduino");
    colleges.add(eeCollege);
    University university = new University(colleges);
    university.printCollege();
    
  • 源码分析:

    • JDK中的ArrayList中用到了迭代器模式。
    • List接口相当于聚合接口,ArrayList相当于聚合实现。迭代器实现iter是ArrayList的内部类。
  • 注意事项:

    • 优点:
      • 提供一个统一的方法遍历对象,客户不用再考虑聚合的类型,使用一种方法就可以遍历对象了。
      • 隐藏了聚合的内部结构,客户端要遍历聚合的时候只能取到迭代器,而不会知道聚合的具体组成。
      • 提供了一种设计思想,就是一个类应该只有一个引起变化的原因(叫做单一责任原则)。在聚合类中,我们把迭代器分开,就是要把管理对象集合和遍历对象集合的责任分开,这样一来集合改变的话,只影响到聚合对象。而如果遍历方式改变的话,只影响到了迭代器。
      • 当要展示一组相似对象,或者遍历一组相同对象时使用,适合使用迭代器模式。
    • 缺点:
      • 每个聚合对象都要一个迭代器,会生成多个迭代器不好管理类。

6、观察者模式

  • 示例:

    • 气象站每天发布气象数据,需要设计API便于的三分接入获取数据。如果数据发生变化,也要实时改变。
    • 把气象数据封装为一个对象,并且提供获取数据和改变数据的方法。
  • 观察者模式(Observer pattern),是对象之间多对一依赖的一种设计方案,被依赖的对象为Subject(一的一方),依赖的对象为Observer(多的一方),subject通知Observer变化。即通过类似于订阅-发布模式来实现对对象的观察。

  • 角色:

    • 主题接口Subject,发布数据。包括注册观察者、移除观察者和通知观察者的方法。
    • 主题实现类,实现Subject中的相关方法,其中聚合了许多观察者。
    • 观察者接口Observer,接收更新的数据输入。
    • 观察者实现。
  • 类图:

  • 代码:

    //主题接口
    public interface Subject {
        void registerObserver(Observer o);
        void removeObserver(Observer o);
        void notifyObservers();
    }
    
    //主题实现
    public class WeatherData implements Subject {
        private List<Observer> list;
        private float dataA;
        private float dataB;
        public WeatherData() {
            this.list = new ArrayList<>();
        }
        public void setData(float dataA, float dataB) {
            this.dataA = dataA;
            this.dataB = dataB;
            notifyObservers();
        }
        @Override
        public void registerObserver(Observer o) {
            list.add(o);
        }
        @Override
        public void removeObserver(Observer o) {
            list.remove(o);
        }
        @Override
        public void notifyObservers() {
            for (Observer observer : list) {
                observer.update(dataA,dataB);
            }
        }
    }
    
    //观察者接口
    public interface Observer {
        void update(float dataA, float dataB);
    }
    
    //观察者实现
    public class CurrentCondition implements Observer {
        privatWeatherData weatherData = new WeatherData();
            CurrentCondition condition = new CurrentCondition();
            weatherData.registerObserver(condition);
            weatherData.setData(10.1F,17.3F);e float dataA;
        private float dataB;
        @Override
        public void update(float dataA, float dataB) {
            this.dataA = dataA;
            this.dataB = dataB;
            display();
        }
        public void display() {
            System.out.println(this+"dataA:"+dataA);
            System.out.println(this+"dataB:"+dataB);
        }
    }
    
    //使用
    WeatherData weatherData = new WeatherData();
    CurrentCondition condition = new CurrentCondition();
    weatherData.registerObserver(condition);
    weatherData.setData(10.1F,17.3F);
    
  • 源码分析:

    • JDK中的Observable中使用了观察者模式。
    • Observable类似于主题接口,Observer类似于观察者接口。
  • 注意事项:

    • 观察者模式设计后,会以集合的方式来管理用户(Observer),包括注册,移除和通知。
    • 这样,增加观察者(这里可以理解成一个新的公告板),就不需要去修改核心类,遵守了ocp原则。

iwehdio的博客园:https://www.cnblogs.com/iwehdio/
原文地址:https://www.cnblogs.com/iwehdio/p/13974345.html