設計模式14-觀察者模式與訪問者模式詳解


1.14-觀察者模式與訪問者模式詳解

1.14.1.訪問者模式詳解

時長:1h

14.1.1.訪問者模式的定義

 定義:

  訪問者模式【visitor Pattern】,是一種將數據結構與數據操作分離設計模式。是指

封裝一些作用於某種數據結構中的各元素的操作。

特征:

  可以在不改變數據結構的前提下定義作用於這些元素的新操作。

屬於行為型模式。

 

說明:

  訪問者模式,被稱為最復雜的設計模式。運用並不多。

14.1.1.1.訪問者模式在生活中的體現

1.參與KPI考核的人員

  KPI的考核標准,一般是固定不變的,但參與KPI考核的員工會經常變化。

  kpi考核打分的人也會經常變化,

2.餐廳就餐人員

  餐廳吃飯,餐廳的菜單是基本穩定的,就餐人員基本每天都在變化。就餐人員就是一個訪問者。

總結

  訪問者,好像就是變化的元素,與不變的結構【標准,規則】的構成關系處理角色。

14.1.1.2.訪問者模式的適用場景

  訪問者模式很少能用到,一旦需要使用,涉及到的系統往往比較復雜。

1.數據結構穩定,作用於數據結構的操作經常變化的場景。

2.需要數據結構與數據操作分離的場景。

3.需要對不同數據類型(元素) 進行操作,而不使用分支判斷具體類型的場景。

14.1.2.訪問者模式的通用實現

14.1.2.1.類圖設計

 

14.1.2.2.代碼實現

14.1.3.訪問者模式的使用案例之KPI考核

14.1.3.1.類圖設計

 

 

 

 

14.1.3.2.代碼實現
1.元素頂層接口定義
package com.wf.visitor.demo.kpi;

import java.util.Random;

/**
 * @ClassName Employee
 * @Description 員工,元素抽象
 * @Author wf
 * @Date 2020/6/24 10:38
 * @Version 1.0
 */
public abstract class Employee {
    private String name;
    private int kpi;

    public Employee(String name) {
        this.name = name;
        this.kpi = new Random().nextInt(10);
    }

    public abstract void accept(IVisitor visitor);

    public String getName() {
        return name;
    }

    public int getKpi() {
        return kpi;
    }
}

 

2.元素具體實現
package com.wf.visitor.demo.kpi;

import java.util.Random;

/**
 * @ClassName Engineer
 * @Description 普通開發人員
 * @Author wf
 * @Date 2020/6/24 10:43
 * @Version 1.0
 */
public class Engineer extends Employee{
    public Engineer(String name) {
        super(name);
    }

    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
    //考核:代碼量
    public int getCodingLine(){
        return new Random().nextInt(100000);
    }

}
package com.wf.visitor.demo.kpi;

import java.util.Random;

/**
 * @ClassName Manager
 * @Description 項目經理
 * @Author wf
 * @Date 2020/6/24 10:44
 * @Version 1.0
 */
public class Manager extends Employee {
    public Manager(String name) {
        super(name);
    }

    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
    //考核:每年的新產品研發數量
    public int getProducts(){
        return new Random().nextInt(10);
    }
}

 

3.訪問者頂層接口及實現
package com.wf.visitor.demo.kpi;

/**
 * @ClassName IVisitor
 * @Description 訪問者接口
 * @Author wf
 * @Date 2020/6/24 10:41
 * @Version 1.0
 */
public interface IVisitor {
    //傳參具體的元素
    void visit(Engineer engineer);

    void visit(Manager manager);
}
package com.wf.visitor.demo.kpi;

/**
 * @ClassName CTOVisitor
 * @Description ceo考核者,只有看kpi打分就行
 * @Author wf
 * @Date 2020/6/24 10:49
 * @Version 1.0
 */
public class CEOVisitor implements IVisitor{

    @Override
    public void visit(Engineer engineer) {
        System.out.println("工程師:"+engineer.getName()+" ,KPI:"+engineer.getKpi());
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("項目經理:"+manager.getName()+" ,KPI:"+manager.getKpi());
    }
}
package com.wf.visitor.demo.kpi;

/**
 * @ClassName CTOVisitor
 * @Description cto考核者
 * @Author wf
 * @Date 2020/6/24 10:49
 * @Version 1.0
 */
public class CTOVisitor implements IVisitor{

    @Override
    public void visit(Engineer engineer) {
        System.out.println("工程師:"+engineer.getName()+" ,編寫代碼行數:"+engineer.getCodingLine());
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("項目經理:"+manager.getName()+" ,產品數量:"+manager.getProducts());
    }
}

 

4.數據結構定義
package com.wf.visitor.demo.kpi;

import java.util.LinkedList;
import java.util.List;

/**
 * @ClassName BusinessReport
 * @Description 業務報表,數據結構
 * @Author wf
 * @Date 2020/6/24 10:55
 * @Version 1.0
 */
public class BusinessReport {
    private List<Employee> employeeList = new LinkedList<>();

    public BusinessReport() {
        employeeList.add(new Manager("項目經理A"));
        employeeList.add(new Manager("項目經理B"));
        employeeList.add(new Engineer("程序員A"));
        employeeList.add(new Engineer("程序員B"));
        employeeList.add(new Engineer("程序員C"));
    }

    public void showReport(IVisitor visitor){
        for (Employee employee : employeeList) {
            employee.accept(visitor);
        }
    }
}

 

5.測試代碼
package com.wf.visitor.demo.kpi;

/**
 * @ClassName Test
 * @Description 測試類
 * @Author wf
 * @Date 2020/6/24 10:54
 * @Version 1.0
 */
public class Test {
    public static void main(String[] args) {
        BusinessReport report = new BusinessReport();
        System.out.println("===========CEO看報表===============");
        report.showReport(new CEOVisitor());
        System.out.println("===========CTO看報表===============");
        report.showReport(new CTOVisitor());
    }
}

 

測試結果:

 

 

說明:

  訪問者頂層接口定義時,內部會定義visit重載方法,針對不同的訪問元素實現子類進行重載。

  為什么不設計成一個方法呢?

  因為這里一個方法,把具體元素關聯起來。

  當系統需要增加元素實現子類時,只需要增加一個實現子類,該接口中增加一個重載方法。

  系統方便擴展。

14.1.4.訪問者模式擴展---分派

  java中靜態分派,和動態分派。還有雙分派。

  Java中分派,是方法重載的一種特殊形式。即重載方法,方法名相同,參數個數相同,類型不同的形式。

14.1.4.1.java中靜態分派示例代碼
package com.wf.visitor.dispatch;

/**
 * @ClassName Main
 * @Description 測試靜態分派
 * @Author wf
 * @Date 2020/6/24 11:20
 * @Version 1.0
 */
public class Main {
    public static void main(String[] args) {
        String str = "1";
        Integer integer = 1;
        Main main = new Main();
        main.test(integer);
        main.test(str);
    }
    public void test(String str){
        System.out.println("String "+str);
    }
    public void test(Integer integer){
        System.out.println("Integer "+integer);
    }
}

 

說明:

  上面測試代碼中,test方法存在兩個重載方法,參數個數相同,類型不同,

  在編譯階段,就能清楚地知道參數類型,稱為靜態分派

  相同方法名,不同類型的不同方法,這種形式也稱為多分派

14.1.4.2.java中動態分派

  在程序編譯階段,不能明確是哪種類型,只有在運行時,才能知道是哪個類型,

稱為動態分派

 

1.定義接口及實現
package com.wf.visitor.dispatch.dynamic;

/**
 * @ClassName Person
 * @Description 接口定義
 * @Author wf
 * @Date 2020/6/24 11:33
 * @Version 1.0
 */
public interface Person {
    void test();
}
package com.wf.visitor.dispatch.dynamic;

/**
 * @ClassName Women
 * @Description 女人
 * @Author wf
 * @Date 2020/6/24 11:35
 * @Version 1.0
 */
public class Women implements Person{
    @Override
    public void test() {
        System.out.println("女人");
    }
}
package com.wf.visitor.dispatch.dynamic;

/**
 * @ClassName Man
 * @Description 男人
 * @Author wf
 * @Date 2020/6/24 11:34
 * @Version 1.0
 */
public class Man implements Person {
    @Override
    public void test() {
        System.out.println("男人");
    }
}

 

2.測試類
package com.wf.visitor.dispatch.dynamic;

/**
 * @ClassName Main
 * @Description 測試類
 * @Author wf
 * @Date 2020/6/24 11:35
 * @Version 1.0
 */
public class Main {
    public static void main(String[] args) {
        Person man = new Man();
        Person women = new Women();
        man.test();
        women.test();
    }
}

 

說明:

  當在編譯期時,man或women並不知道自己是什么類型,只有在運行期,

通過new創建實例時,才能知道具體的類型,所以,是動態分派

14.1.4.3.訪問者模式中偽動態雙分派

在數據結構中,一般會對集合元素進行遍歷處理。如下所示:

 

 

 可以看到,這里是一次動態分派,調用accept方法,具體的類型要到運行時,才能確定。

而且Employee也是一個抽象接口,類型不能確定。

 

 

 當進入到一個子類中,如Engineer,accept方法調用visit方法,傳參this,也是動態分派。需要到

運行時,才能確定類型。【因為需要在運行時創建this實例】

 

 

14.1.5.訪問者模式在源碼中應用

14.1.5.1.jdk中FileVisitor
FileVisitResult visitFile(T file, BasicFileAttributes attrs)
    throws IOException;

 

FileVisitor接口中定義visitFile方法。傳參BasicFileAtributes也是一個接口。

14.1.5.2.spring中BeanDefinitionVisitor
public void visitBeanDefinition(BeanDefinition beanDefinition) {
        visitParentName(beanDefinition);
        visitBeanClassName(beanDefinition);
        visitFactoryBeanName(beanDefinition);
        visitFactoryMethodName(beanDefinition);
        visitScope(beanDefinition);
        if (beanDefinition.hasPropertyValues()) {
            visitPropertyValues(beanDefinition.getPropertyValues());
        }
        if (beanDefinition.hasConstructorArgumentValues()) {
            ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
            visitIndexedArgumentValues(cas.getIndexedArgumentValues());
            visitGenericArgumentValues(cas.getGenericArgumentValues());
        }
    }

 

訪問時,並未改變其中的內容,只是返回相應結果。把數據操作與結構進行分離。

14.1.6.訪問者模式的使用總結

14.1.6.1.優缺點總結

優點:

  1.解耦數據結構與數據操作,使用操作集合可以獨立變化

  2.擴展性好:可以通過擴展訪問者角色,實現對數據集的不同操作

  3.元素具體類型並非單一,訪問者均可操作

  4.各角色職責分離,符合單一職責原則。

缺點:

  1.無法增加元素類型:若系統數據結構對象易於變化,經常有新的數據對象增加進來,

則訪問者類必須增加對應元素的操作,違背開閉原則。

  2.具體元素變更困難:具體元素的增加屬性,刪除屬性等操作會導致對應訪問者類需要

相應的修改,尤其有大量訪問者類時,修改范圍太大。

  3.違背依賴倒置原則:為了達到”區別對待“,訪問者依賴的是具體元素類型,而不是抽象。

1.14.2.觀察者模式詳解

 時長:1h21min

14.2.1.觀察者模式的定義

定義:

  觀察者模式【Observer Pattern】,又叫發布-訂閱【Publish/Subcribe】模式,模型-視圖【Model/View】模式、

源監聽器【Source/Listener】模式,從屬者【Dependents】模式。

  定義一種一對多的關系,一個主題對象可被多個觀察者對象同時監聽,使得每當主題對象狀態變化時,所有

依賴它的對象都會得到通知並被自動更新。

  屬於行為型模式。

14.2.1.1.觀察者模式在生活場景中的應用

  1.App角標通知

  2.起床鬧鍾設置

14.2.1.2.觀察者模式適用場景

1.當一個抽象模型包含兩個方面內容,其中一個方面依賴於另一個方面。

2.其他一個或多個對象的變化依賴於另一個對象的變化。

3.實現類似廣播機制的功能,無需知道具體收聽者,只需分發廣播,系統中感興趣

的對象會自動接收該廣播。

4.多層嵌套使用,形成一種鏈式觸發機制,使得事件具備跨域(跨越兩種觀察者類型)通知。

14.2.2.觀察者模式的通用實現

14.2.2.1.類圖設計

14.2.2.2.代碼實現

14.2.2.觀察者模式應用案例之問答提示角標

  在學習社區,我們有疑問,可以發布問題求助。發布問題時,可以邀請某個人【或某個老師】進行

解答。

  但是,老師平時會很忙,不會總是去刷新頁面。於是,就會做一個通知功能。

  一旦有一個問題,向老師提出,通知圖標上就會數字加1.

  當老師登錄到頁面時,只要查看通知角標,就能知道是否有人向他提問,就方便回答。

14.2.2.1.類圖設計

 

 

14.2.2.2.代碼實現

說明:這里是基於jdk的發布-訂閱api實現。

1.被觀察者定義
package com.wf.observer.demo.gper;

import java.util.Observable;

/**
 * @ClassName GPer
 * @Description 社區生態圈,被觀察者
 * @Author wf
 * @Date 2020/6/24 17:31
 * @Version 1.0
 */
public class GPer extends Observable {
    private String name = "GPer 生態圈";

    public String getName() {
        return name;
    }

    private static final GPer gper = new GPer();

    private GPer() {
    }

    public static GPer getInstance(){
        return gper;
    }

    public void publishQuestion(Question question){
        System.out.println(question.getUserName()+" 在" +this.name +"提交了一個問題");
        //調用jdk api
        setChanged();
        notifyObservers(question);
    }
}

 

2.數據結構,問題類
package com.wf.observer.demo.gper;

/**
 * @ClassName Question
 * @Description 問題
 * @Author wf
 * @Date 2020/6/24 17:34
 * @Version 1.0
 */
public class Question {
    //問題發布者
    private String userName;
    //內容
    private String content;

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getUserName() {
        return userName;
    }

    public String getContent() {
        return content;
    }
}

 

3.觀察者定義
package com.wf.observer.demo.gper;

import java.util.Observable;
import java.util.Observer;

/**
 * @ClassName Teacher
 * @Description 觀察者
 * @Author wf
 * @Date 2020/6/24 17:38
 * @Version 1.0
 */
public class Teacher implements Observer {
    private String name;

    public Teacher(String name) {
        this.name = name;
    }

    @Override
    public void update(Observable ob, Object arg) {
        GPer gper = (GPer) ob;
        Question question = (Question) arg;
        System.out.println("===================");
        System.out.println(name+"老師,你好\n" +
                ",您收到一個來自"+gper.getName()+"的提問,希望你解答,問題內容如下:\n"+question.getContent()+
                "\n提問者:"+question.getUserName());
    }
}

 

4.測試類
package com.wf.observer.demo.gper;

import javax.management.Query;

/**
 * @ClassName Test
 * @Description 測試類
 * @Author wf
 * @Date 2020/6/24 17:44
 * @Version 1.0
 */
public class Test {
    public static void main(String[] args) {
        GPer gper = GPer.getInstance();
        Teacher tom = new Teacher("tom");
        Teacher jerry = new Teacher("Jerry");

        gper.addObserver(tom);
        gper.addObserver(jerry);

        //用戶行為
        Question question = new Question();
        question.setUserName("張三");
        question.setContent("觀察者模式適用於哪些場景?");
        gper.publishQuestion(question);
    }
}

 

測試結果:

 

14.2.4.google開源組件實現觀察者模式

依賴包:

<dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>18.0</version>
    </dependency>

 

14.2.4.1.示例代碼
1.觀察者類
package com.wf.observer.guava;

import com.google.common.eventbus.Subscribe;

/**
 * @ClassName GuavaEvent
 * @Description 觀察者
 * @Author wf
 * @Date 2020/6/24 18:07
 * @Version 1.0
 */
public class GuavaEvent {
    //表示觀察者回調
    @Subscribe
    public void observer(String str){
        System.out.println("執行observer方法,傳參為:"+str);
    }
}

 

2.測試類
package com.wf.observer.guava;

import com.google.common.eventbus.EventBus;

/**
 * @ClassName Test
 * @Description 測試類
 * @Author wf
 * @Date 2020/6/24 18:09
 * @Version 1.0
 */
public class Test {
    public static void main(String[] args) {
        EventBus eventBus = new EventBus();
        GuavaEvent event = new GuavaEvent();
        eventBus.register(event);

        eventBus.post("tom");
    }
}

 

測試結果:

 

14.2.6.觀察者模式應用案例之鼠標事件交互

 當鼠標發起某個動作【如:單擊,移動,滾動。。。】,得到相應的響應。

針對發出動作,需要進行監聽【現實中操作系統進行監聽】

鼠標:當成被觀察者實現。

事件監聽器:監聽動作【觸發一個事件】

業務類:事件類【傳遞參數,區分不同事件或動作】

事件回調:監聽后,需要作出響應,進行回調。充當觀察者。

14.2.6.1.類圖設計

 

 

14.2.6.2.代碼實現
1.事件監聽器
package com.wf.observer.mouseclick.core;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName EventListener
 * @Description 事件監聽器,被觀察者的抽象
 * @Author wf
 * @Date 2020/6/24 18:21
 * @Version 1.0
 */
public class EventListener {
    protected Map<String, Event> events = new HashMap<String,Event>();

    public void addListener(String eventType, Object target, Method callback){
        events.put(eventType,new Event(target,callback));
    }
    public void addListener(String eventType, Object target){
        try{
            this.addListener(eventType,target,target.getClass().getMethod("on"+ toUpperFirstCase(eventType),Event.class));

        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private String toUpperFirstCase(String eventType) {
        char [] chars = eventType.toCharArray();
        if(chars[0]> 'a' && chars[0] < 'z'){
            chars[0] -= 32;
        }
        return String.valueOf(chars);
    }

    private void trigger(Event event){
        event.setSource(this);
        event.setTime(System.currentTimeMillis());
        try{
            if(null != event.getCallback()){
                //反射調用 回調函數
                event.getCallback().invoke(event.getTarget(),event);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    protected void trigger(String trigger){
        if(!this.events.containsKey(trigger)){return;}
        //如果已進行注冊,回調
        trigger(this.events.get(trigger).setTrigger(trigger));

    }
}

 

2.被監聽對象,鼠標類
package com.wf.observer.mouseclick.event;

import com.wf.observer.mouseclick.core.EventListener;

/**
 * @ClassName Mouse
 * @Description 具體的被觀察者
 * @Author wf
 * @Date 2020/6/24 18:21
 * @Version 1.0
 */
public class Mouse extends EventListener {
    public void click() {
        System.out.println("調用單機方法");
        this.trigger(MouseEventType.ON_CLICK);
    }
}

 

3.事件回調
package com.wf.observer.mouseclick.event;

import com.wf.observer.mouseclick.core.Event;

/**
 * @ClassName MouseCallback
 * @Description 事件響應,回調,觀察者
 * @Author wf
 * @Date 2020/6/24 18:22
 * @Version 1.0
 */
public class MouseEventCallback {
    public void onClick(Event event){
        System.out.println("=============觸發鼠標單擊事件========\n"+event);
    }

    public void onMove(Event event){
        System.out.println("觸發鼠標雙擊事件");
    }

}

 

4.傳參bean,事件類 
package com.wf.observer.mouseclick.core;

import java.lang.reflect.Method;

/**
 * @ClassName Event
 * @Description 事件抽象,傳參對象
 * @Author wf
 * @Date 2020/6/24 18:22
 * @Version 1.0
 */
public class Event {
    //事件源,如:鼠標,鍵盤
    private Object source;
    //事件觸發,要通知誰(觀察者)
    private Object target;

    private Method callback;
    //事件名稱
    private String trigger;
    //事件觸發時間
    private Long time;

    public Event(Object target, Method callback) {
        this.target = target;
        this.callback = callback;
    }

    public Object getSource() {
        return source;
    }

    public Event setSource(Object source) {
        this.source = source;
        return this;
    }

    public Object getTarget() {
        return target;
    }

    public Event setTarget(Object target) {
        this.target = target;
        return this;
    }

    public Method getCallback() {
        return callback;
    }

    public Event setCallback(Method callback) {
        this.callback = callback;
        return this;
    }

    public String getTrigger() {
        return trigger;
    }

    public Event setTrigger(String trigger) {
        this.trigger = trigger;
        return this;
    }

    public Long getTime() {
        return time;
    }

    public Event setTime(Long time) {
        this.time = time;
        return this;
    }


    @Override
    public String toString() {
        return "Event{" +
                "source=" + source +
                ", target=" + target +
                ", callback=" + callback +
                ", trigger='" + trigger + '\'' +
                ", time=" + time +
                '}';
    }
}

 

5.事件類型常量定義
package com.wf.observer.mouseclick.event;

/**
 * @ClassName MouseEventType
 * @Description 鼠標事件
 * @Author wf
 * @Date 2020/6/28 10:47
 * @Version 1.0
 */
public interface MouseEventType {
    String ON_CLICK = "click";
}

 

6.測試類
package com.wf.observer.mouseclick;

import com.wf.observer.mouseclick.event.Mouse;
import com.wf.observer.mouseclick.event.MouseEventCallback;
import com.wf.observer.mouseclick.event.MouseEventType;

/**
 * @ClassName Test
 * @Description 測試類
 * @Author wf
 * @Date 2020/6/28 11:15
 * @Version 1.0
 */
public class Test {
    public static void main(String[] args) {
        MouseEventCallback callback = new MouseEventCallback();
        Mouse mouse = new Mouse();
        mouse.addListener(MouseEventType.ON_CLICK,callback);

        mouse.click();
    }
}

 

測試結果如下:

 

14.2.7.觀察者模式在源碼中的應用

14.2.7.1.jdk中ServletContextListener

  實際上,觀察者模式的典型提示,*Listener【監聽器】

public interface ServletContextListener extends EventListener {

    public void contextInitialized ( ServletContextEvent sce );
    public void contextDestroyed ( ServletContextEvent sce );
}

 

14.2.7.2.spring中ContextLoaderListener
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }

    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }

    public void contextInitialized(ServletContextEvent event) {
        this.initWebApplicationContext(event.getServletContext());
    }

    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

 

14.2.8.觀察者的使用總結

14.2.8.1.優點總結

1.觀察者與被觀察者是松耦合的,符合依賴倒置原則。

2.分離表示層【觀察者】和數據邏輯層【被觀察者】,並且建立了一套觸發機制,使得數據的變化可以

響應到多個表示層上。

3.實現一對多的通訊機制,支持事件注冊機制,支持興趣分發機制,當被觀察者觸發事件時,只有感興趣

的觀察者可以接收到通知。

14.2.8.2.缺點總結

1.如果觀察者數量過多,,則事件通知會耗時較長。

2.事件通知呈線性關系,如果其中一個觀察者處理事件卡殼,會影響后續的觀察者接收該事件。

3.如果觀察者和被觀察者之間存在循環依賴,則可能造成兩者之間循環調用,導致系統崩潰。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM