1. 程式人生 > 實用技巧 >任何階段都不能缺少的spring cloud微服務實戰,快來解析一波

任何階段都不能缺少的spring cloud微服務實戰,快來解析一波

python設計模式之策略模式

大多數問題都可以使用多種方法來解決。以排序問題為例,對於以一定次序把元素放入一個列表,排序演算法有很多。通常來說,沒有公認最適合所有場景的演算法一些不同的評判標準能幫助我們為不同的場景選擇不同的排序演算法,其中應該考慮的有以下幾個。

  • [ ] 需要排序的元素數量:這被稱為輸入大小。當輸入較少時,幾乎所有排序演算法的表現都很好,但對於大量輸入,只有部分演算法具有不錯的效能。
  • [ ] 演算法的最佳/平均/最差時間複雜度:時間複雜度是演算法執行完成所花費的(大致)時間長短,不考慮係數和低階項①。這是選擇演算法的最常見標準,但這個標準並不總是那麼充分。
  • [ ] 演算法的空間複雜度:空間複雜度是充分地執行一個演算法所需要的(大致)實體記憶體量。在我們處理大資料或在嵌入式系統(通常記憶體有限)中工作時,這個因素非常重要。
  • [ ] 演算法的穩定性:在執行一個排序演算法之後,如果能保持相等值元素原來的先後相對次序,則認為它是穩定的。
  • [ ] 演算法的程式碼實現複雜度:如果兩個演算法具有相同的時間/空間複雜度,並且都是穩定的,那麼知道哪個演算法更易於編碼實現和維護也是很重要的。

可能還有更多的評判標準值得考慮,但重要的是,我們真的只能使用單個排序演算法來應對所有情況嗎?答案當然不是。一個更好的方案是把所有排序演算法納為己用,然後使用上面提到的標準針對當前情況選擇最好的演算法。這就是策略模式的目的。

策略模式( Strategy pattern)鼓勵使用多種演算法來解決一個問題,其殺手級特性是能夠在執行時透明地切換演算法(客戶端程式碼對變化無感知)。因此,如果你有兩種演算法,並且知道其中一種對少量輸入效果更好,另一種對大量輸入效果更好,則可以使用策略模式在執行時基於輸入資料決定使用哪種演算法。

1. 現實生活的例子

去機場趕飛機是現實中使用策略模式的一個恰當例子。

  • [ ] 如果想省錢,並且早點出發,那麼可以坐公交車/地鐵。
  • [ ] 如果不介意支付停車費,並且有自己的汽車,那麼可以開車去。
  • [ ] 如果沒有自己的車,又比較急,則可以打車。

這是費用、時間、便利性等因素之間的一個折中權衡。

2. 軟體的例子

Python的sorted()和list.sort()函式是策略模式的例子。兩個函式都接受一個命名引數key,這個引數本質上是實現了一個排序策略的函式的名稱。

下面的例子展示瞭如何用以下方式使用兩種不同的策略對程式語言進行排序。

  • [ ] 按字母順序
  • [ ] 基於它們的流行度

namedtuple程式語言用於儲存程式語言的統計資料。命名元組是一種易於建立、輕量、不可變的物件型別,與普通元組相容,但也可以看作一個物件(可以使用常見的類表示法通過名稱呼叫)。

  • [ ] 在我們關注不可變特性時,替代一個類
  • [ ] 在值得使用物件表示法來建立可讀性更高的程式碼時,替代一個元組

順便說明一下pprint和attrgetter模組。 pprint模組用於美化輸出一個數據結構,attrgetter用於通過屬性名訪問class或namedtuple的屬性。也可以使用一個lambda函式來替代使用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也使用了策略設計模式。 java.util.Comparator是一個介面, 包含一個compare()方法,該方法本質上是一個策略,可傳給排序方法,比如Collections.sort和Arrays.sort。

3. 應用案例

策略模式是一種非常通用的設計模式,可應用的場景很多。一般來說,不論何時希望動態、透明地應用不同演算法,策略模式都是可行之路。這裡所說不同演算法的意思是,目的相同但實現方案不同的一類演算法。這意味著演算法結果應該是完全一致的,但每種實現都有不同的效能和程式碼複雜性(舉例來說,對比一下順序查詢和二分查詢)。

我們已看到Python和Java如何使用策略模式來支援不同的排序演算法。然而,策略模式並不限於排序問題,也可用於建立各種不同的資源過濾器(身份驗證、日誌記錄、資料壓縮和加密等)。

策略模式的另一個應用是建立不同的樣式表現,為了實現可移植性(例如,不同平臺之間斷行的不同)或動態地改變資料的表現。

另一個值得一提的應用是模擬;例如模擬機器人,一些機器人比另一些更有攻擊性,一些機器人速度更快,等等。機器人行為中的所有不同之處都可以使用不同的策略來建模。

4. 實現

關於策略模式的實現沒有太多可說的。在函式非一等公民的語言中,每個策略都要用一個不同的類來實現。在Python中,我們可以把函式看作是普通的變數,這就簡化了策略模式的實現。

假設我們要實現一個演算法來檢測在一個字串中是否所有字元都是唯一的。例如,如果輸入字串dream,演算法應返回true,因為沒有字元是重複的。如果輸入字串pizza,演算法應返回false,因為字母z出現了兩次。注意,重複字元不一定是連續的,並且字串也不一定是一個合法單詞。對於字串1r2a3ae,演算法也應該返回false,因為其中字母a出現了兩次。

在仔細考慮問題之後,我們提出一種實現:對字串進行排序並逐對比較所有字元。我們首先實現pairs()函式,它會返回所有相鄰字元對的一個序列seq。

def pairs(seq):
    n = len(seq)
    for i in range(n):
    	yield seq[i], seq[(i + 1) % n]	

接下來,實現allUniqueSort()函式。它接受一個字串引數s,如果該字串中所有字元都是唯一的,則返回True;否則,返回False。為演示策略模式,我們進行一些簡化,假設這個演算法的伸縮性不好,對於不超過5個字元的字串才能工作良好。對於更長的字串,通過插入一條sleep語句來模擬速度減緩。

SLOW = 3 # 單位為秒
LIMIT = 5 # 字元數
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

我們對allUniqueSort()的效能並不滿意,所以嘗試考慮優化的方式。一段時間之後,我們提出一個新演算法allUniqueSet(),消除排序的需要。在這裡,我們使用一個集合來實現演算法。如果正在檢測的字元已經被插入到集合中,則意味著字串中並非所有字元都是唯一的。

def allUniqueSet(s):
    if len(s) < LIMIT:
        print(WARNING)
        time.sleep(SLOW)
    return True if len(set(s)) == len(s) else False

不幸的是, allUniqueSet()雖然沒有伸縮性問題,但出於一些奇怪的原因,它檢測短字串的效能比allUniqueSort()更差。這樣的話我們能做點什麼呢?沒關係,我們可以保留兩個演算法,並根據待檢測字串的長度來選擇最合適的那個演算法。函式allUnique()接受一個輸入字串s和一個策略函式strategy,在這裡是allUniqueSort()和allUniqueSet()中的一個。函式allUnique執行輸入的策略,並向呼叫者返回結果。

使用main()函式可以執行以下操作。

  • [ ] 輸入待檢測字元唯一性的單詞
  • [ ] 選擇要使用的策略

該函式還進行了一些基本的錯誤處理,並讓使用者能夠正常退出程式。

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))

下面是該示例的完整程式碼:

import time
SLOW = 3 # 單位為秒
LIMIT = 5 # 字元數
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()

輸出:

Insert word (type quit to exit)> balloon
Choose strategy: [1] Use a set, [2] Sort and pair> 1
allUnique(balloon): False
Insert word (type quit to exit)> balloon
Choose strategy: [1] Use a set, [2] Sort and pair> 2
too bad, you picked the slow algorithm :(
allUnique(balloon): False
Insert word (type quit to exit)> bye
Choose strategy: [1] Use a set, [2] Sort and pair> 1
too bad, you picked the slow algorithm :(
allUnique(bye): True
Insert word (type quit to exit)> bye
Choose strategy: [1] Use a set, [2] Sort and pair> 2
allUnique(bye): True
Insert word (type quit to exit)> h
Choose strategy: [1] Use a set, [2] Sort and pair> 1
too bad, you picked the slow algorithm :(
allUnique(h): True
Insert word (type quit to exit)> h
Choose strategy: [1] Use a set, [2] Sort and pair> 2
allUnique(h): False
Insert word (type quit to exit)> quit
bye

第一個單詞( ballon)多於5個字元,並且不是所有字元都是唯一的。這種情況下,兩個演算法都返回了正確結果( False),但allUniqueSort()更慢,使用者也收到了警告。

第二個單詞( bye)少於5個字元,並且所有字元都是唯一的。再一次,兩個演算法都返回了期望的結果( True),但這一次, allUniqueSet()更慢,使用者也再一次收到警告。

最後一個單詞( h)是一個特殊案例。 allUniqueSet()執行慢,處理正確,返回期望的True;演算法allUniqueSort()返回超快,但結果錯誤。你能明白為什麼嗎?作為練習,請修復allUniqueSort()演算法。你也許想禁止處理單字元的單詞,我覺得這樣挺不錯(相比返回一個錯誤結果,這樣更好)。

通常,我們想要使用的策略不應該由使用者來選擇。策略模式的要點是可以透明地使用不同的演算法。修改一下程式碼,使得程式始終選擇更快的演算法。

我們的程式碼有兩種常見使用者。一種是終端使用者,他們不應該關心程式碼中發生的事情。為達到這個效果,我們可以遵循前一段給出的提示來實現。另一類使用者是其他開發人員。假設我們想建立一個供其他開發人員使用的API。如何做到讓他們不用關心策略模式?一個提示是考慮在一個公用類(例如, AllUnique)中封裝兩個函式。這樣,其他開發人員只需要建立一個AllUnique類例項,並執行單個方法,例如test()。

5. 實現

策略模式通常用在我們希望對同一個問題透明地使用多種方案時。如果並不存在針對所有輸入資料和所有情況的完美演算法,那麼我們可以使用策略模式,動態地決定在每種情況下應使用哪種演算法。現實中,在我們想趕去機場乘飛機時會使用策略模式。

Python使用策略模式讓客戶端程式碼決定如何對一個數據結構中的元素進行排序。我們看到了一個例子,基於TIOBE指數排行榜對程式語言進行排序。

策略設計模式的使用並不限於排序領域。加密、壓縮、日誌記錄及其他資源處理的領域都可以使用策略模式來提供不同的資料處理方式。可移植性是策略模式的另一個用武之地。模擬也是另一個策略模式適用的領域。