Java設(shè)計模式之java訪問者模式詳解
介紹
- 訪問者模式,是行為型設(shè)計模式之一
- 訪問者模式是一種將數(shù)據(jù)操作與數(shù)據(jù)結(jié)構(gòu)分離的設(shè)計模式
- 訪問者模式的基本思想: 軟件系統(tǒng)中擁有一個由許多對象構(gòu)成的、比較穩(wěn)定的對象結(jié)構(gòu),這些對象的類都擁有一個 accept 方法用來接受訪問者對象的訪問
- 訪問者是一個接口,它擁有一個 visit 方法,這個方法對訪問到的對象結(jié)構(gòu)中不同類型的元素做出不同的處理
- 在對象結(jié)構(gòu)的一次訪問過程中,我們遍歷整個對象結(jié)構(gòu),對每一個元素都實施 accept 方法,在每一個元素的 accept 方法中會調(diào)用訪問者的 visit 方法,從而使訪問者得以處理對象結(jié)構(gòu)的每一個元素,我們可以針對對象結(jié)構(gòu)設(shè)計不同的訪問者類來完成不同的操作,達到區(qū)別對待的效果。
定義及使用場景
定義:封裝一些作用于某種數(shù)據(jù)結(jié)構(gòu)中的各元素的操作,它可以在不改變這個數(shù)據(jù)結(jié)構(gòu)的前提下定義作用于這些元素的新的操作。
可以對定義這么理解:有這么一個操作,它是作用于一些元素之上的,而這些元素屬于某一個對象結(jié)構(gòu)。同時這個操作是在不改變各元素類的前提下,在這個前提下定義新操作是訪問者模式精髓中的精髓。
使用場景:
(1)對象結(jié)構(gòu)比較穩(wěn)定,但經(jīng)常需要在此對象結(jié)構(gòu)上定義新的操作。
(2)需要對一個對象結(jié)構(gòu)中的對象進行很多不同的且不相關(guān)的操作,而需要避免這些操作“污染”這些對象的類,也不希望在增加新操作時修改這些類。
UML類圖
角色
(1)Visitor:接口或者抽象類,它定義了對每一個元素(Element)訪問的行為,它的參數(shù)就是可以訪問的元素,它的方法數(shù)理論上來講與元素個數(shù)是一樣的,因此,訪問者模式要求元素的類族要穩(wěn)定,如果經(jīng)常添加、移除元素類,必然會導致頻繁地修改Visitor接口,如果這樣則不適合使用訪問者模式。
(2)ConcreteVisitor1、ConcreteVisitor2:具體的訪問類,它需要給出對每一個元素類訪問時所產(chǎn)生的具體行為。
(3)Element:元素接口或者抽象類,它定義了一個接受訪問者的方法(Accept),其意義是指每一個元素都要可以被訪問者訪問。
(4)ConcreteElementA、ConcreteElementB:具體的元素類,它提供接受訪問方法的具體實現(xiàn),而這個具體的實現(xiàn),通常情況下是使用訪問者提供的訪問該元素類的方法。
(5)ObjectStructure:定義當中所說的對象結(jié)構(gòu),對象結(jié)構(gòu)是一個抽象表述,它內(nèi)部管理了元素集合,并且可以迭代這些元素供訪問者訪問。
財務案例
- 財務都是有賬本的,這個賬本就可以作為一個對象結(jié)構(gòu),而它其中的元素有兩種,收入和支出,這滿足我們訪問者模式的要求,即元素的個數(shù)是穩(wěn)定的,因為賬本中的元素只能是收入和支出。
- 而查看賬本的人可能有這樣幾種,比如老板,會計事務所的注會,財務主管,等等。而這些人在看賬本的時候顯然目的和行為是不同的。
首先我們給出單子的接口,它只有一個方法accept。
//單個單子的接口(相當于Element) public interface Bill { //accept void accept(AccountBookViewer accountBookViewer); }
其中的方法參數(shù)AccountBookViewer是一個賬本訪問者接口,接下來也就是實現(xiàn)類,收入單子和消費單子,或者說收入和支出類。
//消費的單子---抽象賬本的實現(xiàn)類 public class ConsumeBill implements Bill{ //消費的金額 private double amount; //消費的武平 private String item; //構(gòu)造方法為這兩個屬性賦值 public ConsumeBill(double amount, String item) { super(); this.amount = amount; this.item = item; } //提供讓訪問者訪問當前賬單中消費部分的方法 public void accept(AccountBookViewer viewer) { //調(diào)用訪問者的view方法,執(zhí)行對消費部分賬單訪問的具體業(yè)務邏輯 viewer.view(this); } public double getAmount() { return amount; } public String getItem() { return item; } }
//收入單子---抽象賬本的另一個實現(xiàn)類 public class IncomeBill implements Bill{ private double amount; private String item; public IncomeBill(double amount, String item) { super(); this.amount = amount; this.item = item; } public void accept(AccountBookViewer viewer) { viewer.view(this); } public double getAmount() { return amount; } public String getItem() { return item; } }
上面最關(guān)鍵的還是里面的accept方法,它直接讓訪問者訪問自己,這相當于一次靜態(tài)分派(文章最后進行解釋),當然我們也可以不使用重載而直接給方法不同的名稱。
接下來是賬本訪問者接口
//賬單查看者接口(相當于Visitor) public interface AccountBookViewer { //查看消費的單子 void view(ConsumeBill bill); //查看收入的單子 void view(IncomeBill bill); }
這兩個方法是重載方法,就是在上面的元素類當中用到的,當然你也可以按照訪問者模式類圖當中的方式去做,將兩個方法分別命名為viewConsumeBill和viewIncomeBill,而一般建議按照類圖上來做的
訪問者的實現(xiàn)
//老板類,查看賬本的類之一 public class Boss implements AccountBookViewer{ private double totalIncome; private double totalConsume; //老板只關(guān)注一共花了多少錢以及一共收入多少錢,其余并不關(guān)心 public void view(ConsumeBill bill) { totalConsume += bill.getAmount(); } public void view(IncomeBill bill) { totalIncome += bill.getAmount(); } public double getTotalIncome() { System.out.println("老板查看一共收入多少,數(shù)目是:" + totalIncome); return totalIncome; } public double getTotalConsume() { System.out.println("老板查看一共花費多少,數(shù)目是:" + totalConsume); return totalConsume; } }
//注冊會計師類,查看賬本的類之一 public class CPA implements AccountBookViewer{ //注會在看賬本時,如果是支出,則如果支出是工資,則需要看應該交的稅交了沒 public void view(ConsumeBill bill) { if (bill.getItem().equals("工資")) { System.out.println("注會查看工資是否交個人所得稅。"); } } //如果是收入,則所有的收入都要交稅 public void view(IncomeBill bill) { System.out.println("注會查看收入交稅了沒。"); } }
老板只關(guān)心收入和支出的總額,而注會只關(guān)注該交稅的是否交稅
接下來是賬本類,它是當前訪問者模式例子中的對象結(jié)構(gòu)
//賬本類(相當于ObjectStruture) public class AccountBook { //單子列表 private List<Bill> billList = new ArrayList<Bill>(); //添加單子 public void addBill(Bill bill){ billList.add(bill); } //供賬本的查看者查看賬本 public void show(AccountBookViewer viewer){ for (Bill bill : billList) { bill.accept(viewer); } } }
賬本類當中有一個列表,這個列表是元素(Bill)的集合,這便是對象結(jié)構(gòu)的通常表示,它一般會是一堆元素的集合,不過這個集合不一定是列表,也可能是樹,鏈表等等任何數(shù)據(jù)結(jié)構(gòu),甚至是若干個數(shù)據(jù)結(jié)構(gòu)。其中show方法,就是賬本類的精髓,它會枚舉每一個元素,讓訪問者訪問。
測試客戶端
public class Client { public static void main(String[] args) { AccountBook accountBook = new AccountBook(); //添加兩條收入 accountBook.addBill(new IncomeBill(10000, "賣商品")); accountBook.addBill(new IncomeBill(12000, "賣廣告位")); //添加兩條支出 accountBook.addBill(new ConsumeBill(1000, "工資")); accountBook.addBill(new ConsumeBill(2000, "材料費")); AccountBookViewer boss = new Boss(); AccountBookViewer cpa = new CPA(); //兩個訪問者分別訪問賬本 accountBook.show(cpa); accountBook.show(boss); ((Boss) boss).getTotalConsume(); ((Boss) boss).getTotalIncome(); } }
上面的代碼中,可以這么理解,賬本以及賬本中的元素是非常穩(wěn)定的,這些幾乎不可能改變,而最容易改變的就是訪問者這部分。
訪問者模式最大的優(yōu)點就是增加訪問者非常容易,我們從代碼上來看,如果要增加一個訪問者,你只需要做一件事即可,那就是寫一個類,實現(xiàn)AccountBookViewer接口,然后就可以直接調(diào)用AccountBook的show方法去訪問賬本了。
如果沒使用訪問者模式,一定會增加許多if else,而且每增加一個訪問者,你都需要改你的if else,代碼會顯得非常臃腫,而且非常難以擴展和維護。
個人心得體會
- 訪問者模式將訪問者和被訪問對象之間進行了解耦,通過一個訪問者的頂層接口和被訪問者的頂層接口達到這個目的
靜態(tài)分派以及動態(tài)分派
變量被聲明時的類型叫做變量的靜態(tài)類型(Static Type),有些人又把靜態(tài)類型叫做明顯類型(Apparent Type);而變量所引用的對象的真實類型又叫做變量的實際類型(Actual Type)。比如:
List list = null; list = new ArrayList();
聲明了一個變量list,它的靜態(tài)類型(也叫明顯類型)是List,而它的實際類型是ArrayList。根據(jù)對象的類型而對方法進行的選擇,就是分派(Dispatch),分派(Dispatch)又分為兩種,即靜態(tài)分派和動態(tài)分派。靜態(tài)分派(Static Dispatch)發(fā)生在編譯時期,分派根據(jù)靜態(tài)類型信息發(fā)生。
靜態(tài)分派
靜態(tài)分派就是按照變量的靜態(tài)類型進行分派,從而確定方法的執(zhí)行版本,靜態(tài)分派在編譯時期就可以確定方法的版本。而靜態(tài)分派最典型的應用就是方法重載
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)相反,它不是在編譯期確定的方法版本,而是在運行時才能確定。而動態(tài)分派最典型的應用就是多態(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ù),所以這就是單分派的概念,這時我們的考量標準只有一個宗量,即變量的實際引用類型。相應的,這說明JAVA是動態(tài)單分派的語言。
訪問者模式中的偽動態(tài)雙分派
訪問者模式中使用的是偽動態(tài)雙分派,所謂的動態(tài)雙分派就是在運行時依據(jù)兩個實際類型去判斷一個方法的運行行為,而訪問者模式實現(xiàn)的手段是進行了兩次動態(tài)單分派來達到這個效果。
到上面例子當中賬本類中的accept方法
for (Bill bill : billList) { bill.accept(viewer); }
這里就是依據(jù)biil和viewer兩個實際類型決定了view方法的版本,從而決定了accept方法的動作。
分析accept方法的調(diào)用過程
1.當調(diào)用accept方法時,根據(jù)bill的實際類型決定是調(diào)用ConsumeBill還是IncomeBill的accept方法。
2.這時accept方法的版本已經(jīng)確定,假如是ConsumeBill,它的accept方法是調(diào)用下面這行代碼。
public void accept(AccountBookViewer viewer) { viewer.view(this); }
此時的this是ConsumeBill類型,所以對應于AccountBookViewer接口的view(ConsumeBill bill)方法,此時需要再根據(jù)viewer的實際類型確定view方法的版本,如此一來,就完成了動態(tài)雙分派的過程。
以上的過程就是通過兩次動態(tài)雙分派,第一次對accept方法進行動態(tài)分派,第二次對view(類圖中的visit方法)方法進行動態(tài)分派,從而達到了根據(jù)兩個實際類型確定一個方法的行為的效果。
而原本我們的做法,通常是傳入一個接口,直接使用該接口的方法,此為動態(tài)單分派,就像策略模式一樣。在這里,show方法傳入的viewer接口并不是直接調(diào)用自己的view方法,而是通過bill的實際類型先動態(tài)分派一次,然后在分派后確定的方法版本里再進行自己的動態(tài)分派。
注意:這里確定view(ConsumeBill bill)方法是靜態(tài)分派決定的,所以這個并不在此次動態(tài)雙分派的范疇內(nèi),而且靜態(tài)分派是在編譯期就完成的,所以view(ConsumeBill bill)方法的靜態(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ū)分開這一點。
對訪問者模式的一些思考
假設(shè)我們上面的例子當中再添加一個財務主管,而財務主管不管你是支出還是收入,都要詳細的查看你的單子的項目以及金額,簡單點說就是財務主管類的兩個view方法的代碼是一樣的。
這里的將兩個view方法抽取的方案是,我們可以將元素提煉出層次結(jié)構(gòu),針對層次結(jié)構(gòu)提供操作的方法,這樣就實現(xiàn)了優(yōu)點當中最后兩點提到的針對層次定義操作以及跨越層次定義操作。
//單個單子的接口(相當于Element) public interface Bill { void accept(Viewer viewer); }
//抽象單子類,一個高層次的單子抽象 public abstract class AbstractBill implements Bill{ protected double amount; protected String item; public AbstractBill(double amount, String item) { super(); this.amount = amount; this.item = item; } public double getAmount() { return amount; } public String getItem() { return item; } }
//收入單子 public class IncomeBill extends AbstractBill{ public IncomeBill(double amount, String item) { super(amount, item); } public void accept(Viewer viewer) { if (viewer instanceof AbstractViewer) { ((AbstractViewer)viewer).viewIncomeBill(this); return; } viewer.viewAbstractBill(this); } }
//消費的單子 public class ConsumeBill extends AbstractBill{ public ConsumeBill(double amount, String item) { super(amount, item); } public void accept(Viewer viewer) { if (viewer instanceof AbstractViewer) { ((AbstractViewer)viewer).viewConsumeBill(this); return; } viewer.viewAbstractBill(this); } }
這是元素類的層次結(jié)構(gòu),可以看到,我們的accept當中出現(xiàn)了if判斷,這里的判斷是在判斷一個層次,這段代碼是不會被更改的。
訪問者層次
//超級訪問者接口(它支持定義高層操作) public interface Viewer{ void viewAbstractBill(AbstractBill bill); }
//比Viewer接口低一個層次的訪問者接口 public abstract class AbstractViewer implements Viewer{ //查看消費的單子 abstract void viewConsumeBill(ConsumeBill bill); //查看收入的單子 abstract void viewIncomeBill(IncomeBill bill); public final void viewAbstractBill(AbstractBill bill){} }
//老板類,查看賬本的類之一,作用于最低層次結(jié)構(gòu) public class Boss extends AbstractViewer{ private double totalIncome; private double totalConsume; //老板只關(guān)注一共花了多少錢以及一共收入多少錢,其余并不關(guān)心 public void viewConsumeBill(ConsumeBill bill) { totalConsume += bill.getAmount(); } public void viewIncomeBill(IncomeBill bill) { totalIncome += bill.getAmount(); } public double getTotalIncome() { System.out.println("老板查看一共收入多少,數(shù)目是:" + totalIncome); return totalIncome; } public double getTotalConsume() { System.out.println("老板查看一共花費多少,數(shù)目是:" + totalConsume); return totalConsume; } }
//注冊會計師類,查看賬本的類之一,作用于最低層次結(jié)構(gòu) public class CPA extends AbstractViewer{ //注會在看賬本時,如果是支出,則如果支出是工資,則需要看應該交的稅交了沒 public void viewConsumeBill(ConsumeBill bill) { if (bill.getItem().equals("工資")) { System.out.println("注會查看是否交個人所得稅。"); } } //如果是收入,則所有的收入都要交稅 public void viewIncomeBill(IncomeBill bill) { System.out.println("注會查看收入交稅了沒。"); } }
//財務主管類,查看賬本的類之一,作用于高層的層次結(jié)構(gòu) public class CFO implements Viewer { //財務主管對每一個單子都要核對項目和金額 public void viewAbstractBill(AbstractBill bill) { System.out.println("財務主管查看賬本時,每一個都核對項目和金額,金額是" + bill.getAmount() + ",項目是" + bill.getItem()); } }
財務主管(CFO)是針對AbstractBill這一層定義的操作,而原來的老板(Boss)和注冊會計師(CPA)都是針對ConsumeBill和IncomeBill這一層定義的操作,這時已經(jīng)產(chǎn)生了跨越層次結(jié)構(gòu)的行為,老板和注冊會計師都跨過了抽象單子這一層,直接針對具體的單子定義操作。
賬本類沒有變化,最后看客戶端的使用
public class Client { public static void main(String[] args) { AccountBook accountBook = new AccountBook(); //添加兩條收入 accountBook.addBill(new IncomeBill(10000, "賣商品")); accountBook.addBill(new IncomeBill(12000, "賣廣告位")); //添加兩條支出 accountBook.addBill(new ConsumeBill(1000, "工資")); accountBook.addBill(new ConsumeBill(2000, "材料費")); Viewer boss = new Boss(); Viewer cpa = new CPA(); Viewer cfo = new CFO(); //兩個訪問者分別訪問賬本 accountBook.show(cpa); accountBook.show(boss); accountBook.show(cfo); ((Boss) boss).getTotalConsume(); ((Boss) boss).getTotalIncome(); } }
回想一下,要是再出現(xiàn)和財務主管一樣對所有單子都是一樣操作的人,我們就不需要復制代碼了,只需要讓他實現(xiàn)Viewer接口就可以了,而如果要像老板和會計一樣區(qū)分單子的具體類型,則繼承AbstractViewer就可以。
總結(jié)
優(yōu)點
1、使得數(shù)據(jù)結(jié)構(gòu)和作用于結(jié)構(gòu)上的操作解耦,使得操作集合可以獨立變化。
2、添加新的操作或者說訪問者會非常容易。
3、將對各個元素的一組操作集中在一個訪問者類當中。
4、使得類層次結(jié)構(gòu)不改變的情況下,可以針對各個層次做出不同的操作,而不影響類層次結(jié)構(gòu)的完整性。
5、可以跨越類層次結(jié)構(gòu),訪問不同層次的元素類,做出相應的操作。
缺點
1、增加新的元素會非常困難。
2、實現(xiàn)起來比較復雜,會增加系統(tǒng)的復雜性。
3、破壞封裝,如果將訪問行為放在各個元素中,則可以不暴露元素的內(nèi)部結(jié)構(gòu)和狀態(tài),但使用訪問者模式的時候,為了讓訪問者能獲取到所關(guān)心的信息,元素類不得不暴露出一些內(nèi)部的狀態(tài)和結(jié)構(gòu),就像收入和支出類必須提供訪問金額和單子的項目的方法一樣。
適用性
1、數(shù)據(jù)結(jié)構(gòu)穩(wěn)定,這里指的是被訪問者的數(shù)據(jù)結(jié)構(gòu)穩(wěn)定,被訪問者的類內(nèi)部結(jié)構(gòu)和繼承體系不會有變化,作用于數(shù)據(jù)結(jié)構(gòu)的操作經(jīng)常變化的時候。
2、當一個數(shù)據(jù)結(jié)構(gòu)中,一些元素類需要負責與其不相關(guān)的操作的時候,為了將這些操作分離出去,以減少這些元素類的職責時,可以使用訪問者模式。
3、有時在對數(shù)據(jù)結(jié)構(gòu)上的元素進行操作的時候,需要區(qū)分具體的類型,這時使用訪問者模式可以針對不同的類型,在訪問者類中定義不同的操作,從而去除掉類型判斷。
參考文章
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
SpringBoot前后端分離解決跨域問題的3種解決方案總結(jié)
前后端分離大勢所趨,跨域問題更是老生常談,下面這篇文章主要給大家介紹了SpringBoot前后端分離解決跨域問題的3種解決方案,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-05-05SpringCloud Eureka服務的基本配置和操作方法
Eureka是Netflix開源的一個基于REST的服務治理框架,主要用于實現(xiàn)微服務架構(gòu)中的服務注冊與發(fā)現(xiàn),Eureka是Netflix開源的服務發(fā)現(xiàn)框架,用于在分布式系統(tǒng)中實現(xiàn)服務的自動注冊與發(fā)現(xiàn),本文介紹SpringCloud Eureka服務的基本配置和操作方法,感興趣的朋友一起看看吧2023-12-12Springboot并發(fā)調(diào)優(yōu)之大事務和長連接
這篇文章主要介紹了Springboot并發(fā)調(diào)優(yōu)之大事務和長連接,重點分享長事務以及長連接導致的并發(fā)排查和優(yōu)化思路和示例,具有一定的參考價值,感興趣的可以了解一下2022-05-05基于Springboot實現(xiàn)送水公司信息管理系統(tǒng)
這篇文章主要介紹了基于Springboot實現(xiàn)送水公司信息管理,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-01-01使用Spring Boot實現(xiàn)操作數(shù)據(jù)庫的接口的過程
本文給大家分享使用Spring Boot實現(xiàn)操作數(shù)據(jù)庫的接口的過程,包括springboot原理解析及實例代碼詳解,感興趣的朋友跟隨小編一起看看吧2021-07-07