当函数成为一等公民时,设计模式的变化

GOF提出的设计模式,其本质思想是封装变化。故而,创建型模式封装的是对象创建的变化,结构型模式封装的是对象之间的协作与组合结构,行为型模式则封装了对象行为的变化。所谓“行为”,不正是函数所能要表达的吗?

函数的抽象能力

从函数的抽象角度看,任何行为都可以理解为是一个对类型进行转换的函数,这是FP思想对OO设计模式的最大冲击。例如Strategy模式与Command模式,前者封装了算法策略的变化,后者则封装了命令请求的变化。无论算法策略,还是命令请求,都可以表现为一个函数。

譬如说将各种四则运算看做是一种算法策略,为了应对具体计算的变化,在Java中我们应该定义四则运算的策略接口:

public interface Strategy {  
    int compute(int a, int b);  
} 
public class Context  {  
    private final Strategy strategy;  
    public Context(Strategy strategy) { 
        this.strategy = strategy; 
    }  
    public void use(int a, int b) { 
        strategy.compute(a, b); 
    }  
}

接收两个整数,然后经过计算返回一个整数。显然,四则运算的调用者其实关注的不是Strategy这个接口,而是compute这个行为。跟进一步,调用者其实关注的是将两个整数转换为一个整数的行为,他并不关心接口是什么,函数名有是什么,而是关注f(a, b) = c这个函数。于是,在Scala中,策略模式的实现就变为:

class Context(f: (Int, Int) => Int) {  
  def use(a: Int, b: Int): Int =  f(a, b)  
}

当然,你可以可以为这个函数定义一个类型,使其更加表意:

type Stategy = (Int, Int) => Int

当然,如果面对的是一组策略行为的封装,且这些策略行为的变化是一致的,使用一个接口将这些行为封装起来,在重用和表意角度讲,似乎又比单纯使用函数更佳。Scala提供OO与FP两种范式,算是一种骑墙的取巧,程序员需要依势而为。Scala给你提供了丰富而精彩的食材,如果你没有将菜做得色香味俱全,不能怪食材不好,还是自己太烂了。

Scala还提供了一种类似block的机制,称之为by name call。它接受的是一个语句块,而非函数类型。所以要注意这种形式与无参函数的区别。此外,by name call同时还具有延迟调用的能力。例如,当我们定义一个invoke函数接受一个无传入参数的函数时:

def invoke(f: () => Unit) = f()

如果你向invoke传入println("scala"),scala会报告错误:

这是因为println("scala")返回的是Unit类型,而不是() => Unit函数类型。使用by name call就没有这个问题:

f: => Unit是一个语句块,所以不能像函数那样调用。我们可以使用这种方式来快捷实现Command模式。

由于Java 8已经支持Lambda表达式,虽然它仍然不支持高阶函数,但是作为Java程序员,仍然有必要培养函数抽象的能力与习惯。在Java 8中使用Lambda,不仅让语法变得简洁,还可以让调用者可以脱离对具体某个接口的依赖,而仅仅依赖函数的抽象特征。

函数的组合能力

FP的编程思想中,除了高阶函数(包括Curry等)具有的抽象能力之外,还有一个好处是提供组合子能力。落实到Scala的语法上,就是偏函数(Partial Function)的andThencomposeorElse

Pavel Fatin在文章《Design Patterns in Scala》用OO设计模式中的Chain of Responsibility(职责链)模式来对比组合子,其实还是比较牵强的。或者说,FP思想中的组合子远远比职责链模式更强大。在Elixir语言中,甚至还提供了管道操作符>|来实现这种函数的组合。而我在博客《Scala中的Partial Function》中已经非常详解地介绍了Scala的偏函数,大家可以移步阅读。

如果真要对比,那么结合Scala的语法来看,则orElse可以非常方便地模拟职责链模式,而andThen则近似于管道-过滤器模式。其实我在OO语言中,很少运用GOF标志的职责链模式,也就是当寻找到具体职责的承担者时,履行职责后即可退出的方式;而是对这种模式进行调整,让其在履行职责后继续执行next的职责,又近乎于管道-过滤器了。

所以说,设计模式的运用妙乎于心,讲究应势而变。在融入FP思想后,要从本质思想去面对这些模式,不拘泥于OO还是FP,似乎才是未来编程的取舍之道。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2017-03-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Python小屋

面向对象程序设计中“超类”和“子类”概念的来历

众所周知,在面向对象程序设计中,子类从超类中继承数据成员和成员方法,然后在此基础上再根据实际需要进行扩展并增加属于子类的数据成员和成员方法。也就是说,子类的对象...

2554
来自专栏.NET技术

简单工厂模式和策略模式的区别与结合

    简单工厂模式和策略模式是大部分程序员,在学习设计模式时接触得最早,或在工作实践中也是用得相对比较多的两个设计模式。

1195
来自专栏Python

初识面向对象

面向过程 VS 面向对象  面向过程的程序设计的核心是过程(流水线式思维),过程即解决问题的步骤,面向过程的设计就好比精心设计好一条流水线,考虑周全什么时候处理...

18110
来自专栏哈雷彗星撞地球

设计模式中的设计原则

翻了三本书《设计模式之禅》、《设计模式:可复用的面向对象软件元素》、《Head First 设计模式》,也看了不少博客和关于设计模式原则的文章。关于设计模式有几...

593
来自专栏.NET技术

面向对象基础

通过封装、继承、多态把程序的耦合度降低,用设计模式使程序更加的灵活,容易修改,并且易于复用。缩短开发周期,重用性和继承性高,降低了重复的工作量,但缺点是,程序的...

622
来自专栏子勰随笔

接口设计六大原则

1523
来自专栏Python

面向对象的三大特性(封装、继承、多态)

继承 什么是继承 继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称为派生类或子类 python中类...

4229
来自专栏python3

python3--对象之间的交互,类命名空间与对象、实例的命令空间

现在有个游戏人与狗,人定义一个类,狗定义一个类,如何让两个类之间互相交互起来,让这个游戏变得更加有意思,代码如下

871
来自专栏Java呓语

生成器模式(分离部件构造)

生成器模式的主要功能是构建复杂的产品,而且是细化的,分步骤的构建产品,也就是生成器模式重在解决一步一步构造复杂对象的问题。如果光是这么认识生成器模式的功能是不够...

642
来自专栏吴伟祥

UML 类图介绍 转

Unified Modeling Language (UML)又称统一建模语言或标准建模语言,是始于1997年一个OMG标准,它是一个支持模型化和软件系统开发的...

451

扫描关注云+社区