函数式编程思想:耦合和组合,第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提供,先客气地拒绝,然后使用组合来替代。

结束语



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

时间: 2024-10-12 23:30:34

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

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

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

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

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

面向对象设计思想和函数式编程思想

1.函数式编程思想:核心主体是函数,函数可以作为参数,返回值,具有高度不可变性,是以函数为主体,如果传入参数是相等的,那么返回结果注定是相等. 2.面向对象编程思想:(1)封装:封装变化,可以做到重用,实现代码优雅简洁. (2) 继承: 子类继承父类,实现子类可以继承父类的方法和属性,同时可以自己扩展,实现了代码的可扩展性,让程序更加灵活. (3) 多态:多种形态.说白了就是两种体现形式:第一种就是向上转型,父类可以指向子类,实现多态. 第二种就是方法的重载,一个相同的方法作用给不同的对象,返回

Js 函数式编程思想 (V客学院知识分享)

随之ECMAScript 标准规范不断更新,现在已经更新到ES7,不久ES8规范即将面世,为了是JS 语法对函数编程更加友好,诸如 RxJS (ReactiveX) 等函数式框架的不断流行.函数式编程则应该是以函数做为舰载主体,然后对函数进行拆分封装.更加抽象,可扩展性极强. 与传统命令式函数相比存在那些优势? 语法精简清晰 通用性更好 维护及可扩展性更好 限制作用域 以下列举函数对比 // 数组中每个单词,首字母大写 // 一般写法 const arr = ['apple', 'pen', '

阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第6节 Lambda表达式_1_函数式编程思想概述

函数式编程和面向对象的区别 原文地址:https://www.cnblogs.com/wangjunwei/p/11261580.html

JavaScript函数式编程(1):基本思想

1 函数式编程简介 函数式编程是和传统命令式编程区分的一种编程思想,"在函数式编程语言中,函数是第一类的对象,也就是说,函数 不依赖于任何其他的对象而可以独立存在,而在面向对象的语言中,函数 ( 方法 ) 是依附于对象的,属于对象的一部分.这一点决定了函数在函数式语言中的一些特别的性质,比如作为传出 / 传入参数,作为一个普通的变量等.[1]" 函数式编程思想的源头可以追溯到 20 世纪 30 年代,数学家阿隆左 . 丘奇在进行一项关于问题的可计算性的研究,也就是后来的 lambda

JS函数式编程【译】4.2 函数组合

?? Functional Programming in Javascript 主目录第四章 在Javascript中实现函数式编程的技术 函数组合 终于,我们到了函数组合. 在函数式编程中,我们希望一切都是函数,尤其希望是一元函数,如果可能的话.如果可以把所有的函数转换为一元函数, 将发生神奇的事情. 一元函数是只接受单个输入的函数.函数如果有多个输入就是多元的,不过我们一般把接受两个输入的叫二元函数, 把接受三个输入的叫三元函数. 有的函数接受的输入的数量并不确定,我们称它为可变的. 操作函

[技术] 谈谈编程思想

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

C#函数式编程

转载:http://www.admin10000.com/document/9216.html 提起函数式编程,大家一定想到的是语法高度灵活和动态的LISP,Haskell这样古老的函数式语言,往近了说ruby,javascript,F#也是函数式编程的流行语言.然而自从.net支持了lambda表达式,C#虽然作为一种指令式程序设计语言,在函数式编程方面也毫不逊色.我们在使用c#编写代码的过程中,有意无意的都会使用高阶函数,组合函数,纯函数缓存等思想,连表达式树这样的idea也来自函数式编程思