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.如果觀察者和被觀察者之間存在循環依賴,則可能造成兩者之間循環調用,導致系統崩潰。

