从函数式的角度重看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设计模式的主要内容,如果未能解决你的问题,请参考以下文章