函數(shù)式編程思想:耦合和組合
面向?qū)ο缶幊掏ㄟ^封裝變動(dòng)部分把代碼變成易懂的,函數(shù)式編程則是通過最小化變動(dòng)部分來把代碼變成易懂的。——Michael Feathers,Working with Legacy Code一書的作者
每天都以某種特定的抽象來進(jìn)行編碼工作,這種抽象會(huì)逐漸滲透到你的大腦中,影響到你解決問題的方式。這一文章系列的目標(biāo)之一是說明如何以一種函數(shù)方式看待典型的問題。就本文和下一篇文章來說,我通過重構(gòu)和隨之帶來的抽象影響來解決代碼的重用問題。
面向?qū)ο蟮哪繕?biāo)之一是使封裝和狀態(tài)操作更加容易,因此,其抽象傾向于使用狀態(tài)來解決常見的問題,而這意味會(huì)用到多個(gè)類和交互——這就是前面引述Michael Feathers的話中所說的“變動(dòng)部分”。函數(shù)式編程嘗試通過把各部分組合在一起而不是把結(jié)構(gòu)耦合在一起來最小化變動(dòng)的部分,這是一個(gè)微妙的概念,對(duì)于其經(jīng)驗(yàn)主要體現(xiàn)在面向?qū)ο笳Z言方面的開發(fā)者來說,不太容易體會(huì)到。
經(jīng)由結(jié)構(gòu)的代碼重用
命令式的(特別是)面向?qū)ο蟮木幊田L(fēng)格使用結(jié)構(gòu)和消息來作為構(gòu)建塊。若要重用面向?qū)ο蟮拇a,你需要把對(duì)象代碼提取到另一個(gè)類中,然后使用繼承來訪問它。
無意導(dǎo)致的代碼重復(fù)
為了說明代碼的重用及其影響,我重提之前的文章用來說明代碼結(jié)構(gòu)和風(fēng)格的一個(gè)數(shù)字分類器版本,該分類器確定一個(gè)正數(shù)是富余的(abundant)、完美的(perfect)還是欠缺的(deficient),如果數(shù)字因子的總和大于數(shù)字的兩倍,它就是富余的,如果總和等于數(shù)字的兩倍,它就是完美的,否則(如果總和小于數(shù)字的兩倍)就是欠缺的。
你還可以編寫這樣的代碼,使用正數(shù)的因子來確定它是否是一個(gè)素?cái)?shù)(定義是,一個(gè)大于1的整數(shù),它的因子只有1和它自身)。因?yàn)檫@兩個(gè)問題都依賴于數(shù)字的因子,因此它們是用于重構(gòu)從而也是用于說明代碼重用風(fēng)格的很好的可選案例。
清單1給出了使用命令式風(fēng)格編寫的數(shù)字分類器:
清單1. 命令式的數(shù)字分類器
- 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;
- }
- }
我在第一部分內(nèi)容中已討論了這一代碼的推導(dǎo)過程,因此我現(xiàn)在就不再重復(fù)了。該例子在這里的目標(biāo)是說明代碼的重用,因此我給出了清單2中的代碼,該部分代碼檢測(cè)素?cái)?shù):
清單2. 素?cái)?shù)測(cè)試,以命令方式來編寫
- 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中出現(xiàn)了幾個(gè)值得注意的事項(xiàng),首先是isPrime()方法中的初始化代碼有些不同尋常,這是一個(gè)實(shí)例初始化器的例子(若要了解更多關(guān)于實(shí)例初始化——一種附帶了函數(shù)式編程的Java技術(shù)——這一方面的內(nèi)容,請(qǐng)參閱“Evolutionary architecture and emergent design: Leveraging reusable code, Part 2”。)
清單2中令人感興趣的其他部分是isFactor()和factors()方法。可以注意到,它們與(清單1的)ClassifierAlpha類中的相應(yīng)部分相同,這是分開獨(dú)立實(shí)現(xiàn)兩個(gè)解決方案的自然結(jié)果,這讓你意識(shí)到你用到了相同的功能。
通過重構(gòu)來消除重復(fù)
這一類重復(fù)的解決方法是使用單個(gè)的Factors類來重構(gòu)代碼,如清單3所示:
清單3. 一般重構(gòu)后的因子提取代碼
- 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)這一重構(gòu)做法的結(jié)果,需要注意的是,因?yàn)閮蓚€(gè)提取出來的方法都使用了number這一成員變量,因此它也被放到了超類中。在執(zhí)行這一重構(gòu)時(shí),IDE詢問我想要如何處理訪問(訪問器對(duì)、保護(hù)范圍等等),我選擇了protected(受保護(hù))這一作用域,這一選擇把number加入了類中,并創(chuàng)建了一個(gè)構(gòu)造函數(shù)來設(shè)置它的值。
一旦我孤立并刪除了重復(fù)的代碼,數(shù)字分類器和素?cái)?shù)測(cè)試器兩者就都變得簡單多了。清單4給出了重構(gòu)后的數(shù)字分類器:
清單4. 重構(gòu)后簡化了的數(shù)字分類器
- 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給出了重構(gòu)后的素?cái)?shù)測(cè)試器
清單5. 重構(gòu)后簡化了的素?cái)?shù)測(cè)試器
- 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);
- }
- }
無論在重構(gòu)時(shí)為number成員選擇的訪問選項(xiàng)是哪一種,你在考慮這一問題時(shí)都必須要處理類之間的網(wǎng)絡(luò)關(guān)系。通常這是一件好事,因?yàn)槠湓试S你獨(dú)立出問題的某些部分,但在修改父類時(shí)也會(huì)帶來不利的后果。
這是一個(gè)通過耦合(coupling)來重用代碼的例子:通過number域這一共享狀態(tài)和超類的getFactors()方法來把兩個(gè)元素(在本例中是類)捆綁在一起。換句話說,這種做法起作用是因?yàn)槔昧藘?nèi)置在語言中的耦合規(guī)則。面向?qū)ο蠖x了耦合的交互方式(比如說,你通過繼承訪問成員變量的方式),因此你擁有了關(guān)于事情如何交互的一些預(yù)定義好的風(fēng)格——這沒有什么問題,因?yàn)槟憧梢砸砸环N一致的方式來推理行為。不要誤解我——我并非是在暗示使用繼承是一個(gè)糟糕的主意,相反,我的意思是,它在面向?qū)ο蟮恼Z言中被過度使用,結(jié)果取代了另一種有著更好特性的抽象。
經(jīng)由組合的代碼重用
在這一系列的第二部分內(nèi)容中,我給出了一個(gè)用Java編寫的數(shù)字分類器的函數(shù)式版本,如清單6所示:
清單6. 數(shù)字分類器的一個(gè)更加函數(shù)化的版本
- 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;
- }
- }
我也有素?cái)?shù)測(cè)試器的一個(gè)函數(shù)式版本(使用了純粹的函數(shù),沒有共享狀態(tài)),該版本的 isPrime()方法如清單7所示。其余部分代碼與清單6中的相同命名方法的代碼一樣。
清單7. 素?cái)?shù)測(cè)試器的函數(shù)式版本
- public static boolean isPrime(int number) {
- Set factorsfactors = factors(number);
- return number > 1 &&
- factors.size() == 2 &&
- factors.contains(1) &&
- factors.contains(number);
- }
就像我在命令式版本中所做的那樣,我把重復(fù)的代碼提取到它自己的Factors類中,基于可讀性,我把factors方法的名稱改為of,如圖8所示:
清單8 函數(shù)式的重構(gòu)后的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;
- }
- }
因?yàn)楹瘮?shù)式版本中所有狀態(tài)都是作為參數(shù)傳遞的,因此提取出來的這部分內(nèi)容沒有共享狀態(tài)。一旦提取了該類之后,我就可以重構(gòu)函數(shù)式的分類器和素?cái)?shù)測(cè)試器來使用它了。清單9給出了重構(gòu)后的分類器:
清單9. 重構(gòu)后的數(shù)字分類器
- 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給出了重構(gòu)后的素?cái)?shù)測(cè)試器:
清單10. 重構(gòu)后的素?cái)?shù)測(cè)試器
- 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);
- }
- }
可以注意到,我并未使用任何特殊的庫或是語言來把第二個(gè)版本變得更加的函數(shù)化,相反,我通過使用組合而不是耦合式的代碼重用做到了這一點(diǎn)。清單9和清單10都用到了Factors類,但它的使用完全是包含在了單獨(dú)方法的內(nèi)部之中。
耦合和組合之間的區(qū)別很細(xì)微但很重要,在一個(gè)像這樣的簡單例子中,你可以看到顯露出來的代碼結(jié)構(gòu)骨架。但是,當(dāng)你最終重構(gòu)的是一個(gè)大型的代碼庫時(shí),耦合就顯得無處不在了,因?yàn)檫@是面向?qū)ο笳Z言中的重用機(jī)制之一。繁復(fù)的耦合結(jié)構(gòu)的難以理解性損害到了面向?qū)ο笳Z言的重用性,把有效的重用局限在了諸如對(duì)象-關(guān)系映射和構(gòu)件庫一類已明確定義的技術(shù)領(lǐng)域上,當(dāng)我們?cè)趯懮倭康拿黠@結(jié)構(gòu)化的Java代碼時(shí)(比如說你在業(yè)務(wù)應(yīng)用中編寫的代碼),這種層面的重用我們就用不上了。
你可以通過這樣的做法來改進(jìn)命令式的版本,即在重構(gòu)期間會(huì)告之哪些內(nèi)容由IDE提供,先客氣地拒絕,然后使用組合來替代。
結(jié)束語
作為一個(gè)更函數(shù)化的編程者來進(jìn)行思考,這意味著以不同的方式來思考編碼的各個(gè)方面。代碼重用顯然是開發(fā)的一個(gè)目標(biāo),命令式抽象傾向于以不同于函數(shù)式編程者的方式來解決該問題。這部分內(nèi)容對(duì)比了代碼重用的兩種方式:經(jīng)由繼承的耦合方式和經(jīng)由參數(shù)的組合方式。下一部分內(nèi)容會(huì)繼續(xù)探討這一重要的分歧。
原文:http://article.yeeyan.org/view/213582/224721