訪問者模式(Visitor Pattern)——操作複雜對象結構

模式概述

在軟件開發中,可能會遇到操作複雜對象結構的場景,在該對象結構中存儲了多個不同類型的對象信息,而且對同一對象結構中的元素的操作方式並不唯一,可能需要提供多種不同的處理方式,還有可能增加新的處理方式。

在設計模式中,有一種模式可以滿足上述要求,其模式動機就是以不同的方式操作複雜對象結構,該模式就是下面要介紹的訪問者模式。

模式定義

訪問者模式是一種較爲複雜的行爲型設計模式,它包含訪問者和被訪問元素兩個主要組成部分,這些被訪問的元素通常具有不同的類型,且不同的訪問者可以對它們進行不同的訪問操作。

訪問者模式使得用戶可以在不修改現有系統的情況下擴展系統的功能,爲這些不同類型的元素增加新的操作。

在使用訪問者模式時,被訪問元素通常不是單獨存在的,它們存儲在一個集合中,這個集合被稱爲對象結構,訪問者通過遍歷對象結構實現對其中存儲的元素的逐個操作。

訪問者模式定義如下:

訪問者模式(Visitor Pattern):提供一個作用於某對象結構中的各元素的操作表示,它使我們可以在不改變各元素的類的前提下定義作用於這些元素的新操作。訪問者模式是一種對象行爲型模式。

模式結構圖

訪問者模式的結構較爲複雜,如下圖所示:

在訪問者模式結構圖中包含如下幾個角色:

  • Visitor(抽象訪問者):抽象訪問者爲對象結構中每一個具體元素類ConcreteElement聲明一個訪問操作,從這個操作的名稱或參數類型可以清楚知道需要訪問的具體元素的類型,具體訪問者需要實現這些操作方法,定義對這些元素的訪問操作。
  • ConcreteVisitor(具體訪問者):具體訪問者實現了每個由抽象訪問者聲明的操作,每一個操作用於訪問對象結構中一種類型的元素。
  • Element(抽象元素):抽象元素一般是抽象類或者接口,它定義一個accept()方法,該方法通常以一個抽象訪問者作爲參數。【稍後將介紹爲什麼要這樣設計】
  • ConcreteElement(具體元素):具體元素實現了accept()方法,在accept()方法中調用訪問者的訪問方法以便完成對一個元素的操作。
  • ObjectStructure(對象結構):對象結構是一個元素的集合,它用於存放元素對象,並且提供了遍歷其內部元素的方法。它可以結合組合模式來實現,也可以是一個簡單的集合對象,如一個List對象或一個Set對象。

在訪問者模式中,增加新的訪問者無須修改原有系統,系統具有較好的可擴展性。

模式僞代碼

總結一下,訪問者模式可以理解爲:爲操作某一對象的一組元素抽象出一組接口,配合對象元素的一個accept()操作,從而實現了不需要修改對象元素而給該元素提供不一樣操作的目的。

訪問者代碼大致如下:

/**
 * 抽象訪問者
 */
public interface Visitor {

    void visit(ElementA element);

    void visit(ElementB element);

    void visit(ElementC element);
}

/**
 * 具體訪問者的實現
 */
public class ConcreteVisitor implements Visitor {

    @Override
    public void visit(ElementA element) {
        // ElementA 操作代碼
    }

    @Override
    public void visit(ElementB element) {
        // ElementB 操作代碼
    }

    @Override
    public void visit(ElementC element) {
        // ElementC 操作代碼
    }
}

對於被訪問的元素而言,在其中一般都定義了一個accept()方法,用於接受訪問者的訪問,典型的抽象元素類代碼如下所示:

/**
 * 對被訪問元素進行抽象
 */
public interface Element {
    void accept(Visitor visitor);
}

/**
 * 具體元素A
 */
public class ElementA implements Element {

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

/**
 * 具體元素B
 */
public class ElementB implements Element {

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

/**
 * 具體元素C
 */
public class ElementC implements Element {

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public void operation() {
        // 業務方法
    }
}

需要注意的是這裏傳入了一個抽象訪問者Visitor類型的參數,即針對抽象訪問者進行編程,而不是具體訪問者,在程序運行時再確定具體訪問者的類型,並調用具體訪問者對象的visit()方法實現對元素對象的操作。

在抽象元素類Element的子類中實現了accept()方法,用於接受訪問者的訪問,在具體元素類中還可以定義不同類型的元素所特有的業務方法,比如上面的ElementC

在訪問者模式中,對象結構可能是一個集合,它用於存儲元素對象並接受訪問者的訪問,其典型代碼如下所示:

public class ObjectStructure {

    // 存儲元素對象
    private final List<Element> elements = new ArrayList<>();

    public void accept(Visitor visitor) {
        // 遍歷訪問每一個元素
        for (Element element : elements) {
            element.accept(visitor);
        }
    }
}

模式應用

模式在JDK中的應用

在早期的Java版本中,如果要對指定目錄下的文件進行遍歷,大多用遞歸的方式來實現,這種方法複雜且靈活性不高。

Java 7版本後,Files類提供了walkFileTree()方法,該方法可以很容易的對目錄下的所有文件進行遍歷,需要PathFileVisitor兩個參數。其中,Path是要遍歷文件的路徑,FileVisitor則可以看成一個文件訪問器。

java.nio.file.Files#walkFileTree()源碼如下:

public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor)
        throws IOException
    {
        return walkFileTree(start,
                            EnumSet.noneOf(FileVisitOption.class),
                            Integer.MAX_VALUE,
                            visitor);
    }

FileVisitor主要提供了4個方法,且返回結果的都是FileVisitResult對象值,用於決定當前操作完成後接下來該如何處理。FileVisitResult是一個枚舉類,代表返回之後的一些後續操作。源碼如下。

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

這樣我們就可以很容易實現遞歸拷貝目錄或者刪除目錄等等。

比如我們看下cn.hutool.core.io.file.visitor.CopyVisitor的拷貝操作的實現:


/**
 * 文件拷貝的FileVisitor實現,用於遞歸遍歷拷貝目錄,此類非線程安全
 * 此類在遍歷源目錄並複製過程中會自動創建目標目錄中不存在的上級目錄。
 */
public class CopyVisitor extends SimpleFileVisitor<Path> {

    private final Path source;
    private final Path target;
    private boolean isTargetCreated;
    private final CopyOption[] copyOptions;

    /**
     * 構造
     *
     * @param source 源Path
     * @param target 目標Path
     * @param copyOptions 拷貝選項,如跳過已存在等
     */
    public CopyVisitor(Path source, Path target, CopyOption... copyOptions) {
        if(PathUtil.exists(target, false) && false == PathUtil.isDirectory(target)){
            throw new IllegalArgumentException("Target must be a directory");
        }
        this.source = source;
        this.target = target;
        this.copyOptions = copyOptions;
    }

    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
            throws IOException {
        initTarget();
        // 將當前目錄相對於源路徑轉換爲相對於目標路徑
        final Path targetDir = target.resolve(source.relativize(dir));
        try {
            Files.copy(dir, targetDir, copyOptions);
        } catch (FileAlreadyExistsException e) {
            if (false == Files.isDirectory(targetDir))
                throw e;
        }
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
            throws IOException {
        initTarget();
        Files.copy(file, target.resolve(source.relativize(file)), copyOptions);
        return FileVisitResult.CONTINUE;
    }

    /**
     * 初始化目標文件或目錄
     */
    private void initTarget(){
        if(false == this.isTargetCreated){
            PathUtil.mkdir(this.target);
            this.isTargetCreated = true;
        }
    }
}

再擴充一個刪除操作,同樣不難,見cn.hutool.core.io.file.visitor.DelVisitor


/**
 * 刪除操作的FileVisitor實現,用於遞歸遍歷刪除文件夾
 */
public class DelVisitor extends SimpleFileVisitor<Path> {

	public static DelVisitor INSTANCE = new DelVisitor();

	@Override
	public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
		Files.delete(file);
		return FileVisitResult.CONTINUE;
	}

	/**
	 * 訪問目錄結束後刪除目錄,當執行此方法時,子文件或目錄都已訪問(刪除)完畢<br>
	 * 理論上當執行到此方法時,目錄下已經被清空了
	 *
	 * @param dir 目錄
	 * @param e   異常
	 * @return {@link FileVisitResult}
	 * @throws IOException IO異常
	 */
	@Override
	public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
		if (e == null) {
			Files.delete(dir);
			return FileVisitResult.CONTINUE;
		} else {
			throw e;
		}
	}
}

到這裏,你能感受到訪問者模式的妙用嗎?

【模式動機就是以不同的方式操作複雜對象結構,增加新的處理方式,無需修改既有的代碼。】

模式在開源項目中的應用

在XML文檔解析、編譯器的設計、複雜集合對象的處理等領域訪問者模式得到了一定的應用。有興趣的朋友建議看看 ANTLR專題 ,或許能深刻體會到訪問者模式的魅力。

模式總結

當系統中存在一個較爲複雜的對象結構,且不同訪問者對其所採取的操作也不相同時,可以考慮使用訪問者模式進行設計。

主要優點

  • 將元素對象的訪問行爲集中到一個訪問者對象中,而不是分散在一個個的元素類中。類的職責更加清晰,有利於對象結構中元素對象的複用,相同的對象結構可以供多個不同的訪問者訪問。
  • 增加新的訪問操作就意味着增加一個新的具體訪問者類,方便擴展,無須修改源代碼,符合開閉原則

主要缺點

  • 增加新的元素類很困難。在訪問者模式中,每增加一個新的元素類都意味着要在抽象訪問者角色中增加一個新的抽象操作,並在每一個具體訪問者類中增加相應的具體操作,這違背了開閉原則的要求。
  • 破壞封裝。訪問者模式要求訪問者對象訪問並調用每一個元素對象的操作,這意味着元素對象有時候必須暴露一些自己的內部操作和內部狀態,否則無法供訪問者訪問。

適用場景

  • 一個對象結構包含多個類型的對象,希望對這些對象實施一些依賴其具體類型的操作。在訪問者中針對每一種具體的類型都提供了一個訪問操作,不同類型的對象可以有不同的訪問操作。
  • 需要對一個對象結構中的對象進行很多不同的並且不相關的操作,而需要避免讓這些操作污染這些對象的類,也不希望在增加新操作時修改這些類。訪問者模式使得我們可以將相關的訪問操作集中起來定義在訪問者類中,對象結構可以被多個不同的訪問者類所使用,將對象本身與對象的訪問操作分離。
  • 對象結構中對象對應的類很少改變,但經常需要在此對象結構上定義新的操作。

參考:

《設計模式的藝術之道(軟件開發人員內功修煉之道)》—— 劉偉

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章