转载

从函数式的角度重看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设计模式(一)

原文  http://mp.weixin.qq.com/s?__biz=MzIxMjAzMDA1MQ==&mid=2648945441&idx=1&sn=52681f8dde9916aa94fd520b598db5aa#rd
正文到此结束
Loading...