鉆井網(wǎng)站建設(shè)電商代運(yùn)營(yíng)一般收多少服務(wù)費(fèi)
訪問者模式
訪問者模式被稱為是最復(fù)雜的設(shè)計(jì)模式,比較難理解并且使用頻率不高。
在 GoF 的《設(shè)計(jì)模式》?書中,訪問者者模式(Visitor Design Pattern)是這么定義的:
Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.
允許?個(gè)或者多個(gè)操作應(yīng)?到?組對(duì)象上,解耦操作和對(duì)象本?。
訪問者模式是一種將數(shù)據(jù)結(jié)構(gòu)和數(shù)據(jù)操作分離的設(shè)計(jì)模式,屬于行為型模式。
訪問者模式的基本思想是: 假設(shè)系統(tǒng)中有一個(gè)由許多對(duì)象構(gòu)成的對(duì)象結(jié)構(gòu)(元素),這些對(duì)象的類都提供一個(gè)accept()
方法用來接受訪問者對(duì)象的訪問,不同的訪問者訪問同一對(duì)象可以產(chǎn)生不同的數(shù)據(jù)結(jié)果(訪問者其實(shí)就是一個(gè)擁有visit()方法的接口) 。accept()
方法可以接收不同的訪問者對(duì)象,然后在其內(nèi)部將自己(元素)轉(zhuǎn)發(fā)到訪問者對(duì)象的visit()
方法內(nèi)。
訪問者模式的核心是解耦數(shù)據(jù)結(jié)構(gòu)和數(shù)據(jù)操作,使得對(duì)元素的操作具備良好的擴(kuò)展性??梢酝ㄟ^擴(kuò)展不同的訪問者來實(shí)現(xiàn)對(duì)同一元素集的不同操作。
如果你的系統(tǒng)中只是對(duì)單個(gè)對(duì)象(類)進(jìn)行操作或者對(duì)多個(gè)類進(jìn)行一種操作,那么就沒必要使用訪問者模式了。運(yùn)用訪問者模式是為了方便后續(xù)擴(kuò)展操作類型,在對(duì)對(duì)象集(多個(gè)類對(duì)象)擴(kuò)展操作的時(shí)候可以不需要修改所有類的代碼。
當(dāng)系統(tǒng)中存在類型數(shù)目穩(wěn)定(固定)的一類數(shù)據(jù)結(jié)構(gòu)時(shí),可以通過訪問者模式方便地實(shí)現(xiàn)對(duì)該類型所有數(shù)據(jù)結(jié)構(gòu)的不同操作。
訪問者模式類圖
主要包含四個(gè)角色 :
Visitor 抽象訪問者:接口或者抽象類,它定義了對(duì)每一個(gè)可訪問元素(Visitable Element)訪問的行為,它的參數(shù)就是可以訪問的元素,它的方法個(gè)數(shù)理論上來講與元素個(gè)數(shù)(Element的實(shí)現(xiàn)類個(gè)數(shù))是一樣的,從這點(diǎn)不難看出,訪問者模式要求元素類的個(gè)數(shù)不能改變(不能改變的意思是說,如果元素類的個(gè)數(shù)經(jīng)常改變,則說明不適合使用訪問者模式)。
ConcreteVisitor:具體的訪問者,實(shí)現(xiàn)對(duì)每一個(gè)元素的具體操作。
Element 抽象元素:元素接口,它定義了一個(gè)接受訪問者(accept)的方法,其意義是指,每一個(gè)元素都要可以被訪問者訪問。
ConcreteElement:具體的元素類,它提供接受訪問者的具體實(shí)現(xiàn),而這個(gè)具體的實(shí)現(xiàn),通常情況下是調(diào)用訪問者提供的訪問該元素類的方法。
代碼示例
我們使用訪問者模式模擬一個(gè)處理不同類型文件的場(chǎng)景
假設(shè)我們有三類文件,PDF,EXCEL,WORD,我們需要從三種文件中提取信息存到到自己的系統(tǒng)里(假設(shè)需要導(dǎo)入到自己的一個(gè)txt文件),然后還需要對(duì)三類文件都進(jìn)行壓縮等一系列功能
將不同文件類型定義為抽象元素(這是一個(gè)穩(wěn)定的數(shù)據(jù)結(jié)構(gòu)), 對(duì)文件的操作定義為訪問者,代碼如下 :
public abstract class ResourceFile {private String name;protected ResourceFile(String name) {this.name = name;}abstract void accept(Vistor vistor);}
public class PDFFile extends ResourceFile {protected PDFFile(String name) {super(name);}@Overridevoid accept(Vistor vistor) {vistor.visit(this);}
}public class ExcelFile extends ResourceFile{protected ExcelFile(String name) {super(name);}@Overridevoid accept(Vistor vistor) {vistor.visit(this);}
}public class WordFile extends ResourceFile{public WordFile(String name) {super(name);}@Overridevoid accept(Vistor vistor) {vistor.visit(this);}
}
public interface Vistor {void visit(PDFFile file);void visit(ExcelFile file);void visit(WordFile file);}
public class CompressionVistor implements Vistor{@Overridepublic void visit(PDFFile file) {System.out.println("壓縮pdf文件");}@Overridepublic void visit(ExcelFile file) {System.out.println("壓縮excel文件");}@Overridepublic void visit(WordFile file) {System.out.println("壓縮word文件");}
}
public class ExtractVistor implements Vistor{@Overridepublic void visit(PDFFile file) {System.out.println("提取pdf文字內(nèi)容");}@Overridepublic void visit(ExcelFile file) {System.out.println("提取excel文字內(nèi)容");}@Overridepublic void visit(WordFile file) {System.out.println("提取word文字內(nèi)容");}
}
public class Test {private static final List<ResourceFile> resourceFileList = new ArrayList<>();static {resourceFileList.add(new PDFFile("設(shè)計(jì)模式.pdf"));resourceFileList.add(new ExcelFile("Data.excel"));resourceFileList.add(new WordFile("筆記.doc"));}public static void main(String[] args) {for (ResourceFile resourceFile: resourceFileList) {resourceFile.accept(new CompressionVistor());}for (ResourceFile resourceFile: resourceFileList) {resourceFile.accept(new ExtractVistor());}}}
Double Dispatch
靜態(tài)分派
靜態(tài)分派(Static Dispatch)就是按照變量的靜態(tài)類型(變量被聲明時(shí)的類型)進(jìn)行分派,從而確定方法的執(zhí)行版本,靜態(tài)分派在編譯時(shí)就可以確定方法的版本,典型例子就是java的方法重載
java在靜態(tài)分派的時(shí)候,我們可以根據(jù)多個(gè)判斷依據(jù)(即參數(shù)個(gè)數(shù)和參數(shù)類型)判斷使用哪個(gè)方法,所以java是靜態(tài)多分派的語言
動(dòng)態(tài)分派
動(dòng)態(tài)分派,不是在編譯期確定方法版本,而是在運(yùn)行時(shí)才能確定
Single Dispatch,指的是我們僅僅需要根據(jù)對(duì)象運(yùn)行時(shí)的類型來決定執(zhí)行哪個(gè)對(duì)象的方法
Double Dispatch,指的是我們需要根據(jù)對(duì)象的運(yùn)行時(shí)類型和參數(shù)的運(yùn)行時(shí)類型來決定執(zhí)行哪個(gè)對(duì)象的哪個(gè)方法 (二者區(qū)別主要在于是否可以根據(jù)方法參數(shù)運(yùn)行時(shí)的類型來判斷執(zhí)行對(duì)象的哪個(gè)方法)
當(dāng)前主流的?向?qū)ο缶幊陶Z?(?如,Java、C++、C#)都只?持 Single Dispatch,不?持 Double Dispatch。
以Java為例,Java?持多態(tài),代碼可以在運(yùn)?時(shí)獲得對(duì)象的實(shí)際類型,然后根據(jù)實(shí)際類型決定調(diào)?哪個(gè)對(duì)象的方法。 Java 也?持方法重載,但 Java 設(shè)計(jì)的方法重載的語法規(guī)則是在編譯時(shí),根據(jù)傳遞進(jìn)函數(shù)的參數(shù)的聲明類型,來決定調(diào)?哪個(gè)重載方法。也就是說,具體執(zhí)?哪個(gè)對(duì)象的哪個(gè)?法,只跟對(duì)象的運(yùn)?時(shí)類型有關(guān),跟參數(shù)的運(yùn)?時(shí)類型?關(guān)。所以,Java 語?是 動(dòng)態(tài)單分派的語言。
我們可以看下具體的例子 :
public class ParentClass {public void method() {System.out.println("ParentClass 執(zhí)行method方法");}}public class SonClass extends ParentClass{@Overridepublic void method() {System.out.println("SonClass 執(zhí)行method方法");}
}
public class SingleDispatch {public void method(ParentClass parentClass) {parentClass.method();}public void print(ParentClass parentClass) {System.out.println("打印parentClass");}public void print(SonClass sonClass) {System.out.println("打印sonClass");}
}
public class Test {public static void main(String[] args) {ParentClass s = new SonClass();SingleDispatch singleDispatch = new SingleDispatch();singleDispatch.method(s);//執(zhí)?哪個(gè)對(duì)象的?法,由對(duì)象的實(shí)際類型決定(多態(tài))singleDispatch.print(s);//執(zhí)?對(duì)象的哪個(gè)?法,由參數(shù)對(duì)象的聲明類型決定,這里聲明的時(shí)ParentClass類型}}
動(dòng)態(tài)雙分派的語言不需要訪問者模式
假設(shè) Java 語??持 動(dòng)態(tài)雙分派,那么下面的代碼就可以編譯通過,正常執(zhí)行了。
public class ExtractExecutor {public void extract(PDFFile file) {System.out.println("提取pdf文字內(nèi)容");}public void extract(WordFile file) {System.out.println("提取word文字內(nèi)容");}public void extract(ExcelFile file) {System.out.println("提取excel文字內(nèi)容");}}
public static void main(String[] args) {ExtractExecutor extractExecutor = new ExtractExecutor();for (ResourceFile resourceFile: resourceFileList) {//這里會(huì)編譯報(bào)錯(cuò): Cannot resolve method 'extract(ResourceFile)'extractExecutor.extract(resourceFile);}}
代碼會(huì)在運(yùn)?時(shí),根據(jù)參數(shù)(resourceFile)的實(shí)際類型(PDFFile、ExcelFile、WordFile),來決定調(diào)用extract()
的三個(gè)重載方法中的哪?個(gè),也就不需要訪問者模式了。
訪問者模式中的偽動(dòng)態(tài)雙分派
所謂的動(dòng)態(tài)雙分派就是在運(yùn)行時(shí)根據(jù)對(duì)象和參數(shù)的運(yùn)行時(shí)類型去判斷調(diào)用哪個(gè)一個(gè)對(duì)象的哪個(gè)方法。訪問者模式通過進(jìn)行兩次動(dòng)態(tài)單分派來達(dá)到這個(gè)效果。
for (ResourceFile resourceFile: resourceFileList) {resourceFile.accept(new ExtractVistor());}
@Overridevoid accept(Vistor vistor) {vistor.visit(this);}
當(dāng)調(diào)用accept()
方法的時(shí)候, 根據(jù)resourceFile的實(shí)際類型決定調(diào)用哪個(gè)文件的accept()
方法;
在執(zhí)行accept()
方法的時(shí)候,根據(jù)vistor的示例類型來決定調(diào)用哪個(gè)Vistor的visist方法,此時(shí)的this的類型就是這個(gè)類的靜態(tài)類型,這是在編譯期就確定的,所以也可以確定是調(diào)用的哪個(gè)重載方法
通過工廠模式實(shí)現(xiàn)上述功能
上述的例子,如果對(duì)文件的操作也比較固定,也可以使用工廠模式來實(shí)現(xiàn),定義?個(gè)包含 extract()
接?的Executor接?。PdfExtractExecutor、ExcelExtractExecutor、WordExtractExecutor 類實(shí)現(xiàn) Executor接?,完成對(duì)各自?件的?本內(nèi)容抽取。然后再提供一個(gè)ExtractExecutorFactory ??類根據(jù)不同的?件類型,返回不同的 Executor。
public abstract class ResourceFile {private String name;protected ResourceFile(String name) {this.name = name;}abstract String getType();
}
public class PDFFile extends ResourceFile {protected PDFFile(String name) {super(name);}@OverrideString getType() {return "PDF";}}
public interface Executor {void extract(ResourceFile file);
}
//省略了WordExtractExecutor,ExcelExtractExecutor的代碼
public class PDFExtractExecutor implements Executor{@Overridepublic void extract(ResourceFile file) {System.out.println("提取pdf的內(nèi)容");}
}
public class ExtractExecutorFactory {private static Map<String, Executor> map = new HashMap<>();static {map.put("PDF", new PDFExtractExecutor());
// map.put("EXCEL", new ExeclExtractExecutor());
// map.put("WORD", new WordExtractExecutor());}public static Executor getExecutor(ResourceFile file) {return map.get(file.getType());}}
訪問者模式在源碼中的應(yīng)用
Java 7 版本后,Files 類提供了 walkFileTree() 方法,該方法可以很容易的對(duì)目錄下的所有文件進(jìn)行遍歷,需要 Path、FileVisitor 兩個(gè)參數(shù)。其中,Path 是要遍歷文件的路徑,FileVisitor 則可以看成一個(gè)文件訪問器。源碼如下。
package java.nio.file;
public final class Files {...public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor)throws IOException{return walkFileTree(start,EnumSet.noneOf(FileVisitOption.class),Integer.MAX_VALUE,visitor);}...
}
FileVisitor 提供了遞歸遍歷文件樹的支持,這個(gè)接口的方法表示了遍歷過程中的關(guān)鍵過程,允許在文件被訪問、目錄將被訪問、目錄已被訪問、發(fā)生錯(cuò)誤等過程中進(jìn)行控制。換句話說,這個(gè)接口在文件被訪問前、訪問中和訪問后,以及產(chǎn)生錯(cuò)誤的時(shí)候都有相應(yīng)的鉤子程序進(jìn)行處理。
FileVisitor 主要提供了 4 個(gè)方法,且返回結(jié)果的都是 FileVisitResult 對(duì)象值,用于決定當(dāng)前操作完成后接下來該如何處理。FileVisitResult 是一個(gè)枚舉類,代表返回之后的一些后續(xù)操作。
package java.nio.file;import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
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;
}package java.nio.file;public enum FileVisitResult {CONTINUE,TERMINATE,SKIP_SUBTREE,SKIP_SIBLINGS;
}
FileVisitResult 主要包含 4 個(gè)常見的操作。
- FileVisitResult.CONTINUE:這個(gè)訪問結(jié)果表示當(dāng)前的遍歷過程將會(huì)繼續(xù)。
- FileVisitResult.SKIP_SIBLINGS:這個(gè)訪問結(jié)果表示當(dāng)前的遍歷過程將會(huì)繼續(xù),但是要忽略當(dāng)前文件/目錄的兄弟節(jié)點(diǎn)。
- FileVisitResult.SKIP_SUBTREE:這個(gè)訪問結(jié)果表示當(dāng)前的遍歷過程將會(huì)繼續(xù),但是要忽略當(dāng)前目錄下的所有節(jié)點(diǎn)。
- FileVisitResult.TERMINATE:這個(gè)訪問結(jié)果表示當(dāng)前的遍歷過程將會(huì)停止。
通過訪問者去遍歷文件樹會(huì)比較方便,比如查找文件夾內(nèi)符合某個(gè)條件的文件或者某一天內(nèi)所創(chuàng)建的文件,這個(gè)類中都提供了相對(duì)應(yīng)的方法。它的實(shí)現(xiàn)也非常簡(jiǎn)單,代碼如下
public class SimpleFileVisitor<T> implements FileVisitor<T> {protected SimpleFileVisitor() {}@Overridepublic FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)throws IOException{Objects.requireNonNull(dir);Objects.requireNonNull(attrs);return FileVisitResult.CONTINUE;}@Overridepublic FileVisitResult visitFile(T file, BasicFileAttributes attrs)throws IOException{Objects.requireNonNull(file);Objects.requireNonNull(attrs);return FileVisitResult.CONTINUE;}@Overridepublic FileVisitResult visitFileFailed(T file, IOException exc)throws IOException{Objects.requireNonNull(file);throw exc;}@Overridepublic FileVisitResult postVisitDirectory(T dir, IOException exc)throws IOException{Objects.requireNonNull(dir);if (exc != null)throw exc;return FileVisitResult.CONTINUE;}
}
一開始覺得這里的設(shè)計(jì)比較多余,后來仔細(xì)想了下,在不同場(chǎng)景下我們對(duì)文件樹的遍歷要求是不一樣的,通過訪問者模式,用戶可以方便的定義自己的遍歷操作
比方說在JavacPathFileManager里就重寫了preVisitDirectory
和visitFile
方法
Files.walkFileTree(packageDir, opts, maxDepth,new SimpleFileVisitor<Path>() {@Overridepublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {Path name = dir.getFileName();if (name == null || SourceVersion.isIdentifier(name.toString())) // JSR 292?return FileVisitResult.CONTINUE;elsereturn FileVisitResult.SKIP_SUBTREE;}@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {if (attrs.isRegularFile() && kinds.contains(getKind(file.getFileName().toString()))) {JavaFileObject fe =PathFileObject.createDirectoryPathFileObject(JavacPathFileManager.this, file, pathDir);results.append(fe);}return FileVisitResult.CONTINUE;}});
總結(jié)
簡(jiǎn)單來說,訪問者模式就是封裝一些作用于某種數(shù)據(jù)結(jié)構(gòu)中的各元素的操作,它可以在不改變這個(gè)數(shù)據(jù)結(jié)構(gòu)的前提下定義作用于這些元素的新操作。
訪問者模式適用場(chǎng)景 :
- 數(shù)據(jù)結(jié)構(gòu)穩(wěn)定,但是作用于數(shù)據(jù)結(jié)構(gòu)的操作經(jīng)常變化
- 需要數(shù)據(jù)結(jié)構(gòu)與數(shù)據(jù)操作分離
- 需要對(duì)不同數(shù)據(jù)類型(元素)進(jìn)行操作,但是有不使用
if.. else ..
判斷具體類型
優(yōu)點(diǎn):
- 使得數(shù)據(jù)結(jié)構(gòu)和作用于結(jié)構(gòu)上的操作解耦,使得操作集合可以獨(dú)立變化。
- 擴(kuò)展性好,添加新的操作或者說訪問者會(huì)非常容易。
缺點(diǎn):
- 增加新的元素類型會(huì)非常困難,每次新增元素類型,則訪問者類必須增加對(duì)應(yīng)元素類型的操作
- 違反了依賴倒置原則,訪問者依賴的是具體元素類型,而不是抽象
- 變更元素的屬性可能會(huì)導(dǎo)致對(duì)應(yīng)的訪問者類也需要修改