函數式編程思想:耦合和組合
面向對象編程通過封裝變動部分把代碼變成易懂的,函數式編程則是通過最小化變動部分來把代碼變成易懂的。——Michael Feathers,Working with Legacy Code一書的作者
每天都以某種特定的抽象來進行編碼工作,這種抽象會逐漸滲透到你的大腦中,影響到你解決問題的方式。這一文章系列的目標之一是說明如何以一種函數方式看待典型的問題。就本文和下一篇文章來說,我通過重構和隨之帶來的抽象影響來解決代碼的重用問題。
面向對象的目標之一是使封裝和狀態操作更加容易,因此,其抽象傾向于使用狀態來解決常見的問題,而這意味會用到多個類和交互——這就是前面引述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. 重構后簡化了的數字分類器
- mport 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 factorsfactors = factors(number);
- return number > 1 &&
- factors.size() == 2 &&
- factors.contains(1) &&
- factors.contains(number);
- }
就像我在命令式版本中所做的那樣,我把重復的代碼提取到它自己的Factors類中,基于可讀性,我把factors方法的名稱改為of,如圖8所示:
清單8 函數式的重構后的Factors類
- mport 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提供,先客氣地拒絕,然后使用組合來替代。
結束語
作為一個更函數化的編程者來進行思考,這意味著以不同的方式來思考編碼的各個方面。代碼重用顯然是開發的一個目標,命令式抽象傾向于以不同于函數式編程者的方式來解決該問題。這部分內容對比了代碼重用的兩種方式:經由繼承的耦合方式和經由參數的組合方式。下一部分內容會繼續探討這一重要的分歧。
原文:http://article.yeeyan.org/view/213582/224721