使用函数式方式实现责任链模式

19-01-29 banq
              

该模式包括创建一系列用于处理输入的对象。链中的每个对象都可以或不可以处理特定的输入,否则它会将输入传递给链的下一个对象。如果链中的最后一个对象也无法处理给定的输入,则链将无提示失败,或者更常见的是,将通过异常通知用户失败。

假设我们有一个要解析的文件,文件可以有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。