函数式思维
耦合和组合,第 1 部分
探讨原生耦合抽象的含义
系列内容:
此内容是该系列 # 部分中的第 # 部分: 函数式思维
此内容是该系列的一部分:函数式思维
敬请期待该系列的后续内容。
面向对象的编程通过封装移动部件来让代码变得易于理解,而函数式编程则通过尽量减少 移动部件来使代码变得易于理解。
— 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 代码时(比如您在业务应用程序中所编写的代码),我们无法做到同一级的重用。
结束语
像更加函数式的程序员一样思考指的是换个方式来考虑编码的各个方面。代码重用是一个显著的开发目标,而命令式抽象倾向于用不同于函数式程序员解决问题的方法来解决问题。本文对两种代码重用进行了比较:通过继承实现的耦合和通过参数实现的组合。下一篇文件将继续讨论这一重要的内容。
相关主题
- The Productive Programmer(Neal Ford,O'Reilly Media,2008 年):Neal Ford 最近出版的书,讨论了有助于您提高代码效率的工具和实践。
- Functional Java:函数式 Java 是一个框架,可向 Java 增加很多函数式语言结构。
- Stuart Halloway on Clojure:在 developerWorks 播客上了解更多关于 Clojure 的信息(一种运行在 JVM 上的现代函数式 Lisp)。
- 面向 Java 开发人员的 Scala 指南系列:Scala 是另一个运行在 JVM 上的现代函数语言。通过 Ted Neward 编写的 developerWorks 系列文章深入了解 Scala。
- developerWorks 中国网站 Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。
- 下载 IBM 产品评估试用版软件 或 IBM SOA 人员沙箱,并开始使用来自 DB2®、 Lotus®、 Rational®、 Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。