用函数范式实现战略模式

19-01-30 banq
                   

战略模式又称为策略模式,其目的是让我们能使用不同但可互换的算法。现在我们在另一个实际例子中使用这种模式。我们想要概括一个流程,该流程在输入中获取文本,使用给定的条件对其进行过滤,并在最终格式化或转换后将其打印在标准输出上。换句话说,我们需要概括2个行为:一个过滤文本,另一个转换它。第一步是将这两种行为的抽象定义放入一个接口中。

interface TextFormatter {
    boolean filter(String text);
    String format(String text);
}

在此之后,可以开发一个实现一般流程的类来发布一个文本,该文本与TextFormatter接口(策略)的实例一起传递,该实例又封装了用户想要如何过滤和格式化文本的细节。

public class TextEditor {
    private final TextFormatter textFormatter;
 
    public TextEditor(TextFormatter textFormatter) {
        this.textFormatter = textFormatter;
    }
 
    public void publishText(String text) {
       if (textFormatter.filter( text )) {
           System.out.println( textFormatter.format( text ) );
        }
    }
}

我们现在可以提供策略的多个实现。最明显的一个接受任何文本并按原样打印。

public class PlainTextFormatter implements TextFormatter {
 
    @Override
    public boolean filter( String text ) {
        return true;
    }
 
    @Override
    public String format( String text ) {
        return text;
    }
}

另一个可以用于逐行解析日志文件以搜索错误消息,因此它只接受以“ERROR”开头的句子并以大写形式打印它们。

public class ErrorTextFormatter implements TextFormatter {
 
    @Override
    public boolean filter( String text ) {
        return text.startsWith( "ERROR" );
    }
 
    @Override
    public String format( String text ) {
        return text.toUpperCase();
    }
}

最后,我们可以更具想象力,以小写字母打印仅短于20个字符的文本。

public class ShortTextFormatter implements TextFormatter {
 
    @Override
    public boolean filter( String text ) {
        return text.length() < 20;
    }
 
    @Override
    public String format( String text ) {
        return text.toLowerCase();
    }
}

此时,我们可以创建一个TextEditor,传递一个TextFormatter实例,该实例特定于我们想要执行的文本发布类型。

TextEditor textEditor = new TextEditor( new ErrorTextFormatter() );
textEditor.publishText( "ERROR - something bad happened" );
textEditor.publishText( "DEBUG - I'm here" );

到目前为止一直很好,但再一次感觉这种实现比它更加冗长。这一次,唯一相关的信号是TextEditor类的publishText中实现的逻辑。TextFormatter接口定义的2个行为可以使用2个函数传递给publishText方法:一个谓词来过滤要发布的测试;另外一个是UnaryOperator(一个函数转换另一个相同类型的对象)来格式化之前将其发送到标准输出。

public static void publishText( String text, Predicate<String> filter, UnaryOperator<String> format) {
    if (filter.test( text )) {
        System.out.println( format.apply( text ) );
    }
}

通过这种方式,我们可以按原样发布任何文本,如下所示由PlainTextFormatter完成:

publishText( "DEBUG - I'm here", s -> true, s -> s );

或者可以重新实现ErrorTextFormatter提供的功能,传递一个仅接受以“ERROR”开头的文本的Predicate和一个将String转换为大写的函数。

publishText( "ERROR - something bad happened", s -> s.startsWith( "ERROR" ), String::toUpperCase );

对这种更紧凑的解决方案的一个常见反对意见是,在类中显式实现TextFormatter允许拥有一个可以重用的策略库,而不是为每个单独的调用重复它们的实现。然而,没有什么能阻止我们对函数采用类似的方法并将它们收集在这样一个合适的库中。

public class TextUtil {
    public boolean acceptAll(String text) {
        return true;
    }
 
    public String noFormatting(String text) {
        return text;
    }
 
    public boolean acceptErrors(String text) {
        return text.startsWith( "ERROR" );
    }
 
    public String formatError(String text) {
        return text.toUpperCase();
    }
}

通过这种方式,我们可以重用定义的函数,而不是使用匿名lambdas:

publishText( "DEBUG - I'm here", TextUtil::acceptAll, TextUtil::noFormatting );

publishText( "ERROR - something bad happened", TextUtil::acceptErrors, TextUtil::formatError );

值得注意的是,这些函数实际上比策略类更精细(它们可以以任何类不可用的方式组合),并允许更好的可重用性。