上一節(jié)課,我們主要介紹了策略模式的原理和實(shí)現(xiàn),以及如何利用策略模式來移除 if-else 或者 switch-case 分支判斷邏輯。今天,我們結(jié)合“給文件排序”這樣一個(gè)具體的例子,來詳細(xì)講一講策略模式的設(shè)計(jì)意圖和應(yīng)用場(chǎng)景。
除此之外,在今天的講解中,我還會(huì)通過一步一步地分析、重構(gòu),給你展示一個(gè)設(shè)計(jì)模式是如何“創(chuàng)造”出來的。通過今天的學(xué)習(xí),你會(huì)發(fā)現(xiàn),設(shè)計(jì)原則和思想其實(shí)比設(shè)計(jì)模式更加普適和重要,掌握了代碼的設(shè)計(jì)原則和思想,我們甚至可以自己創(chuàng)造出來新的設(shè)計(jì)模式。
話不多說,讓我們正式開始今天的學(xué)習(xí)吧!
問題與解決思路
假設(shè)有這樣一個(gè)需求,希望寫一個(gè)小程序,實(shí)現(xiàn)對(duì)一個(gè)文件進(jìn)行排序的功能。文件中只包含整型數(shù),并且,相鄰的數(shù)字通過逗號(hào)來區(qū)隔。如果由你來編寫這樣一個(gè)小程序,你會(huì)如何來實(shí)現(xiàn)呢?你可以把它當(dāng)作面試題,先自己思考一下,再來看我下面的講解。
你可能會(huì)說,這不是很簡(jiǎn)單嘛,只需要將文件中的內(nèi)容讀取出來,并且通過逗號(hào)分割成一個(gè)一個(gè)的數(shù)字,放到內(nèi)存數(shù)組中,然后編寫某種排序算法(比如快排),或者直接使用編程語言提供的排序函數(shù),對(duì)數(shù)組進(jìn)行排序,最后再將數(shù)組中的數(shù)據(jù)寫入文件就可以了。
但是,如果文件很大呢?比如有 10GB 大小,因?yàn)閮?nèi)存有限(比如只有 8GB 大小),我們沒辦法一次性加載文件中的所有數(shù)據(jù)到內(nèi)存中,這個(gè)時(shí)候,我們就要利用外部排序算法(具體怎么做,可以參看我的另一個(gè)專欄《數(shù)據(jù)結(jié)構(gòu)與算法之美》中的“排序”相關(guān)章節(jié))了。
如果文件更大,比如有 100GB 大小,我們?yōu)榱死?CPU 多核的優(yōu)勢(shì),可以在外部排序的基礎(chǔ)之上進(jìn)行優(yōu)化,加入多線程并發(fā)排序的功能,這就有點(diǎn)類似“單機(jī)版”的 MapReduce。
如果文件非常大,比如有 1TB 大小,即便是單機(jī)多線程排序,這也算很慢了。這個(gè)時(shí)候,我們可以使用真正的 MapReduce 框架,利用多機(jī)的處理能力,提高排序的效率。
代碼實(shí)現(xiàn)與分析
解決思路講完了,不難理解。接下來,我們看一下,如何將解決思路翻譯成代碼實(shí)現(xiàn)。
我先用最簡(jiǎn)單直接的方式實(shí)現(xiàn)將它實(shí)現(xiàn)出來。具體代碼我貼在下面了,你可以先看一下。因?yàn)槲覀兪窃谥v設(shè)計(jì)模式,不是講算法,所以,在下面的代碼實(shí)現(xiàn)中,我只給出了跟設(shè)計(jì)模式相關(guān)的骨架代碼,并沒有給出每種排序算法的具體代碼實(shí)現(xiàn)。感興趣的話,你可以自行實(shí)現(xiàn)一下。
public class Sorter {
private static final long GB = 1000 * 1000 * 1000;
public void sortFile(String filePath) {
// 省略校驗(yàn)邏輯
File file = new File(filePath);
long fileSize = file.length();
if (fileSize < 6 * GB) { // [0, 6GB)
quickSort(filePath);
} else if (fileSize < 10 * GB) { // [6GB, 10GB)
externalSort(filePath);
} else if (fileSize < 100 * GB) { // [10GB, 100GB)
concurrentExternalSort(filePath);
} else { // [100GB, ~)
mapreduceSort(filePath);
}
}
private void quickSort(String filePath) {
// 快速排序
}
private void externalSort(String filePath) {
// 外部排序
}
private void concurrentExternalSort(String filePath) {
// 多線程外部排序
}
private void mapreduceSort(String filePath) {
// 利用MapReduce多機(jī)排序
}
}
public class SortingTool {
public static void main(String[] args) {
Sorter sorter = new Sorter();
sorter.sortFile(args[0]);
}
}
在“編碼規(guī)范”那一部分我們講過,函數(shù)的行數(shù)不能過多,最好不要超過一屏的大小。所以,為了避免 sortFile() 函數(shù)過長,我們把每種排序算法從 sortFile() 函數(shù)中抽離出來,拆分成 4 個(gè)獨(dú)立的排序函數(shù)。
如果只是開發(fā)一個(gè)簡(jiǎn)單的工具,那上面的代碼實(shí)現(xiàn)就足夠了。畢竟,代碼不多,后續(xù)修改、擴(kuò)展的需求也不多,怎么寫都不會(huì)導(dǎo)致代碼不可維護(hù)。但是,如果我們是在開發(fā)一個(gè)大型項(xiàng)目,排序文件只是其中的一個(gè)功能模塊,那我們就要在代碼設(shè)計(jì)、代碼質(zhì)量上下點(diǎn)兒功夫了。只有每個(gè)小的功能模塊都寫好,整個(gè)項(xiàng)目的代碼才能不差。
在剛剛的代碼中,我們并沒有給出每種排序算法的代碼實(shí)現(xiàn)。實(shí)際上,如果自己實(shí)現(xiàn)一下的話,你會(huì)發(fā)現(xiàn),每種排序算法的實(shí)現(xiàn)邏輯都比較復(fù)雜,代碼行數(shù)都比較多。所有排序算法的代碼實(shí)現(xiàn)都堆在 Sorter 一個(gè)類中,這就會(huì)導(dǎo)致這個(gè)類的代碼很多。而在“編碼規(guī)范”那一部分中,我們也講到,一個(gè)類的代碼太多也會(huì)影響到可讀性、可維護(hù)性。除此之外,所有的排序算法都設(shè)計(jì)成 Sorter 的私有函數(shù),也會(huì)影響代碼的可復(fù)用性。
代碼優(yōu)化與重構(gòu)
只要掌握了我們之前講過的設(shè)計(jì)原則和思想,針對(duì)上面的問題,即便我們想不到該用什么設(shè)計(jì)模式來重構(gòu),也應(yīng)該能知道該如何解決,那就是將 Sorter 類中的某些代碼拆分出來,獨(dú)立成職責(zé)更加單一的小類。實(shí)際上,拆分是應(yīng)對(duì)類或者函數(shù)代碼過多、應(yīng)對(duì)代碼復(fù)雜性的一個(gè)常用手段。按照這個(gè)解決思路,我們對(duì)代碼進(jìn)行重構(gòu)。重構(gòu)之后的代碼如下所示:
public interface ISortAlg {
void sort(String filePath);
}
public class QuickSort implements ISortAlg {
@Override
public void sort(String filePath) {
//...
}
}
public class ExternalSort implements ISortAlg {
@Override
public void sort(String filePath) {
//...
}
}
public class ConcurrentExternalSort implements ISortAlg {
@Override
public void sort(String filePath) {
//...
}
}
public class MapReduceSort implements ISortAlg {
@Override
public void sort(String filePath) {
//...
}
}
public class Sorter {
private static final long GB = 1000 * 1000 * 1000;
public void sortFile(String filePath) {
// 省略校驗(yàn)邏輯
File file = new File(filePath);
long fileSize = file.length();
ISortAlg sortAlg;
if (fileSize < 6 * GB) { // [0, 6GB)
sortAlg = new QuickSort();
} else if (fileSize < 10 * GB) { // [6GB, 10GB)
sortAlg = new ExternalSort();
} else if (fileSize < 100 * GB) { // [10GB, 100GB)
sortAlg = new ConcurrentExternalSort();
} else { // [100GB, ~)
sortAlg = new MapReduceSort();
}
sortAlg.sort(filePath);
}
}
經(jīng)過拆分之后,每個(gè)類的代碼都不會(huì)太多,每個(gè)類的邏輯都不會(huì)太復(fù)雜,代碼的可讀性、可維護(hù)性提高了。除此之外,我們將排序算法設(shè)計(jì)成獨(dú)立的類,跟具體的業(yè)務(wù)邏輯(代碼中的 if-else 那部分邏輯)解耦,也讓排序算法能夠復(fù)用。這一步實(shí)際上就是策略模式的第一步,也就是將策略的定義分離出來。
實(shí)際上,上面的代碼還可以繼續(xù)優(yōu)化。每種排序類都是無狀態(tài)的,我們沒必要在每次使用的時(shí)候,都重新創(chuàng)建一個(gè)新的對(duì)象。所以,我們可以使用工廠模式對(duì)對(duì)象的創(chuàng)建進(jìn)行封裝。按照這個(gè)思路,我們對(duì)代碼進(jìn)行重構(gòu)。重構(gòu)之后的代碼如下所示:
public class SortAlgFactory {
private static final Map<String, ISortAlg> algs = new HashMap<>();
static {
algs.put("QuickSort", new QuickSort());
algs.put("ExternalSort", new ExternalSort());
algs.put("ConcurrentExternalSort", new ConcurrentExternalSort());
algs.put("MapReduceSort", new MapReduceSort());
}
public static ISortAlg getSortAlg(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}
return algs.get(type);
}
}
public class Sorter {
private static final long GB = 1000 * 1000 * 1000;
public void sortFile(String filePath) {
// 省略校驗(yàn)邏輯
File file = new File(filePath);
long fileSize = file.length();
ISortAlg sortAlg;
if (fileSize < 6 * GB) { // [0, 6GB)
sortAlg = SortAlgFactory.getSortAlg("QuickSort");
} else if (fileSize < 10 * GB) { // [6GB, 10GB)
sortAlg = SortAlgFactory.getSortAlg("ExternalSort");
} else if (fileSize < 100 * GB) { // [10GB, 100GB)
sortAlg = SortAlgFactory.getSortAlg("ConcurrentExternalSort");
} else { // [100GB, ~)
sortAlg = SortAlgFactory.getSortAlg("MapReduceSort");
}
sortAlg.sort(filePath);
}
}
經(jīng)過上面兩次重構(gòu)之后,現(xiàn)在的代碼實(shí)際上已經(jīng)符合策略模式的代碼結(jié)構(gòu)了。我們通過策略模式將策略的定義、創(chuàng)建、使用解耦,讓每一部分都不至于太復(fù)雜。不過,Sorter 類中的 sortFile() 函數(shù)還是有一堆 if-else 邏輯。這里的 if-else 邏輯分支不多、也不復(fù)雜,這樣寫完全沒問題。但如果你特別想將 if-else 分支判斷移除掉,那也是有辦法的。我直接給出代碼,你一看就能明白。實(shí)際上,這也是基于查表法來解決的,其中的“algs”就是“表”。
public class Sorter {
private static final long GB = 1000 * 1000 * 1000;
private static final List<AlgRange> algs = new ArrayList<>();
static {
algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort")));
algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort")));
algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort")));
algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort")));
}
public void sortFile(String filePath) {
// 省略校驗(yàn)邏輯
File file = new File(filePath);
long fileSize = file.length();
ISortAlg sortAlg = null;
for (AlgRange algRange : algs) {
if (algRange.inRange(fileSize)) {
sortAlg = algRange.getAlg();
break;
}
}
sortAlg.sort(filePath);
}
private static class AlgRange {
private long start;
private long end;
private ISortAlg alg;
public AlgRange(long start, long end, ISortAlg alg) {
this.start = start;
this.end = end;
this.alg = alg;
}
public ISortAlg getAlg() {
return alg;
}
public boolean inRange(long size) {
return size >= start && size < end;
}
}
}
現(xiàn)在的代碼實(shí)現(xiàn)就更加優(yōu)美了。我們把可變的部分隔離到了策略工廠類和 Sorter 類中的靜態(tài)代碼段中。當(dāng)要添加一個(gè)新的排序算法時(shí),我們只需要修改策略工廠類和 Sort 類中的靜態(tài)代碼段,其他代碼都不需要修改,這樣就將代碼改動(dòng)最小化、集中化了。
你可能會(huì)說,即便這樣,當(dāng)我們添加新的排序算法的時(shí)候,還是需要修改代碼,并不完全符合開閉原則。有什么辦法讓我們完全滿足開閉原則呢?
對(duì)于 Java 語言來說,我們可以通過反射來避免對(duì)策略工廠類的修改。具體是這么做的:我們通過一個(gè)配置文件或者自定義的 annotation 來標(biāo)注都有哪些策略類;策略工廠類讀取配置文件或者搜索被 annotation 標(biāo)注的策略類,然后通過反射了動(dòng)態(tài)地加載這些策略類、創(chuàng)建策略對(duì)象;當(dāng)我們新添加一個(gè)策略的時(shí)候,只需要將這個(gè)新添加的策略類添加到配置文件或者用 annotation 標(biāo)注即可。還記得上一節(jié)課的課堂討論題嗎?我們也可以用這種方法來解決。
對(duì)于 Sorter 來說,我們可以通過同樣的方法來避免修改。我們通過將文件大小區(qū)間和算法之間的對(duì)應(yīng)關(guān)系放到配置文件中。當(dāng)添加新的排序算法時(shí),我們只需要改動(dòng)配置文件即可,不需要改動(dòng)代碼。
重點(diǎn)回顧
好了,今天的內(nèi)容到此就講完了。我們一塊來總結(jié)回顧一下,你需要重點(diǎn)掌握的內(nèi)容。
一提到 if-else 分支判斷,有人就覺得它是爛代碼。如果 if-else 分支判斷不復(fù)雜、代碼不多,這并沒有任何問題,畢竟 if-else 分支判斷幾乎是所有編程語言都會(huì)提供的語法,存在即有理由。遵循 KISS 原則,怎么簡(jiǎn)單怎么來,就是最好的設(shè)計(jì)。非得用策略模式,搞出 n 多類,反倒是一種過度設(shè)計(jì)。
一提到策略模式,有人就覺得,它的作用是避免 if-else 分支判斷邏輯。實(shí)際上,這種認(rèn)識(shí)是很片面的。策略模式主要的作用還是解耦策略的定義、創(chuàng)建和使用,控制代碼的復(fù)雜度,讓每個(gè)部分都不至于過于復(fù)雜、代碼量過多。除此之外,對(duì)于復(fù)雜代碼來說,策略模式還能讓其滿足開閉原則,添加新策略的時(shí)候,最小化、集中化代碼改動(dòng),減少引入 bug 的風(fēng)險(xiǎn)。文章來源:http://www.zghlxwxcb.cn/news/detail-497133.html
實(shí)際上,設(shè)計(jì)原則和思想比設(shè)計(jì)模式更加普適和重要。掌握了代碼的設(shè)計(jì)原則和思想,我們能更清楚的了解,為什么要用某種設(shè)計(jì)模式,就能更恰到好處地應(yīng)用設(shè)計(jì)模式。文章來源地址http://www.zghlxwxcb.cn/news/detail-497133.html
課堂討論
- 在過去的項(xiàng)目開發(fā)中,你有沒有用過策略模式,都是為了解決什么問題才使用的?
- 你可以說一說,在什么情況下,我們才有必要去掉代碼中的 if-else 或者 switch-case 分支邏輯呢?
到了這里,關(guān)于【設(shè)計(jì)模式與范式:行為型】61 | 策略模式(下):如何實(shí)現(xiàn)一個(gè)支持給不同大小文件排序的小程序?的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!