1. 程式人生 > 實用技巧 >設計模式(四)——代理、模板、命令、訪問者、迭代器、觀察者

設計模式(四)——代理、模板、命令、訪問者、迭代器、觀察者

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);
        }
    }
    
    //呼叫
    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/