策略模式
大多數(shù)問題都可以使用多種方法來解決。以排序問題為例,對(duì)于以一定次序把元素放入一個(gè)列表,排序算法有很多。通常來說,沒有公認(rèn)最適合所有場(chǎng)景的算法(請(qǐng)參考網(wǎng)頁[t.cn/RqrBZJQ])。一些不同的評(píng)判標(biāo)準(zhǔn)能幫助我們?yōu)椴煌膱?chǎng)景選擇不同的排序算法,其中應(yīng)該考慮的有以下幾個(gè)。
- 需要排序的元素?cái)?shù)量:這被稱為輸入大小。當(dāng)輸入較少時(shí),幾乎所有排序算法的表現(xiàn)都很好,但對(duì)于大量輸入,只有部分算法具有不錯(cuò)的性能。
- 算法的最佳/平均/最差時(shí)間復(fù)雜度:時(shí)間復(fù)雜度是算法運(yùn)行完成所花費(fèi)的(大致)時(shí)間長短,不考慮系數(shù)和低階項(xiàng)(在算法分析中,只考慮時(shí)間復(fù)雜度函數(shù)的最高次項(xiàng),不考慮低階項(xiàng),也忽略最高次項(xiàng)的系數(shù))。這是選擇算法的最常見標(biāo)準(zhǔn),但這個(gè)標(biāo)準(zhǔn)并不總是那么充分。
- 算法的空間復(fù)雜度:空間復(fù)雜度是充分地運(yùn)行一個(gè)算法所需要的(大致)物理內(nèi)存量。在我們處理大數(shù)據(jù)或在嵌入式系統(tǒng)(通常內(nèi)存有限)中工作時(shí),這個(gè)因素非常重要。
- 算法的穩(wěn)定性:在執(zhí)行一個(gè)排序算法之后,如果能保持相等值元素原來的先后相對(duì)次序,則認(rèn)為它是穩(wěn)定的。
- 算法的代碼實(shí)現(xiàn)復(fù)雜度:如果兩個(gè)算法具有相同的時(shí)間/空間復(fù)雜度,并且都是穩(wěn)定的,那么知道哪個(gè)算法更易于編碼實(shí)現(xiàn)和維護(hù)也是很重要的。
可能還有更多的評(píng)判標(biāo)準(zhǔn)值得考慮,但重要的是,我們真的只能使用單個(gè)排序算法來應(yīng)對(duì)所有情況嗎?答案當(dāng)然不是。一個(gè)更好的方案是把所有排序算法納為己用,然后使用上面提到的標(biāo)準(zhǔn)針對(duì)當(dāng)前情況選擇最好的算法。這就是策略模式的作用。
策略模式(Strategy pattern)鼓勵(lì)使用多種算法來解決一個(gè)問題,其殺手級(jí)特性是能夠在運(yùn)行時(shí)透明地切換算法(客戶端代碼對(duì)變化尤感知)。因此,如果你有兩種算法,并且知道其中一種對(duì)少量輸入效果更好,另一種對(duì)大量輸入效果更好,則可以使用策略模式在運(yùn)行時(shí)基于輸入數(shù)據(jù)決定使用哪種算法。
以下示例來自于Github:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
http://stackoverflow.com/questions/963965/how-is-this-strategy-pattern
-written-in-python-the-sample-in-wikipedia
In most of other languages Strategy pattern is implemented via creating some
base strategy interface/abstract class and subclassing it with a number of
concrete strategies (as we can see at
http://en.wikipedia.org/wiki/Strategy_pattern), however Python supports
higher-order functions and allows us to have only one class and inject
functions into it's instances, as shown in this example.
"""
import types
class StrategyExample:
def __init__(self, func=None):
self.name = 'Strategy Example 0'
if func is not None:
self.execute = types.MethodType(func, self)
def execute(self):
print(self.name)
def execute_replacement1(self):
print(self.name + ' from execute 1')
def execute_replacement2(self):
print(self.name + ' from execute 2')
if __name__ == '__main__':
strat0 = StrategyExample()
strat1 = StrategyExample(execute_replacement1)
strat1.name = 'Strategy Example 1'
strat2 = StrategyExample(execute_replacement2)
strat2.name = 'Strategy Example 2'
strat0.execute()
strat1.execute()
strat2.execute()
### OUTPUT ###
# Strategy Example 0
# Strategy Example 1 from execute 1
# Strategy Example 2 from execute 2
現(xiàn)實(shí)生活的例子
去機(jī)場(chǎng)趕飛機(jī)是現(xiàn)實(shí)中使用策略模式的一個(gè)恰當(dāng)例子。
- 如果想省錢,并且早點(diǎn)出發(fā),那么可以坐公交車/地鐵。
- 如果不介意支付停車費(fèi),并且有自己的汽車,那么可以開車去。
- 如果沒有自己的車,又比較急,則可以打車。
這是費(fèi)用、時(shí)間、便利性等因素之間的一個(gè)折中權(quán)衡。下圖展示了以多種方式(策略)去機(jī)場(chǎng)的一個(gè)例子,經(jīng)www.sourcemaking.com允許使用(請(qǐng)參考網(wǎng)頁[t.cn/RqrBAeJ])。
軟件的例子
Python的sorted()和list.sort()函數(shù)是策略模式的例子。兩個(gè)函數(shù)都接受一個(gè)命名參數(shù)key,這個(gè)參數(shù)本質(zhì)上是實(shí)現(xiàn)了一個(gè)排序策略的函數(shù)的名稱(請(qǐng)參考[Eckel08,第202頁])。
下面的例子(代碼在文件langs.py中)展示了如何用以下方式使用兩種不同的策略對(duì)編程語言進(jìn)行排序。
- 按字母順序
- 基于它們的流行度(使用TIOBE指數(shù),請(qǐng)參考網(wǎng)頁[t.cn/RGQ0jM7])
namedtuple編程語言(請(qǐng)參考網(wǎng)頁[t.cn/RqrBUrf])用于保存編程語言的統(tǒng)計(jì)數(shù)據(jù)。命名元組是一種易于創(chuàng)建、輕量、不可變的對(duì)象類型,與普通元組兼容,但也可以看作一個(gè)對(duì)象(可以使用常見的類表示法通過名稱調(diào)用)。命名元組可用于替代以下各項(xiàng)(請(qǐng)參考網(wǎng)頁[t.cn/RqrBGwP])。
- 在我們關(guān)注不可變特性時(shí),替代一個(gè)類。
- 在值得使用對(duì)象表示法來創(chuàng)建可讀性更高的代碼時(shí),替代一個(gè)元組。
順便說明一下pprint和attrgetter模塊。pprint模塊用于美化輸出一個(gè)數(shù)據(jù)結(jié)構(gòu),attrgetter用于通過屬性名訪問class或namedtuple的屬性。也可以使用一個(gè)lambda函數(shù)來替代使用attrgetter,但我覺得attrgetter的可讀性更高。
import pprint
from collections import namedtuple
from operator import attrgetter
if __name__ == '__main__':
ProgrammingLang = namedtuple('ProgrammingLang', 'name ranking')
stats = (('Ruby', 14), ('Javascript', 8), ('Python', 7),
('Scala', 31), ('Swift', 18), ('Lisp', 23))
lang_stats = [ProgrammingLang(n, r) for n, r in stats]
pp = pprint.PrettyPrinter(indent=5)
pp.pprint(sorted(lang_stats, key=attrgetter('name')))
print()
pp.pprint(sorted(lang_stats, key=attrgetter('ranking')))
[ ProgrammingLang(name='Javascript', ranking=8),
ProgrammingLang(name='Lisp', ranking=23),
ProgrammingLang(name='Python', ranking=7),
ProgrammingLang(name='Ruby', ranking=14),
ProgrammingLang(name='Scala', ranking=31),
ProgrammingLang(name='Swift', ranking=18)]
[ ProgrammingLang(name='Python', ranking=7),
ProgrammingLang(name='Javascript', ranking=8),
ProgrammingLang(name='Ruby', ranking=14),
ProgrammingLang(name='Swift', ranking=18),
ProgrammingLang(name='Lisp', ranking=23),
ProgrammingLang(name='Scala', ranking=31)]
Java API也使用了策略設(shè)計(jì)模式。java.util.Comparator是一個(gè)接口,包含一個(gè)compare()方法,該方法本質(zhì)上是一個(gè)策略,可傳給排序方法,比如Collections.sort和Arrays.sort(請(qǐng)參考網(wǎng)頁[t.cn/RqrB5o9])。
應(yīng)用案例
策略模式是一種非常通用的設(shè)計(jì)模式,可應(yīng)用的場(chǎng)景很多。一般來說,不論何時(shí)希望動(dòng)態(tài)、透明地應(yīng)用不同算法,策略模式都是可行之路。這里所說不同算法的意思是,結(jié)果相同但實(shí)現(xiàn)方案不同的一類算法。這意味著算法結(jié)果應(yīng)該是完全一致的,但每種實(shí)現(xiàn)都有不同的性能和代碼復(fù)雜性(舉例來說,對(duì)比一下順序查找和二分查找)。
我們已看到Python和Java如何使用策略模式來支持不同的排序算法。然而,策略模式并不限于排序問題,也可用于創(chuàng)建各種不同的資源過濾器(身份驗(yàn)證、H志記錄、數(shù)據(jù)壓縮和加密等),請(qǐng)參考網(wǎng)頁[t.cn/RqrBchI]。
策略模式的另一個(gè)應(yīng)用是創(chuàng)建不同的樣式表現(xiàn),為了實(shí)現(xiàn)可移植性(例如,不同平臺(tái)之間斷行的不同)或動(dòng)態(tài)地改變數(shù)據(jù)的表現(xiàn)。
另一個(gè)值得一提的應(yīng)用是模擬;例如模擬機(jī)器人,一些機(jī)器人比另一些更有攻擊性,一些機(jī)器人速度更快,等等。機(jī)器人行為中的所有不同之處都可以使用不同的策略來建模(請(qǐng)參考網(wǎng)頁[t.cn/RqrBf2q])。
實(shí)現(xiàn)
關(guān)于策略模式的實(shí)現(xiàn)沒有太多可說的。在函數(shù)非一等公民的語言中,每個(gè)策略都要用一個(gè)不同的類來實(shí)現(xiàn)。Wikipedia頁面中有UML圖展示了這一點(diǎn)(請(qǐng)參考網(wǎng)頁[t.cn/RqrBMhW])。在Python中,我們可以把函數(shù)看作是普通的變量,這就簡化了策略模式的實(shí)現(xiàn)。
假設(shè)我們要實(shí)現(xiàn)一個(gè)算法來檢測(cè)在一個(gè)字符串中是否所有字符都是唯一的。例如,如果輸入字符串dream,算法應(yīng)返回true,因?yàn)闆]有字符是重復(fù)的。如果輸入字符串pizza,算法應(yīng)返回false,因?yàn)樽帜竮出現(xiàn)了兩次。注意,重復(fù)字符不一定是連續(xù)的,并且字符串也不一定是一個(gè)合法單詞。對(duì)于字符串1r2a3ae,算法也應(yīng)該返回false,因?yàn)槠渲凶帜竌出現(xiàn)了兩次。
在仔細(xì)考慮問題之后,我們提出一種實(shí)現(xiàn):對(duì)字符串進(jìn)行排序并逐對(duì)比較所有字符。我們首先實(shí)現(xiàn)pairs()函數(shù),它會(huì)返回所有相鄰字符對(duì)的一個(gè)序列seq。
def pairs(seq):
n = len(seq)
for i in range(n):
yield seq[i], seq[(i + 1) % n]
接下來,實(shí)現(xiàn)allUniqueSort()函數(shù)。它接受一個(gè)字符串參數(shù)s,如果該字符串中所有字符都是唯一的,則返回True;否則,返回False。為演示策略模式,我們進(jìn)行一些簡化,假設(shè)這個(gè)算法的伸縮性不好,對(duì)于不超過5個(gè)字符的字符串才能工作良好。對(duì)于更長的字符串,通過捕入一條sleep語旬來模擬速度減緩。
SLOW = 3 # 單位為秒
LIMIT = 5 # 字符數(shù)
WARNING = 'too bad, you picked the slow algorithm :('
def allUniqueSort(s):
if len(s) > LIMIT:
print(WARNING)
time.sleep(SLOW)
srtStr = sorted(s)
for (c1, c2) in pairs(srtStr):
if c1 == c2:
return False
return True
我們對(duì)allUniqueSort()的性能并不滿意,所以嘗試考慮優(yōu)化的方式。一段時(shí)間之后,我們提出一個(gè)新算法allUniqueSet(),消除排序的需要。在這里,我們使用一個(gè)集合來實(shí)現(xiàn)算法。如果正在檢測(cè)的字符已經(jīng)被捕入到集合中,則意味著字符串中并非所有字符都是唯一的。
def allUniqueSet(s):
if len(s) < LIMIT:
print(WARNING)
time.sleep(SLOW)
return True if len(set(s)) == len(s) else False
不幸的是,allUniqueSet()雖然沒有伸縮性問題,但出于一些奇怪的原因,它檢測(cè)短字符串的性能比allUniqueSort()更差。這樣的話我們能做點(diǎn)什么呢?沒關(guān)系,我們可以保留兩個(gè)算法,并根據(jù)待檢測(cè)字符串的長度來選擇最合適的那個(gè)算法。函數(shù)allUnique()接受一個(gè)輸入字符串s和一個(gè)策略函數(shù)strategy,在這里是allUniqueSort()和allUniqueSet()中的一個(gè)。函數(shù)allUnique執(zhí)行輸入的策略,并向調(diào)用者返回結(jié)果。
使用main()函數(shù)可以執(zhí)行以下操作。
- 輸入待檢測(cè)字符唯一性的單詞
- 選擇要使用的策略
該函數(shù)還進(jìn)行了一些基本的錯(cuò)誤處理,并讓用戶能夠正常退出程序。
def main():
while True:
word = None
while not word:
word = input('Insert word (type quit to exit)> ')
if word == 'quit':
print('bye')
return
strategy_picked = None
strategies = { '1': allUniqueSet, '2': allUniqueSort }
while strategy_picked not in strategies.keys():
strategy_picked = input('Choose strategy: [1] Use a set, [2] Sort and pair> ')
try:
strategy = strategies[strategy_picked]
print('allUnique({}): {}'.format(word, allUnique(word, strategy)))
except KeyError as err:
print('Incorrect option: {}'.format(strategy_picked))
print()
下面是該示例的完整代碼(文件strategy.py)。
import time
SLOW = 3 # in seconds
LIMIT = 5 # in characters
WARNING = 'too bad, you picked the slow algorithm :('
def pairs(seq):
n = len(seq)
for i in range(n):
yield seq[i], seq[(i + 1) % n]
def allUniqueSort(s):
if len(s) > LIMIT:
print(WARNING)
time.sleep(SLOW)
srtStr = sorted(s)
for (c1, c2) in pairs(srtStr):
if c1 == c2:
return False
return True
def allUniqueSet(s):
if len(s) < LIMIT:
print(WARNING)
time.sleep(SLOW)
return True if len(set(s)) == len(s) else False
def allUnique(s, strategy):
return strategy(s)
def main():
while True:
word = None
while not word:
word = input('Insert word (type quit to exit)> ')
if word == 'quit':
print('bye')
return
strategy_picked = None
strategies = { '1': allUniqueSet, '2': allUniqueSort }
while strategy_picked not in strategies.keys():
strategy_picked = input('Choose strategy: [1] Use a set, [2] Sort and pair> ')
try:
strategy = strategies[strategy_picked]
print('allUnique({}): {}'.format(word, allUnique(word, strategy)))
except KeyError as err:
print('Incorrect option: {}'.format(strategy_picked))
print()
if __name__ == '__main__':
main()
第一個(gè)單詞(ballon)多于5個(gè)字符,并且不是所有字符都是唯一的。這種情況下,兩個(gè)算法都返回了正確結(jié)果(False),但allUniqueSort()更慢,用戶也收到了警告。
第二個(gè)單詞(bye)少于5個(gè)字符,并且所有字符都是唯一的。再一次,兩個(gè)算法都返回了期望的結(jié)果(True),但這一次,allUniqueSet()更慢,用戶也再一次收到警告。
最后一個(gè)單詞(h)是一個(gè)特殊案例。allUniqueSet()運(yùn)行慢,處理正確,返回期望的True;算法allUniqueSort()返回超快,但結(jié)果錯(cuò)誤。你能明臼為什么嗎?作為練習(xí),請(qǐng)修復(fù)allUniqueSort()算法。你也許想禁止處理單字符的單詞,我覺得這樣挺不錯(cuò)(相比返回一個(gè)錯(cuò)誤結(jié)果,這樣更好)。
通常,我們想要使用的策略不應(yīng)該由用戶來選擇。策略模式的要點(diǎn)是可以透明地使用不同的算法。修改一下代碼,使得程序始終選擇更快的算法。
我們的代碼有兩種常見用戶。一種是最終用戶,他們不應(yīng)該關(guān)心代碼中發(fā)生的事情。為達(dá)到這個(gè)效果,我們可以遵循前一段給出的提示來實(shí)現(xiàn)。另一類用戶是其他開發(fā)人員。假設(shè)我們想創(chuàng)建一個(gè)供其他開發(fā)人員使用的API。如何做到讓他們不用關(guān)心策略模式?一個(gè)提示是考慮在一個(gè)公用類(例如,AllUnique)中封裝兩個(gè)函數(shù)。這樣,其他開發(fā)人員只需要?jiǎng)?chuàng)建一個(gè)AllUnique類實(shí)例,并執(zhí)行單個(gè)方法,例如test()。在這個(gè)方法中需要做些什么呢?
小結(jié)
本章中,我們學(xué)習(xí)了策略設(shè)計(jì)模式。策略模式通常用在我們希望對(duì)同一個(gè)問題透明地使用多種方案時(shí)。如果并不存在針對(duì)所有輸入數(shù)據(jù)和所有情況的完美算法,那么我們可以使用策略模式,動(dòng)態(tài)地決定在每種情況下應(yīng)使用哪種算法。現(xiàn)實(shí)中,在我們想趕去機(jī)場(chǎng)乘飛機(jī)時(shí)會(huì)使用策略模式。
Python使用策略模式讓客戶端代碼決定如何對(duì)一個(gè)數(shù)據(jù)結(jié)構(gòu)中的元素進(jìn)行排序。我們看到了一個(gè)例子,基于TIOBE指數(shù)排行榜對(duì)編程語言進(jìn)行排序。
策略設(shè)計(jì)模式的使用并不限于排序領(lǐng)域。加密、壓縮、H志記錄及其他資源處理的領(lǐng)域都可以使用策略模式來提供不同的數(shù)據(jù)處理方式。可移植性是策略模式的另一個(gè)用武之地。模擬也是另一個(gè)策略模式適用的領(lǐng)域。
通過實(shí)現(xiàn)兩種不同算法來檢測(cè)一個(gè)單詞中所有字符的唯一性,我們學(xué)習(xí)了Python如何因其具有一等函數(shù)而簡化了策略模式的實(shí)現(xiàn)。
在本書的第16章中,我們將學(xué)習(xí)模板模式。該模式用于抽取一個(gè)算法的通用部分,從而提高代碼復(fù)用。