函数式思维: 耦合和组合,第 1 部分

探讨原生耦合抽象的含义

每天都与特定的抽象(如面向对象)打交道,当它把您引向一个不是最佳的解决方案时,您几乎都无法察觉到。本文探讨代码重用中的面向对象思维的含义,并将它们与更加函数式的方案,如组合,进行比较。

Neal Ford, 软件架构师, ThoughtWorks Inc.

Neal FordNeal Ford 是一家全球性 IT 咨询公司 ThoughtWorks 的软件架构师和 Meme Wrangler。他的工作还包括设计和开发应用程序、教材、杂志文章、课件和视频/DVD 演示,而且他是各种技术书籍的作者或编辑,包括最近的新书 The Productive Programmer。他主要的工作重心是设计和构建大型企业应用程序。他还是全球开发人员会议上的国际知名演说家。请访问他的 Web 站点



2011 年 11 月 28 日

关于本系列

本系列旨在重新调整您的函数式思维定势,帮助您以新的方式来思考常见的问题,并找出改进您日常的编码工作的方法。探讨了函数式编程概念、允许使用 Jave 语言进行函数式编程的框架、在 JVM 上运行的函数式编程语言以及语言设计的一些未来学习方向。本系列面向那些了解 Java 语言及其抽象工作原理但又没有或只有少许关于函数式语言使用经验的开发人员。

面向对象的编程通过封装移动部件来让代码变得易于理解,而函数式编程则通过尽量减少 移动部件来使代码变得易于理解。
— Michael Feathers,Working with Legacy Code 的作者

每天都与特定的抽象打交道,它会慢慢的渗透到您的脑中,影响您解决问题的方法。本系列文章的目标之一是说明一种看待传统问题的函数式方法。为此,在本文与此后的文章,我将通过重构以及随之而来的抽象影响来处理代码重用问题。

面向对象的其中一个目标是使封装和状态处理更加简单。因此,它的抽象倾向于使用状态来解决常见问题,这里指的是多个类和交互的使用(引用 Michael Feathers 上面的话为 “移动部件”)。函数式编程设法通过组合 部件而不是耦合 结构来尽量减少移动部件。对于主要从事于面向对象语言的开发人员来说,这是一个难懂的概念。

通过结构实现代码重用

命令式的(特别是)面向对象编程风格采用结构和消息传递作为构建块。要重用面向对象的代码,您要将目标代码提取(extract)到另一个类中,然后再用继承来访问它。

疏忽的代码重复

要说明代码重用及其含义,我要回过来谈谈先前文章说明代码结构和风格所用的数字分类器。分类器确定正整数为过剩数(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<Integer> factors() {
        HashSet<Integer> factors = new HashSet<Integer>();
        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<Integer> 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;
    }

}

我在系列的 第 1 部分 中已讨论了代码的实现,所以在此不再重复。这里主要是说明代码重用。使用清单 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<Integer> primeSet = new HashSet<Integer>() {{
            add(1); add(number);}};
        return number > 1 &&
                factors().equals(primeSet);
    }

    public boolean isFactor(int potential_factor) {
        return number % potential_factor == 0;
    }

    public Set<Integer> factors() {
        HashSet<Integer> factors = new HashSet<Integer>();
        for (int i = 1; i <= sqrt(number); i++)
            if (isFactor(i)) {
                factors.add(i);
                factors.add(number / i);
            }
        return factors;
    }
}

清单 2 中有几点需要注意。第一个是在 isPrime() 方法中加入了有点奇怪的初始化代码。这是一个关于实例初始值设定项 的示例。了解更多关于实例初始化的信息(函数式编程所附带的一种 Java 技术),请参阅 “演化架构和紧急设计: 利用可重用代码,第 2 部分”。

清单 2 中另要注意的是 isFactor()factors() 方法。注意,它们与 ClassifierAlpha 类(在 清单 1)中的对应方法相同。这是单独实施两个解决方案并意识到您实际上有两个相同的功能所产生的自然结果。

采用重构去除重复

去除重复的解决方案是将代码重构为一个单一的 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<Integer> getFactors() {
        HashSet<Integer> factors = new HashSet<Integer>();
        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 scope),将 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<Integer> primeSet = new HashSet<Integer>() {{
            add(1); add(number);}};
        return getFactors().equals(primeSet);
    }
}

不管您在重构时为 number 成员所选择的访问选项是什么,当您考虑到此问题时,必须处理一组类。这通常是一件好事,因为它充许您将问题分成几部分,但是当您对父类进行变更时,它会产生不好的影响。

这是一个通过耦合 所实现的代码重用示例:通过超类中的 number 字段和 getFactors() 方法的共享状态来连接两个元素(在本例中为类)。换言之,就是采用语言中内置的耦合规则。面向对象定义了耦合的交互风格(例如,您如何通过继承访问成员变量), 所以您要预先定义耦合的规则,这样很好,因为您可以用一致的方式来推出行为。不要误解我的意思,我并不是说用继承不太好。而是说,它被过多地用于面向对象的语言中来取代其他拥有更优秀特征的抽象。


通过组合实现代码重用

在本系列的 第 2 部分中,我介绍了 Java 函数版的数字分类器,如清单 6 所示:

清单 6. 更加函数化版本的数字分类器
public class FClassifier {

    static public boolean isFactor(int number, int potential_factor) {
        return number % potential_factor == 0;
    }

    static public Set<Integer> factors(int number) {
        HashSet<Integer> factors = new HashSet<Integer>();
        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<Integer> 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<Integer> 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<Integer> of(int number) {
        HashSet<Integer> factors = new HashSet<Integer>();
        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<Integer> 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<Integer> factors = Factors.of(number);
        return number > 1 &&
                factors.size() == 2 &&
                factors.contains(1) &&
                factors.contains(number);
    }
}

注意:我并没有使用任何特殊的库或语言来使第二个版本更加函数化。相反,我是通过使用组合 而非耦合来实现代码重用。清单 9清单 10 都使用了 Factors 类,但是对于它的使用是完全包含在单独的方法中的。

耦合与组合的区别非常细微但是又很重要。举一个简单的例子,您可以看到呈现代码结构的框架。然而,当您重构大型的代码库后,到处都会出现耦合的地方,因为那是面向对象语言中的其中 一种重用机制。对于丰富的耦合结构的理解困难阻碍了面向对象语言中的重用,限制了定义明确的技术域(如对象关系映射和小部件库)的有效重用。在编写不太明显的结构化 Java 代码时(比如您在业务应用程序中所编写的代码),我们无法做到同一级的重用。


结束语

像更加函数式的程序员一样思考指的是换个方式来考虑编码的各个方面。代码重用是一个显著的开发目标,而命令式抽象倾向于用不同于函数式程序员解决问题的方法来解决问题。本文对两种代码重用进行了比较:通过继承实现的耦合和通过参数实现的组合。下一篇文件将继续讨论这一重要的内容。

参考资料

学习

获得产品和技术

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=776976
ArticleTitle=函数式思维: 耦合和组合,第 1 部分
publish-date=11282011