您好,登錄后才能下訂單哦!
今天小編給大家分享一下java訪問者模式的靜態(tài)動態(tài)及偽動態(tài)分派實例分析的相關(guān)知識點,內(nèi)容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
每到年底,管理層就要開始評定員工一年的工作績效,員工分為工程師和經(jīng)理;管理層有CEO和CTO。那么CTO關(guān)注工程師的代碼量、經(jīng)理的新產(chǎn)品數(shù)量;CEO關(guān)注工程師的KPI、經(jīng)理的KPI及新產(chǎn)品數(shù)量。
由于CEO和CTO對于不同的員工的關(guān)注點是不一樣的,這就需要對不同的員工類型進行不同的處理。此時,訪問者模式可以派上用場了,來看代碼。
//員工基類 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()方法表示接受訪問者的訪問,由具體的子類來實現(xiàn)。訪問者是一個接口,傳入不同的實現(xiàn)類,可訪問不同的數(shù)據(jù)。下面看工程師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); } }
經(jīng)理Manager類的代碼如下。
//經(jīng)理 public class Manager extends Employee { public Manager(String name) { super(name); } @Override public void accept(IVisitor visitor) { visitor.visit(this); } //一年做的新產(chǎn)品數(shù)量 public int getProducts() { return new Random().nextInt(10); } }
工程師被考核的是代碼量,經(jīng)理被考核的是新產(chǎn)品數(shù)量,二者的職責不一樣。也正是因為有這樣的差異性,才使得訪問模式能夠在這個場景下發(fā)揮作用。Employee、Engineer、Manager 3個類型相當于數(shù)據(jù)結(jié)構(gòu),這些類型相對穩(wěn)定,不會發(fā)生變化。
將這些員工添加到一個業(yè)務(wù)報表類中,公司高層可以通過該報表類的showReport()方法查看所有員工的業(yè)績,代碼如下。
//員工業(yè)務(wù)報表類 public class BusinessReport { private List<Employee> employees = new LinkedList<Employee>(); public BusinessReport() { employees.add(new Manager("經(jīng)理-A")); employees.add(new Engineer("工程師-A")); employees.add(new Engineer("工程師-B")); employees.add(new Engineer("工程師-C")); employees.add(new Manager("經(jīng)理-B")); employees.add(new Engineer("工程師-D")); } /** * 為訪問者展示報表 * @param visitor 公司高層,如CEO、CTO */ public void showReport(IVisitor visitor) { for (Employee employee : employees) { employee.accept(visitor); } } }
下面來看訪問者類型的定義,訪問者聲明了兩個visit()方法,分別對工程師和經(jīng)理訪問,代碼如下。
public interface IVisitor { //訪問工程師類型 void visit(Engineer engineer); //訪問經(jīng)理類型 void visit(Manager manager); }
上面代碼定義了一個IVisitor接口,該接口有兩個visit()方法,參數(shù)分別是Engineer和Manager,也就是說對于Engineer和Manager的訪問會調(diào)用兩個不同的方法,以此達到差異化處理的目的。這兩個訪問者具體的實現(xiàn)類為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("經(jīng)理: " + manager.name + ", KPI: " + manager.kpi + ", 新產(chǎn)品數(shù)量: " + manager.getProducts()); } }
在CEO的訪問者中,CEO關(guān)注工程師的KPI、經(jīng)理的KPI和新產(chǎn)品數(shù)量,通過兩個visit()方法分別進行處理。如果不使用訪問者模式,只通過一個visit()方法進行處理,則需要在這個visit()方法中進行判斷,然后分別處理,代碼如下。
public class ReportUtil { public void visit(Employee employee) { if (employee instanceof Manager) { Manager manager = (Manager) employee; System.out.println("經(jīng)理: " + manager.name + ", KPI: " + manager.kpi + ", 新產(chǎn)品數(shù)量: " + manager.getProducts()); } else if (employee instanceof Engineer) { Engineer engineer = (Engineer) employee; System.out.println("工程師: " + engineer.name + ", KPI: " + engineer.kpi); } } }
這就導致了if...else邏輯的嵌套及類型的強制轉(zhuǎn)換,難以擴展和維護,當類型較多時,這個ReportUtil就會很復雜。而使用訪問者模式,通過同一個函數(shù)對不同的元素類型進行相應(yīng)處理,使結(jié)構(gòu)更加清晰、靈活性更高。然后添加一個CTO的訪問者類CTOVisitor。
public class CTOVisitor implements IVisitor { public void visit(Engineer engineer) { System.out.println("工程師: " + engineer.name + ", 代碼行數(shù): " + engineer.getCodeLines()); } public void visit(Manager manager) { System.out.println("經(jīng)理: " + manager.name + ", 產(chǎn)品數(shù)量: " + manager.getProducts()); } }
重載的visit()方法會對元素進行不同的操作,而通過注入不同的訪問者又可以替換掉訪問者的具體實現(xiàn),使得對元素的操作變得更靈活,可擴展性更高,同時,消除了類型轉(zhuǎn)換、if...else等“丑陋”的代碼。
客戶端測試代碼如下。
public static void main(String[] args) { //構(gòu)建報表 BusinessReport report = new BusinessReport(); System.out.println("=========== CEO看報表 ==========="); report.showReport(new CEOVisitor()); System.out.println("=========== CTO看報表 ==========="); report.showReport(new CTOVisitor()); }
運行結(jié)果如下圖所示。
file
在上述案例中,Employee扮演了Element角色,Engineer和Manager都是 ConcreteElement,CEOVisitor和CTOVisitor都是具體的Visitor對象,BusinessReport就是ObjectStructure。
訪問者模式最大的優(yōu)點就是增加訪問者非常容易,從代碼中可以看到,如果要增加一個訪問者,則只要新實現(xiàn)一個訪問者接口的類,從而達到數(shù)據(jù)對象與數(shù)據(jù)操作相分離的效果。如果不使用訪問者模式,而又不想對不同的元素進行不同的操作,則必定需要使用if...else和類型轉(zhuǎn)換,這使得代碼難以升級維護。
我們要根據(jù)具體情況來評估是否適合使用訪問者模式。例如,對象結(jié)構(gòu)是否足夠穩(wěn)定,是否需要經(jīng)常定義新的操作,使用訪問者模式是否能優(yōu)化代碼,而不使代碼變得更復雜。
變量被聲明時的類型叫作變量的靜態(tài)類型(Static Type),有些人又把靜態(tài)類型叫作明顯類型(Apparent Type);而變量所引用的對象的真實類型又叫作變量的實際類型(Actual Type)。比如:
List list = null; list = new ArrayList();
上面代碼聲明了一個變量list,它的靜態(tài)類型(也叫作明顯類型)是List,而它的實際類型是ArrayList。根據(jù)對象的類型對方法進行的選擇,就是分派(Dispatch)。分派又分為兩種,即靜態(tài)分派和動態(tài)分派。
靜態(tài)分派(Static Dispatch)就是按照變量的靜態(tài)類型進行分派,從而確定方法的執(zhí)行版本,靜態(tài)分派在編譯期就可以確定方法的版本。而靜態(tài)分派最典型的應(yīng)用就是方法重載,來看下面的代碼。
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); } }
在靜態(tài)分派判斷的時候,根據(jù)多個判斷依據(jù)(即參數(shù)類型和個數(shù))判斷出方法的版本,這就是多分派的概念,因為我們有一個以上的考量標準,所以Java是靜態(tài)多分派的語言。
對于動態(tài)分派,與靜態(tài)分派相反,它不是在編譯期確定的方法版本,而是在運行時才能確定的。而動態(tài)分派最典型的應(yīng)用就是多態(tài)的特性。舉個例子,來看下面的代碼。
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(); } }
這段代碼的輸出結(jié)果為依次打印男人和女人,然而這里的test()方法版本,無法根據(jù)Man和Woman的靜態(tài)類型判斷,他們的靜態(tài)類型都是Person接口,根本無從判斷。
顯然,產(chǎn)生這樣的輸出結(jié)果,就是因為test()方法的版本是在運行時判斷的,這就是動態(tài)分派。
動態(tài)分派判斷的方法是在運行時獲取Man和Woman的實際引用類型,再確定方法的版本,而由于此時判斷的依據(jù)只是實際引用類型,只有一個判斷依據(jù),所以這就是單分派的概念,這時考量標準只有一個,即變量的實際引用類型。相應(yīng)地,這說明Java是動態(tài)單分派的語言。
通過前面的分析,我們知道Java是靜態(tài)多分派、動態(tài)單分派的語言。Java底層不支持動態(tài)雙分派。但是通過使用設(shè)計模式,也可以在Java里實現(xiàn)偽動態(tài)雙分派。在訪問者模式中使用的就是偽動態(tài)雙分派。所謂動態(tài)雙分派就是在運行時依據(jù)兩個實際類型去判斷一個方法的運行行為,而訪問者模式實現(xiàn)的手段是進行兩次動態(tài)單分派來達到這個效果。
還是回到前面的KPI考核業(yè)務(wù)場景中,BusinessReport類中的showReport()方法的代碼如下。
public void showReport(IVisitor visitor) { for (Employee employee : employees) { employee.accept(visitor); } }
這里依據(jù)Employee和IVisitor兩個實際類型決定了showReport()方法的執(zhí)行結(jié)果,從而決定了accept()方法的動作。
accept()方法的調(diào)用過程分析如下。
(1)當調(diào)用accept()方法時,根據(jù)Employee的實際類型決定是調(diào)用Engineer還是Manager的accept()方法。
(2)這時accept()方法的版本已經(jīng)確定,假如是Engineer,則它的accept()方法調(diào)用下面這行代碼。
public void accept(IVisitor visitor) { visitor.visit(this); }
此時的this是Engineer類型,因此對應(yīng)的是IVisitor接口的visit(Engineer engineer)方法,此時需要再根據(jù)訪問者的實際類型確定visit()方法的版本,如此一來,就完成了動態(tài)雙分派的過程。
以上過程通過兩次動態(tài)雙分派,第一次對accept()方法進行動態(tài)分派,第二次對訪問者的visit()方法進行動態(tài)分派,從而達到根據(jù)兩個實際類型確定一個方法的行為的效果。
而原本的做法通常是傳入一個接口,直接使用該接口的方法,此為動態(tài)單分派,就像策略模式一樣。在這里,showReport()方法傳入的訪問者接口并不是直接調(diào)用自己的visit()方法,而是通過Employee的實際類型先動態(tài)分派一次,然后在分派后確定的方法版本里進行自己的動態(tài)分派。
注:這里確定accept(IVisitor visitor)方法是由靜態(tài)分派決定的,所以這個并不在此次動態(tài)雙分派的范疇內(nèi),而且靜態(tài)分派是在編譯期完成的,所以accept(IVisitor visitor)方法的靜態(tài)分派與訪問者模式的動態(tài)雙分派并沒有任何關(guān)系。動態(tài)雙分派說到底還是動態(tài)分派,是在運行時發(fā)生的,它與靜態(tài)分派有著本質(zhì)上的區(qū)別,不可以說一次動態(tài)分派加一次靜態(tài)分派就是動態(tài)雙分派,而且訪問者模式的雙分派本身也是另有所指。
而this的類型不是動態(tài)分派確定的,把它寫在哪個類中,它的靜態(tài)類型就是哪個類,這是在編譯期就確定的,不確定的是它的實際類型,請小伙伴們也要區(qū)分開來。
首先來看JDK的NIO模塊下的FileVisitor接口,它提供了遞歸遍歷文件樹的支持。這個接口上的方法表示了遍歷過程中的關(guān)鍵過程,允許在文件被訪問、目錄將被訪問、目錄已被訪問、發(fā)生錯誤等過程中進行控制。換句話說,這個接口在文件被訪問前、訪問中和訪問后,以及產(chǎn)生錯誤的時候都有相應(yīng)的鉤子程序進行處理。
調(diào)用FileVisitor中的方法,會返回訪問結(jié)果的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:這個訪問結(jié)果表示當前的遍歷過程將會繼續(xù)。
(2)FileVisitResult.SKIP_SIBLINGS:這個訪問結(jié)果表示當前的遍歷過程將會繼續(xù),但是要忽略當前文件/目錄的兄弟節(jié)點。
(3)FileVisitResult.SKIP_SUBTREE:這個訪問結(jié)果表示當前的遍歷過程將會繼續(xù),但是要忽略當前目錄下的所有節(jié)點。
(4)FileVisitResult.TERMINATE:這個訪問結(jié)果表示當前的遍歷過程將會停止。
通過訪問者去遍歷文件樹會比較方便,比如查找文件夾內(nèi)符合某個條件的文件或者某一天內(nèi)所創(chuàng)建的文件,這個類中都提供了相對應(yīng)的方法。它的實現(xiàn)其實也非常簡單,代碼如下。
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; } }
再來看訪問者模式在Spring中的應(yīng)用,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()方法中,訪問了其他數(shù)據(jù),比如父類的名字、自己的類名、在IoC容器中的名稱等各種信息。
以上就是“java訪問者模式的靜態(tài)動態(tài)及偽動態(tài)分派實例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關(guān)注億速云行業(yè)資訊頻道。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。