java訪問者模式的靜態動態及偽動態分派實例分析

蝸牛 互聯網技術資訊 2022-06-24 21 0

今天小編給大家分享一下java訪問者模式的靜態動態及偽動態分派實例分析的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

1 使用訪問者模式實現KPI考核的場景

每到年底,管理層就要開始評定員工一年的工作績效,員工分為工程師和經理;管理層有CEO和CTO。那么CTO關注工程師的代碼量、經理的新產品數量;CEO關注工程師的KPI、經理的KPI及新產品數量。

由于CEO和CTO對于不同的員工的關注點是不一樣的,這就需要對不同的員工類型進行不同的處理。此時,訪問者模式可以派上用場了,來看代碼。

//員工基類
public?abstract?class?Employee?{
????public?String?name;
????public?int?kpi;//員工KPI
????public?Employee(String?name)?{
????????this.name?=?name;
????????kpi?=?new?Random().nextInt(10);
????}
????//核心方法,接受訪問者的訪問
????public?abstract?void?accept(IVisitor?visitor);
}

Employee類定義了員工基本信息及一個accept()方法,accept()方法表示接受訪問者的訪問,由具體的子類來實現。訪問者是一個接口,傳入不同的實現類,可訪問不同的數據。下面看工程師Engineer類的代碼。

//工程師
public?class?Engineer?extends?Employee?{
????public?Engineer(String?name)?{
????????super(name);
????}
????@Override
????public?void?accept(IVisitor?visitor)?{
????????visitor.visit(this);
????}
????//工程師一年的代碼量
????public?int?getCodeLines()?{
????????return?new?Random().nextInt(10?*?10000);
????}
}

經理Manager類的代碼如下。

//經理
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);
????}
}

工程師被考核的是代碼量,經理被考核的是新產品數量,二者的職責不一樣。也正是因為有這樣的差異性,才使得訪問模式能夠在這個場景下發揮作用。Employee、Engineer、Manager 3個類型相當于數據結構,這些類型相對穩定,不會發生變化。

將這些員工添加到一個業務報表類中,公司高層可以通過該報表類的showReport()方法查看所有員工的業績,代碼如下。

//員工業務報表類
public?class?BusinessReport?{
????private?List<Employee>?employees?=?new?LinkedList<Employee>();
????public?BusinessReport()?{
????????employees.add(new?Manager("經理-A"));
????????employees.add(new?Engineer("工程師-A"));
????????employees.add(new?Engineer("工程師-B"));
????????employees.add(new?Engineer("工程師-C"));
????????employees.add(new?Manager("經理-B"));
????????employees.add(new?Engineer("工程師-D"));
????}
????/**
?????*?為訪問者展示報表
?????*?@param?visitor?公司高層,如CEO、CTO
?????*/
????public?void?showReport(IVisitor?visitor)?{
????????for?(Employee?employee?:?employees)?{
????????????employee.accept(visitor);
????????}
????}
}

下面來看訪問者類型的定義,訪問者聲明了兩個visit()方法,分別對工程師和經理訪問,代碼如下。

public?interface?IVisitor?{
????//訪問工程師類型
????void?visit(Engineer?engineer);
????//訪問經理類型
????void?visit(Manager?manager);
}

上面代碼定義了一個IVisitor接口,該接口有兩個visit()方法,參數分別是Engineer和Manager,也就是說對于Engineer和Manager的訪問會調用兩個不同的方法,以此達到差異化處理的目的。這兩個訪問者具體的實現類為CEOVisitor類和CTOVisitor類。首先來看CEOVisitor類的代碼。

//CEO訪問者
public?class?CEOVisitor?implements?IVisitor?{
????public?void?visit(Engineer?engineer)?{
????????System.out.println("工程師:?"?+?engineer.name?+?",?KPI:?"?+?engineer.kpi);
????}
????public?void?visit(Manager?manager)?{
????????System.out.println("經理:?"?+?manager.name?+?",?KPI:?"?+?manager.kpi?+
????????????????",?新產品數量:?"?+?manager.getProducts());
????}
}

在CEO的訪問者中,CEO關注工程師的KPI、經理的KPI和新產品數量,通過兩個visit()方法分別進行處理。如果不使用訪問者模式,只通過一個visit()方法進行處理,則需要在這個visit()方法中進行判斷,然后分別處理,代碼如下。

public?class?ReportUtil?{
????public?void?visit(Employee?employee)?{
????????if?(employee?instanceof?Manager)?{
????????????Manager?manager?=?(Manager)?employee;
????????????System.out.println("經理:?"?+?manager.name?+?",?KPI:?"?+?manager.kpi?+
????????????????????",?新產品數量:?"?+?manager.getProducts());
????????}?else?if?(employee?instanceof?Engineer)?{
????????????Engineer?engineer?=?(Engineer)?employee;
????????????System.out.println("工程師:?"?+?engineer.name?+?",?KPI:?"?+?engineer.kpi);
????????}
????}
}

這就導致了if...else邏輯的嵌套及類型的強制轉換,難以擴展和維護,當類型較多時,這個ReportUtil就會很復雜。而使用訪問者模式,通過同一個函數對不同的元素類型進行相應處理,使結構更加清晰、靈活性更高。然后添加一個CTO的訪問者類CTOVisitor。

public?class?CTOVisitor?implements?IVisitor?{
????public?void?visit(Engineer?engineer)?{
????????System.out.println("工程師:?"?+?engineer.name?+?",?代碼行數:?"?+?engineer.getCodeLines());
????}
????public?void?visit(Manager?manager)?{
????????System.out.println("經理:?"?+?manager.name?+?",?產品數量:?"?+?manager.getProducts());
????}
}

重載的visit()方法會對元素進行不同的操作,而通過注入不同的訪問者又可以替換掉訪問者的具體實現,使得對元素的操作變得更靈活,可擴展性更高,同時,消除了類型轉換、if...else等“丑陋”的代碼。

客戶端測試代碼如下。

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());
}

運行結果如下圖所示。

java訪問者模式的靜態動態及偽動態分派實例分析  java 第1張

file

在上述案例中,Employee扮演了Element角色,Engineer和Manager都是 ConcreteElement,CEOVisitor和CTOVisitor都是具體的Visitor對象,BusinessReport就是ObjectStructure。

訪問者模式最大的優點就是增加訪問者非常容易,從代碼中可以看到,如果要增加一個訪問者,則只要新實現一個訪問者接口的類,從而達到數據對象與數據操作相分離的效果。如果不使用訪問者模式,而又不想對不同的元素進行不同的操作,則必定需要使用if...else和類型轉換,這使得代碼難以升級維護。

我們要根據具體情況來評估是否適合使用訪問者模式。例如,對象結構是否足夠穩定,是否需要經常定義新的操作,使用訪問者模式是否能優化代碼,而不使代碼變得更復雜。

2 從靜態分派到動態分派

變量被聲明時的類型叫作變量的靜態類型(Static Type),有些人又把靜態類型叫作明顯類型(Apparent Type);而變量所引用的對象的真實類型又叫作變量的實際類型(Actual Type)。比如:

List?list?=?null;
list?=?new?ArrayList();

上面代碼聲明了一個變量list,它的靜態類型(也叫作明顯類型)是List,而它的實際類型是ArrayList。根據對象的類型對方法進行的選擇,就是分派(Dispatch)。分派又分為兩種,即靜態分派和動態分派。

2.1 靜態分派

靜態分派(Static Dispatch)就是按照變量的靜態類型進行分派,從而確定方法的執行版本,靜態分派在編譯期就可以確定方法的版本。而靜態分派最典型的應用就是方法重載,來看下面的代碼。

public?class?Main?{
????public?void?test(String?string){
????????System.out.println("string");
????}
????public?void?test(Integer?integer){
????????System.out.println("integer");
????}
????public?static?void?main(String[]?args)?{
????????String?string?=?"1";
????????Integer?integer?=?1;
????????Main?main?=?new?Main();
????????main.test(integer);
????????main.test(string);
????}
}

在靜態分派判斷的時候,根據多個判斷依據(即參數類型和個數)判斷出方法的版本,這就是多分派的概念,因為我們有一個以上的考量標準,所以Java是靜態多分派的語言。

2.2 動態分派

對于動態分派,與靜態分派相反,它不是在編譯期確定的方法版本,而是在運行時才能確定的。而動態分派最典型的應用就是多態的特性。舉個例子,來看下面的代碼。

interface?Person{
????void?test();
}
class?Man?implements?Person{
????public?void?test(){
????????System.out.println("男人");
????}
}
class?Woman?implements?Person{
????public?void?test(){
????????System.out.println("女人");
????}
}
public?class?Main?{
????public?static?void?main(String[]?args)?{
????????Person?man?=?new?Man();
????????Person?woman?=?new?Woman();
????????man.test();
????????woman.test();
????}
}

這段代碼的輸出結果為依次打印男人和女人,然而這里的test()方法版本,無法根據Man和Woman的靜態類型判斷,他們的靜態類型都是Person接口,根本無從判斷。

顯然,產生這樣的輸出結果,就是因為test()方法的版本是在運行時判斷的,這就是動態分派。

動態分派判斷的方法是在運行時獲取Man和Woman的實際引用類型,再確定方法的版本,而由于此時判斷的依據只是實際引用類型,只有一個判斷依據,所以這就是單分派的概念,這時考量標準只有一個,即變量的實際引用類型。相應地,這說明Java是動態單分派的語言。

3 訪問者模式中的偽動態分派

通過前面的分析,我們知道Java是靜態多分派、動態單分派的語言。Java底層不支持動態雙分派。但是通過使用設計模式,也可以在Java里實現偽動態雙分派。在訪問者模式中使用的就是偽動態雙分派。所謂動態雙分派就是在運行時依據兩個實際類型去判斷一個方法的運行行為,而訪問者模式實現的手段是進行兩次動態單分派來達到這個效果。

還是回到前面的KPI考核業務場景中,BusinessReport類中的showReport()方法的代碼如下。

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

這里依據Employee和IVisitor兩個實際類型決定了showReport()方法的執行結果,從而決定了accept()方法的動作。

accept()方法的調用過程分析如下。

(1)當調用accept()方法時,根據Employee的實際類型決定是調用Engineer還是Manager的accept()方法。

(2)這時accept()方法的版本已經確定,假如是Engineer,則它的accept()方法調用下面這行代碼。

????public?void?accept(IVisitor?visitor)?{
????????visitor.visit(this);
????}		

此時的this是Engineer類型,因此對應的是IVisitor接口的visit(Engineer engineer)方法,此時需要再根據訪問者的實際類型確定visit()方法的版本,如此一來,就完成了動態雙分派的過程。

以上過程通過兩次動態雙分派,第一次對accept()方法進行動態分派,第二次對訪問者的visit()方法進行動態分派,從而達到根據兩個實際類型確定一個方法的行為的效果。

而原本的做法通常是傳入一個接口,直接使用該接口的方法,此為動態單分派,就像策略模式一樣。在這里,showReport()方法傳入的訪問者接口并不是直接調用自己的visit()方法,而是通過Employee的實際類型先動態分派一次,然后在分派后確定的方法版本里進行自己的動態分派。

注:這里確定accept(IVisitor visitor)方法是由靜態分派決定的,所以這個并不在此次動態雙分派的范疇內,而且靜態分派是在編譯期完成的,所以accept(IVisitor visitor)方法的靜態分派與訪問者模式的動態雙分派并沒有任何關系。動態雙分派說到底還是動態分派,是在運行時發生的,它與靜態分派有著本質上的區別,不可以說一次動態分派加一次靜態分派就是動態雙分派,而且訪問者模式的雙分派本身也是另有所指。

而this的類型不是動態分派確定的,把它寫在哪個類中,它的靜態類型就是哪個類,這是在編譯期就確定的,不確定的是它的實際類型,請小伙伴們也要區分開來。

4 訪問者模式在JDK源碼中的應用

首先來看JDK的NIO模塊下的FileVisitor接口,它提供了遞歸遍歷文件樹的支持。這個接口上的方法表示了遍歷過程中的關鍵過程,允許在文件被訪問、目錄將被訪問、目錄已被訪問、發生錯誤等過程中進行控制。換句話說,這個接口在文件被訪問前、訪問中和訪問后,以及產生錯誤的時候都有相應的鉤子程序進行處理。

調用FileVisitor中的方法,會返回訪問結果的FileVisitResult對象值,用于決定當前操作完成后接下來該如何處理。FileVisitResult的標準返回值存放在FileVisitResult枚舉類型中,代碼如下。

public?interface?FileVisitor<T>?{
????FileVisitResult?preVisitDirectory(T?dir,?BasicFileAttributes?attrs)
????????throws?IOException;
????FileVisitResult?visitFile(T?file,?BasicFileAttributes?attrs)
????????throws?IOException;
????FileVisitResult?visitFileFailed(T?file,?IOException?exc)
????????throws?IOException;
????FileVisitResult?postVisitDirectory(T?dir,?IOException?exc)
????????throws?IOException;
}

(1)FileVisitResult.CONTINUE:這個訪問結果表示當前的遍歷過程將會繼續。

(2)FileVisitResult.SKIP_SIBLINGS:這個訪問結果表示當前的遍歷過程將會繼續,但是要忽略當前文件/目錄的兄弟節點。

(3)FileVisitResult.SKIP_SUBTREE:這個訪問結果表示當前的遍歷過程將會繼續,但是要忽略當前目錄下的所有節點。

(4)FileVisitResult.TERMINATE:這個訪問結果表示當前的遍歷過程將會停止。

通過訪問者去遍歷文件樹會比較方便,比如查找文件夾內符合某個條件的文件或者某一天內所創建的文件,這個類中都提供了相對應的方法。它的實現其實也非常簡單,代碼如下。

public?class?SimpleFileVisitor<T>?implements?FileVisitor<T>?{
????protected?SimpleFileVisitor()?{
????}
????@Override
????public?FileVisitResult?preVisitDirectory(T?dir,?BasicFileAttributes?attrs)
????????throws?IOException
????{
????????Objects.requireNonNull(dir);
????????Objects.requireNonNull(attrs);
????????return?FileVisitResult.CONTINUE;
????}
????@Override
????public?FileVisitResult?visitFile(T?file,?BasicFileAttributes?attrs)
????????throws?IOException
????{
????????Objects.requireNonNull(file);
????????Objects.requireNonNull(attrs);
????????return?FileVisitResult.CONTINUE;
????}
????@Override
????public?FileVisitResult?visitFileFailed(T?file,?IOException?exc)
????????throws?IOException
????{
????????Objects.requireNonNull(file);
????????throw?exc;
????}
????@Override
????public?FileVisitResult?postVisitDirectory(T?dir,?IOException?exc)
????????throws?IOException
????{
????????Objects.requireNonNull(dir);
????????if?(exc?!=?null)
????????????throw?exc;
????????return?FileVisitResult.CONTINUE;
????}
}

5 訪問者模式在Spring源碼中的應用

再來看訪問者模式在Spring中的應用,Spring IoC中有個BeanDefinitionVisitor類,其中有一個visitBeanDefinition()方法,源碼如下。

public?class?BeanDefinitionVisitor?{
	@Nullable
	private?StringValueResolver?valueResolver;

	public?BeanDefinitionVisitor(StringValueResolver?valueResolver)?{
		Assert.notNull(valueResolver,?"StringValueResolver?must?not?be?null");
		this.valueResolver?=?valueResolver;
	}
	protected?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());
		}
	}
	...
}

我們看到,在visitBeanDefinition()方法中,訪問了其他數據,比如父類的名字、自己的類名、在IoC容器中的名稱等各種信息。

以上就是“java訪問者模式的靜態動態及偽動態分派實例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注蝸牛博客行業資訊頻道。

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:niceseo99@gmail.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

評論

日本韩欧美一级A片在线观看