函数式编程思想:耦合和组合,第1部分

总是在某种特定抽象(比如说面向对象)中进行编码工作,这使得很难看清楚何时这一抽象会把你引向一种并非最好的解决方案上。作为这一系列的两篇文章中的头 一篇,本文探讨了用于代码重用的面向对象编程思想的一些影响,并把它们与一些更函数化的可选方法,比如说组合,进行比较。

面向对象编程通过封装变动部分把代码变成易懂的,函数式编程则是通过最小化变动部分来把代码变成易懂的。——Michael Feathers,Working with Legacy Code一书的作者,经由Twitter

每天都以某种特定的抽象来进行编码工作,这种抽象会逐渐渗透到你的大脑中,影响到你解决问题的方式。这一文章系列的目标之一是说明如何以一种函数方式看待典型的问题。就本文和下一篇文章来说,我通过重构和随之带来的抽象影响来解决代码的重用问题。

面向对象的目标之一是使封装和状态操作更加容易,因此,其抽象倾向于使用状态来解决常见的问题,而这意味会用到多个类和交互——这就是前面引述 Michael Feathers的话中所说的“变动部分”。函数式编程尝试通过把各部分组合在一起而不是把结构耦合在一起来最小化变动的部分,这是一个微妙的概念,对于 其经验主要体现在面向对象语言方面的开发者来说,不太容易体会到。

经由结构的代码重用



命令式的(特别是)面向对象的编程风格使用结构和消息来作为构建块。若要重用面向对象的代码,你需要把对象代码提取到另一个类中,然后使用继承来访问它。

无意导致的代码重复

为 了说明代码的重用及其影响,我重提之前的文章用来说明代码结构和风格的一个数字分类器版本,该分类器确定一个正数是富余的(abundant)、完美的 (perfect)还是欠缺的(deficient),如果数字因子的总和大于数字的两倍,它就是富余的,如果总和等于数字的两倍,它就是完美的,否则 (如果总和小于数字的两倍)就是欠缺的。

你还可以编写这样的代码,使用正数的因子来确定它是否是一个素数(定义是,一个大于1的整数,它的因子只有1和它自身)。因为这两个问题都依赖于数字的因子,因此它们是用于重构从而也是用于说明代码重用风格的很好的可选案例。

清单1给出了使用命令式风格编写的数字分类器:

清单1. 命令式的数字分类器

import java.util.HashSet;

import java.util.Iterator;

import java.util.Set;

import static java.lang.Math.sqrt;

public class ClassifierAlpha {

private int number;

public ClassifierAlpha(int number) {

this.number = number;

}

public boolean isFactor(int potential_factor) {

return number % potential_factor == 0;

}

public Set factors() {

HashSet factors = new HashSet();

for (int i = 1; i <= sqrt(number); i++)

if (isFactor(i)) {

factors.add(i);

factors.add(number / i);

}

return factors;

}

static public int sum(Set factors) {

Iterator it = factors.iterator();

int sum = 0;

while (it.hasNext())

sum += (Integer) it.next();

return sum;

}

public boolean isPerfect() {

return sum(factors()) - number == number;

}

public boolean isAbundant() {

return sum(factors()) - number > number;

}

public boolean isDeficient() {

return sum(factors()) - number < number;

}

}

我在第一部分内容中已讨论了这一代码的推导过程,因此我现在就不再重复了。该例子在这里的目标是说明代码的重用,因此我给出了清单2中的代码,该部分代码检测素数:

清单2. 素数测试,以命令方式来编写

import java.util.HashSet;

import java.util.Set;

import static java.lang.Math.sqrt;

public class PrimeAlpha {

private int number;

public PrimeAlpha(int number) {

this.number = number;

}

public boolean isPrime() {

Set primeSet = new HashSet() {{

add(1); add(number);}};

return number > 1 &&

factors().equals(primeSet);

}

public boolean isFactor(int potential_factor) {

return number % potential_factor == 0;

}

public Set factors() {

HashSet factors = new HashSet();

for (int i = 1; i <= sqrt(number); i++)

if (isFactor(i)) {

factors.add(i);

factors.add(number / i);

}

return factors;

}

}

清单2中出现了几个值得注意的事项,首先是isPrime()方法中的初始化代码有些不同寻常,这是一个实例初始化器的例子(若要了解更多关于实例初始化——一种附带了函数式编程的Java技术——这一方面的内容,请参阅“Evolutionary architecture and emergent design: Leveraging reusable code, Part 2”。)

清单2中令人感兴趣的其他部分是isFactor()和factors()方法。可以注意到,它们与(清单1的)ClassifierAlpha类中的相应部分相同,这是分开独立实现两个解决方案的自然结果,这让你意识到你用到了相同的功能。

通过重构来消除重复

这一类重复的解决方法是使用单个的Factors类来重构代码,如清单3所示:

清单3. 一般重构后的因子提取代码

import java.util.Set;

import static java.lang.Math.sqrt;

import java.util.HashSet;

public class FactorsBeta {

protected int number;

public FactorsBeta(int number) {

this.number = number;

}

public boolean isFactor(int potential_factor) {

return number % potential_factor == 0;

}

public Set getFactors() {

HashSet factors = new HashSet();

for (int i = 1; i <= sqrt(number); i++)

if (isFactor(i)) {

factors.add(i);

factors.add(number / i);

}

return factors;

}

}

清 单3中的代码是使用提取超类(Extract Superclass)这一重构做法的结果,需要注意的是,因为两个提取出来的方法都使用了number这一成员变量,因此它也被放到了超类中。在执行这 一重构时,IDE询问我想要如何处理访问(访问器对、保护范围等等),我选择了protected(受保护)这一作用域,这一选择把number加入了类 中,并创建了一个构造函数来设置它的值。

一旦我孤立并删除了重复的代码,数字分类器和素数测试器两者就都变得简单多了。清单4给出了重构后的数字分类器:

清单4. 重构后简化了的数字分类器

import java.util.Iterator;

import java.util.Set;

public class ClassifierBeta extends FactorsBeta {

public ClassifierBeta(int number) {

super(number);

}

public int sum() {

Iterator it = getFactors().iterator();

int sum = 0;

while (it.hasNext())

sum += (Integer) it.next();

return sum;

}

public boolean isPerfect() {

return sum() - number == number;

}

public boolean isAbundant() {

return sum() - number > number;

}

public boolean isDeficient() {

return sum() - number < number;

}

}

清单5给出了重构后的素数测试器

清单5. 重构后简化了的素数测试器

import java.util.HashSet;

import java.util.Set;

public class PrimeBeta extends FactorsBeta {

public PrimeBeta(int number) {

super(number);

}

public boolean isPrime() {

Set primeSet = new HashSet() {{

add(1); add(number);}};

return getFactors().equals(primeSet);

}

}

无论在重构时为number成员选择的访问选项是哪一种,你在考虑这一问题时都必须要处理类之间的网络关系。通常这是一件好事,因为其允许你独立出问题的某些部分,但在修改父类时也会带来不利的后果。

这 是一个通过耦合(coupling)来重用代码的例子:通过number域这一共享状态和超类的getFactors()方法来把两个元素(在本例中是 类)捆绑在一起。换句话说,这种做法起作用是因为利用了内置在语言中的耦合规则。面向对象定义了耦合的交互方式(比如说,你通过继承访问成员变量的方 式),因此你拥有了关于事情如何交互的一些预定义好的风格——这没有什么问题,因为你可以以一种一致的方式来推理行为。不要误解我——我并非是在暗示使用 继承是一个糟糕的主意,相反,我的意思是,它在面向对象的语言中被过度使用,结果取代了另一种有着更好特性的抽象。

经由组合的代码重用



在这一系列的第二部分内容中,我给出了一个用Java编写的数字分类器的函数式版本,如清单6所示:

清单6. 数字分类器的一个更加函数化的版本

public class FClassifier {

static public boolean isFactor(int number, int potential_factor) {

return number % potential_factor == 0;

}

static public Set factors(int number) {

HashSet factors = new HashSet();

for (int i = 1; i <= sqrt(number); i++)

if (isFactor(number, i)) {

factors.add(i);

factors.add(number / i);

}

return factors;

}

public static int sumOfFactors(int number) {

Iterator it = factors(number).iterator();

int sum = 0;

while (it.hasNext())

sum += it.next();

return sum;

}

public static boolean isPerfect(int number) {

return sumOfFactors(number) - number == number;

}

public static boolean isAbundant(int number) {

return sumOfFactors(number) - number > number;

}

public static boolean isDeficient(int number) {

return sumOfFactors(number) - number < number;

}

}

我也有素数测试器的一个函数式版本(使用了纯粹的函数,没有共享状态),该版本的 isPrime()方法如清单7所示。其余部分代码与清单6中的相同命名方法的代码一样。

清单7. 素数测试器的函数式版本

public static boolean isPrime(int number) {

Set factors = factors(number);

return number > 1 &&

factors.size() == 2 &&

factors.contains(1) &&

factors.contains(number);

}

就像我在命令式版本中所做的那样,我把重复的代码提取到它自己的Factors类中,基于可读性,我把factors方法的名称改为of,如图8所示:

清单8 函数式的重构后的Factors类

import java.util.HashSet;

import java.util.Set;

import static java.lang.Math.sqrt;

public class Factors {

static public boolean isFactor(int number, int potential_factor) {

return number % potential_factor == 0;

}

static public Set of(int number) {

HashSet factors = new HashSet();

for (int i = 1; i <= sqrt(number); i++)

if (isFactor(number, i)) {

factors.add(i);

factors.add(number / i);

}

return factors;

}

}

因为函数式版本中所有状态都是作为参数传递的,因此提取出来的这部分内容没有共享状态。一旦提取了该类之后,我就可以重构函数式的分类器和素数测试器来使用它了。清单9给出了重构后的分类器:

清单9. 重构后的数字分类器

public class FClassifier {

public static int sumOfFactors(int number) {

Iterator it = Factors.of(number).iterator();

int sum = 0;

while (it.hasNext())

sum += it.next();

return sum;

}

public static boolean isPerfect(int number) {

return sumOfFactors(number) - number == number;

}

public static boolean isAbundant(int number) {

return sumOfFactors(number) - number > number;

}

public static boolean isDeficient(int number) {

return sumOfFactors(number) - number < number;

}

}

清单10给出了重构后的素数测试器:

清单10. 重构后的素数测试器

import java.util.Set;

public class FPrime {

public static boolean isPrime(int number) {

Set factors = Factors.of(number);

return number > 1 &&

factors.size() == 2 &&

factors.contains(1) &&

factors.contains(number);

}

}

可以注意到,我并未使用任何特殊的库或是语言来把第二个版本变得更加的函数化,相反,我通过使用组合而不是耦合式的代码重用做到了这一点。清单9和清单10都用到了Factors类,但它的使用完全是包含在了单独方法的内部之中。

耦 合和组合之间的区别很细微但很重要,在一个像这样的简单例子中,你可以看到显露出来的代码结构骨架。但是,当你最终重构的是一个大型的代码库时,耦合就显 得无处不在了,因为这是面向对象语言中的重用机制之一。繁复的耦合结构的难以理解性损害到了面向对象语言的重用性,把有效的重用局限在了诸如对象-关系映 射和构件库一类已明确定义的技术领域上,当我们在写少量的明显结构化的Java代码时(比如说你在业务应用中编写的代码),这种层面的重用我们就用不上 了。

你可以通过这样的做法来改进命令式的版本,即在重构期间会告之哪些内容由IDE提供,先客气地拒绝,然后使用组合来替代。

结束语



作 为一个更函数化的编程者来进行思考,这意味着以不同的方式来思考编码的各个方面。代码重用显然是开发的一个目标,命令式抽象倾向于以不同于函数式编程者的 方式来解决该问题。这部分内容对比了代码重用的两种方式:经由继承的耦合方式和经由参数的组合方式。下一部分内容会继续探讨这一重要的分歧。

时间: 06-09

函数式编程思想:耦合和组合,第1部分的相关文章

函数式编程思想:耦合和组合,第2部分

习惯于使用面向对象构建块(继承.多态等)的编程者可能会对这一方法的缺点及其他的可选做法视而不见,函数式编程使用不同的构建块来实现重用,其基于的是 更一般化的概念,比如说列表转换和可移植代码.函数式编程思想的这一部分内容比较了作为重用机制的经由继承的耦合和组合,指出了命令式编程和函数式编程之 间的主要区别之一. 在上一部分内容中,我说明了代码重用的不同做法.在面向对象的版本中,我提取出了重复的方法,把他们和一个受保护(protected)域一起移到 一个超类中.在函数式版本中,我把纯函数(不会带来

函数式编程思想:以函数的方式思考,第3部分

过滤.单元测试和代码重用技术 译者:Elaine.Ye原文作者:Neal Ford 发布:2011-07-06 11:23:24挑错 | 查看译者版本 | 收藏本文 在函数式编程思想的第一部分和第二部分中, 我考察了一些函数式编程的主题,研究了这些主题如何与Java?及其相关语言产生关联.本篇文章继续这一探索过程,给出来自前面文章的数字分类器的一个 Scala版本,并会讨论一些颇具学术色彩的主题,比如说局部套用(currying).部分应用(partial application)和递归等. 用

[技术] 谈谈编程思想

https://zhuanlan.zhihu.com/p/19736530?columnSlug=prattle 作者:陈天链接:https://zhuanlan.zhihu.com/p/19736530来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 这段时间又攒了很多答应了,但还未动手的文章.大概一两周前,有个读者留言:「程序君,能发篇文章有关编程思想的吗?我是编程初学者,对编程思想没啥概念,求传授点经验!」 今天就讲讲编程思想.编程思想是个宏大的主题,我不敢保

Haskell 函数式编程快速入门【草】

什么是函数式编程 用常规编程语言中的函数指针.委托和Lambda表达式等概念来帮助理解(其实函数式编程就是Lambda演算延伸而来的编程范式). 函数式编程中函数可以被非常容易的定义和传递. Haskell 快速入门 概述 Haskell是一个按照纯函数式编程思想创造的语言,支持静态类型.类型推断.惰性处理(推迟计算).支持并发编程. 安装 从官方网站的下载页面 https://www.haskell.org/downloads 根据自己的操作系统选择. 第一次接触Haskell

IOS编程思想的概念

iOS几大编程思想 面向对象思想:万物皆对象,做一件事情的过程转变为对象处理事件的过程. 链式编程思想:将多个操作通过点(.)链接在一起成为一句代码,使得代码更好阅读.例如p.add(1).add(2).特别的地方在于每个方法返回一个block,这个block的返回值又是这个对象本身,block的参数需要自己考虑实际情况.这里在多讲一下,为什么add后面要加括号,还有参数.其实这个就是调用了这个block(block的调用就是括号,里面添加参数).其代表最出名的第三方框架是Masonry.不懂的

函数式编程 读书笔记

函数式编程 函数式编程思想:在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值. 已经掌握的使用场景: 1.      获取集合中的最大或最小值,当集合类型为自定义类型时的使用比较器 2.      循环进行一些操作.foreEach( ) 3.      统计符合条件的有多少个 List.stream().filter( 条件).count(); .map(  ) : 方法将一个流中的值转换成一个新的流 .filter(   ) :  方法将流进行过滤,保留符合条件的(返回

函数式编程与面向对象编程的比较

函数式编程作为结构化编程的一种,正在受到越来越多的重视.工程中不在只是面向对象编程,更多的人尝试着开始使用函数式编程来解决软件工程中遇到的问题. 什么是函数式编程?在维基百科中给出了详细的定义,函数式编程(英语:functional programming)或称函数程序设计,又称泛函编程,是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象.函数编程语言最重要的基础是λ演算(lambda calculus).而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值

何为编程思想?

有一个很不错的概念叫做“Unknown Unknown”,意思是如果你不知道一个东西的话,你也不会知道你自己不知道它. 众所周知大家所熟悉的主流编程思想还是面向对象编程,然而,并不是只存在于这一种方式.我们要习惯去怀疑生活工作中的一些既定的理论和方法,没有确凿理论依据的出现,我们还是要相信有其他的情况,只是我们还没有发现它. 面向对象OOP 面向对象的编程思想最大的特色就是可以编写自己所需的数据类型,以更好的解决问题.“类”就是描述了一组有相同特性(属性)和相同行为(方法)的集合. 抽象成了其中

js 函数式编程 浅谈

js 函数式编程 函数式的思想, 就是不断地用已有函数, 来组合出新的函数. 函数式编程具有五个鲜明的特点: 1. 函数是"第一等公民" 指的是函数与其他数据类型一样,处于平等地位 2. 只用"表达式",不用"语句" "表达式"(expression)是一个单纯的运算过程,总是有返回值: "语句"(statement)是执行某种操作,没有返回值. 3. 没有"副作用" 指的是函数内部与外