从函数式的角度重看GOF设计模式

Posted 写程序的康德

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从函数式的角度重看GOF设计模式相关的知识,希望对你有一定的参考价值。

这是本系列文章中的第一篇,我们会回顾一些GOF模式,然后尝试用更简洁、更灵活的方式重新实现它们。

在开始分析各种设计模式之前,先讨论一个问题:简单的英语语法练习。看这样的句子:“smoking is unhealthy”和“running is tiring”。注意句子中的“smoking”和“running”,在英语中,ing后缀可以把一个名词转换动词。GOF设计模式尤其是“行为模式”,也是采用相似的做法。和ing后缀实现名词动词转换一样,某些设计模式也是一种转换,只不过涉及到的是函数、对象而已。

比较扯淡的是,这种转换往往不是有必要的,仅仅是把一些函数式编程的概念强行转换成面向对象风格。所以这就带来更多的代码,更低的可读性而且维护起来更困难。实际上,这不仅仅是用对象把函数包装起来那么简单,你还必须得把这些松散的对象“粘起来”。相同的结果,如果用函数式编程实现更加简单。

让我们开始看最常见的一个设计模式

Command模式

Command模式是一个从函数式强行变成面向对象的典范(代码量剧增)。我们看一下面向对象怎么实现。首先得有一个“接口”:

interface Command {
  void run(); }

现在,可以提供这个Command接口的不同实现了。比如想输出一条消息——可以这样写:

public class Logger implements Command {
   public final String message;
   public Logger( String message ) {
       this.message = message;    }
   @Override    public void run() {        System.out.println("Logging: " + message);    } }

现在把消息放到文件中

public class FileSaver implements Command {
   public final String message;
   public FileSaver( String message ) {
       this.message = message;    }
   @Override    public void run() {        System.out.println("Saving: " + message);    } }

把消息通过邮件发送出去

public class Mailer implements Command {    
  public final String message;    
  public Mailer( String message ) {      
      this.message = message;   }
  @Override   public void run() {       System.out.println("Sending: " + message);   } }

现在需要创建一些指令对象来执行

public class Executor {    
   public void execute(List<Command> tasks) {
       for (Command task : tasks) {            task.run();        }    } }

最后让我new一些对象,塞到List里面调用Executor,开跑~~~

就像这段“裹脚布”代码一样的,GOF的设计模式就是把函数包装(要执行的动作)成对象(把动作变成一个一个的“指令”)。但是这种扯淡的方式除了为了“Java的面向对象”之外没有任何好处。随着lambda在Java8中被引入,现在终于可以混合使用函数式和面向对象了,我们来尝试把这个例子变的更简洁。

首先需要注意,我们不需要定义Command接口了,有一个Runnable类它的抽象方法和Command要实现的接口签名一样(注:方法签名相同是指方法的返回值类型相同,参数相同)。所以我们只需要实现三个静态函数就可以了。

public static void log(String message) {
    System.out.println("Logging: " + message);
}
public static void save(String message) {    System.out.println("Saving: " + message); }
public static void send(String message) {    System.out.println("Sending: " + message); }

回头看看函数式的实现更加突出代码的业务逻辑而不是做各种“裹脚布”一样的转换。Executor类可以直接用一句代码实现

public static void execute(List<Runnable> tasks ) {
    tasks.forEach( Runnable::run );
}

我们可以在执行之前定义一些要执行的函数

List<Runnable> tasks = new ArrayList<>();
tasks.add(() -> log("Hi"));
tasks.add(() -> save("Cheers"));
tasks.add(() -> send("Bye"));

execute( tasks );

这里没有写参数Java编译器会自动翻译成lambda匿名函数,实际上它就是把“调用静态方法执行某个动作”包装在一个实现了Runnable接口匿名函数(注:其实是方法签名和Runnable的run相同)让后扔个一个List去执行。

Strategy模式

策略模式是一个数据加工过程,我们可以而多个算法,把他们放到一起封装起来,使之可以相互替换。下面的例子中,我需要定义一个处理文本的过程:输入,筛选,最后把结果转换格式化输出。话句话说我需要两个行为:过滤文本,转换格式。第一步定义一个接口:

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”,如果发现”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方法。其他两个“行为”都可以通过参数传递给publishText方法。第一个参数一个用于过滤的谓语(注:一个函数,返回true或者false),一个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,传一个谓语(注:一个函数,返回true或者false)用于判断文本是否以ERROR开头,在扔一个String的大写转换函数。

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

可能你说这种更加“紧凑”的的方法没有通过类实现的更具有复用性,每次都要写一个这么长的调用。函数式其实允许我们把这些放到一个类里面,形成一个工具类

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

通过这种方式来代替而不是lambda匿名函数,我们就可以重用这些函数的定义了

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

值得注意的是,这些比类的实现方式更加短小的函数(它们可以自由组合而不必考虑“类”),而且更具有复用性。在本系列的下一部分,我们将回顾2个其他GOF中常用的模式——Template和Observer。


欢迎关注公众账号了解更多信息

以上是关于从函数式的角度重看GOF设计模式的主要内容,如果未能解决你的问题,请参考以下文章

三角函数式的化简与求值20201128

——函数)

这些角度电子邮件指令代码片段如何连接

从JVM的角度看JAVA代码--代码优化

Scala的面向对象与函数编程

《游戏编程模式》记录