使用函式式方式實現責任鏈模式

banq發表於2019-01-29

該模式包括建立一系列用於處理輸入的物件。鏈中的每個物件都可以或不可以處理特定的輸入,否則它會將輸入傳遞給鏈的下一個物件。如果鏈中的最後一個物件也無法處理給定的輸入,則鏈將無提示失敗,或者更常見的是,將透過異常通知使用者失敗。
假設我們有一個要解析的檔案,檔案可以有3種不同的型別:文字,音訊和影片。

public class File {
      
    enum Type { TEXT, AUDIO, VIDEO }
     
    private final Type type;
    private final String content;
     
    public File( Type type, String content ) {
        this.type = type;
        this.content = content;
    }
     
    public Type getType() {
        return type;
    }
     
    public String getContent() {
        return content;
    }
     
    @Override
    public String toString() {
        return type + ": " + content;
    }
}


然後讓我們建立一個介面來定義鏈中每個專案的行為:解析檔案並設定鏈的下一個專案。

interface FileParser {
    String parse(File file);
    void setNextParser(FileParser next);
}


設定一個抽象的解析器類處理輸入物件:

public abstract class AbstractFileParser implements FileParser {
    protected FileParser next;
 
    @Override
    public void setNextParser( FileParser next ) {
        this.next = next;
    }
}


現在我們已經準備好實現第一個具體的檔案解析器,它將能夠管理文字型別的檔案:

public class TextFileParser extends AbstractFileParser {
    @Override
    public String parse( File file ) {
        if ( file.getType() == File.Type.TEXT ) {
            return "Text file: " + file.getContent();
        } else if (next != null) {
            return next.parse( file );
        } else {
           throw new RuntimeException( "Unknown file: " + file );
        }
    }
}

如果檔案是文字的,則此解析器返回解析的結果,否則它將解析過程委託給鏈中的下一個解析器。如果此解析器是最後一個,則意味著整個鏈無法解析該檔案,然後它將透過丟擲異常來報告此問題。音訊和影片解析器將針對其能力的相應型別的檔案執行完全相同的操作。

public class AudioFileParser extends AbstractFileParser {
    @Override
    public String parse( File file ) {
        if ( file.getType() == File.Type.AUDIO ) {
            return "Audio file: " + file.getContent();
        } else if (next != null) {
            return next.parse( file );
        } else {
            throw new RuntimeException( "Unknown file: " + file );
        }
    }
}
 
public class VideoFileParser extends AbstractFileParser {
    @Override
    public String parse( File file ) {
        if ( file.getType() == File.Type.VIDEO ) {
            return "Video file: " + file.getContent();
        } else if (next != null) {
            return next.parse( file );
        } else {
            throw new RuntimeException( "Unknown file: " + file );
        }
    }
}


現在實現呼叫這些基於責任鏈模式的解析器。首先,我們需要為鏈的每個項建立一個例項:

FileParser textParser = new TextFileParser();
FileParser audioParser = new AudioFileParser();
FileParser videoParser = new VideoFileParser();


然後有必要透過將所有單個解析器連線在一起來設定鏈。

textParser.setNextParser( audioParser );
audioParser.setNextParser( videoParser );


最後,現在可以嘗試解析將檔案傳遞給鏈中的第一個專案的檔案。

File file = new File( File.Type.AUDIO, "Dream Theater  - The Astonishing" );
String result = textParser.parse( file );


鏈的第一個解析器,用於解析文字檔案的解析器將無法解析此檔案,因此它將檔案傳遞給下一個解析器,即音訊檔案的解析器。第二個解析器可以解析Dream Theater專輯,因此它將返回解析結果。由於鏈的第二項已經能夠執行所需的解析任務,因此該檔案將不會到達第三個解析器,即影片檔案的解析器。
正如我們已經分析過的所有其他模式一樣,到目前為止,讓我們嘗試將人工包裝的業務邏輯提取到純函式中。例如,我們可以有一個可以解析文字檔案的函式,但是當它與不同型別的檔案一起傳遞時應該怎麼做?在這個階段,我們沒有另一個解析器函式來委託解析過程,我們不想透過丟擲異常來報告問題,即使因為如果我們這樣做,我們將無法與其他人一起編寫此函式。
在這種情況下,丟擲異常或返回空指標都不是功能慣用解決方案。Java 8明確地引入了新的Optional類來處理這些情況。一個Optional值對可能存在或不存在的結果進行建模,因此我們可以使解析函式返回一個可選的包裝結果,以便成功解析,如果函式是透過非文字檔案傳遞的,則返回null。

public static Optional<String> parseText(File file) {
    return file.getType() == File.Type.TEXT ?
           Optional.of("Text file: " + file.getContent()) :
           Optional.empty();
}


同樣,我們可以開發另外兩個函式來解析音訊和影片檔案。

public static Optional<String> parseAudio(File file) {
    return file.getType() == File.Type.AUDIO ?
           Optional.of("Audio file: " + file.getContent()) :
           Optional.empty();
}
 
public static Optional<String> parseVideo(File file) {
    return file.getType() == File.Type.VIDEO ?
           Optional.of("Video file: " + file.getContent()) :
           Optional.empty();
}

現在可以透過將這些函式放入Stream中並將要解析的檔案傳遞給Stream中的所有函式來將這些函式連線到虛擬鏈中。

File file = new File( File.Type.AUDIO, "Dream Theater  - The Astonishing" );
 
String result = Stream.<Function<File, Optional<String>>>of( // [1]
        ChainOfRespLambda::parseText,
        ChainOfRespLambda::parseAudio,
        ChainOfRespLambda::parseVideo )
        .map(f -> f.apply( file )) // [2]
        .filter( Optional::isPresent ) // [3]
        .findFirst() // [4]
        .flatMap( Function.identity() ) // [5]
        .orElseThrow( () -> new RuntimeException( "Unknown file: " + file ) ) ); [6]



首先,我們將所有解析函式放在Stream [1]中。這裡的Java編譯器需要一些幫助:它無法確定我們在Stream中放置的物件型別,因此我們必須明確說明這一點。然後我們將Stream轉換為Stream <Optional>,方法是呼叫apply對原始Stream的每個函式傳遞要解析的檔案[2]。
現在我們需要在生成的Stream中找到非空的第一個Optional,因此我們只過濾出現的Optionals [3]並取第一個[4]。在Stream of Optional上呼叫findFirst()有一個小缺點:因為它將結果包裝在Optional中(因為Stream可能為空,在這種情況下它將返回一個Optional.empty()),這次結果將是Optional<Optional>。我們需要在單個級別中展平這個雙重巢狀的Optional,並且要實現這一點就足以呼叫flatMap傳遞一個Function.identity()[5]。最後,如果至少有一個解析器能夠解析它,或者它是空的,我們有一個包裝值的Optional。

物件導向和功能版本之間還有另一個類比:在兩種情況下都會呼叫解析器,直到找到能夠執行檔案解析的第一個解析器。在我們的示例中,影片解析器永遠不會被呼叫,因為解析器序列中的音訊解析器可以解析Dream Theater的專輯並返回非空結果。在函式實現中,Stream的懶惰保證了這個特性,它只是在呼叫傳遞給map()方法的lambda中定義的轉換函式,直到它找到第一個非空的Optional。
 

相關文章