解讀器模式旨在定義特定語言的文法並建構其解讀器,本文以智慧家庭控制場景為例,利用 pyparsing 函式庫解析自定義的 DSL,實作語法分析和裝置控制邏輯。策略模式則關注於演算法的彈性切換,本文以字串唯一性檢查為例,示範如何根據字串長度,動態選擇效率更高的演算法,例如短字串使用排序法,長字串則使用集合法,避免不必要的效能損耗。兩種模式都提升了程式碼的可讀性、可維護性和擴充套件性,適用於處理不同情境下的演算法選擇和領域特定語言的解析。

解讀器模式:開發智慧家庭控制的內部DSL

解讀器模式(Interpreter pattern)是一種行為設計模式,主要用於定義一種語言的文法表示,並提供一個解讀器來解釋該語言中的句子。在本例中,我們將利用解讀器模式建立一個控制智慧家庭的內部領域特定語言(DSL),使其更加直觀易用。

為何選擇解讀器模式

我們的目標是為非程式設計師的專家提供適當的程式抽象,使他們能夠高效工作。理想情況下,他們不需瞭解進階的Python程式設計知識就能使用我們的DSL,但具備一定的Python基礎將會有所助益。進階的Python概念並非必要條件。此外,DSL的效能通常並非主要關注點。我們的重點是提供一種語言,能夠隱藏宿主語言的特殊性,並提供更易讀的語法。Python本身已經是一種可讀性很高的語言,但透過DSL,我們可以進一步簡化語法,使其更貼近自然語言。

實作解讀器模式

讓我們來建立一個控制智慧家庭的內部DSL。使用者可以使用簡單的事件表示法來控制家中裝置,事件格式為命令 -> 接收者 -> 引數,其中引數部分是可選的。

事件語法定義

並非所有事件都需要引數。例如,不需要引數的事件如下:

open -> gate

需要引數的事件如下:

increase -> boiler temperature -> 3 degrees

使用->符號來標記事件各部分之間的分隔。

語法定義與解析

在開始編碼之前,先定義DSL的簡單語法。我們使用巴科斯-瑙爾正規化(BNF)表示法來定義語法:

event ::= command token receiver token arguments
command ::= word+
word ::= 一個或多個英數字元的集合
token ::= ->
receiver ::= word+
arguments ::= word+

這套語法告訴我們,事件具有命令 -> 接收者 -> 引數的形式,並且命令、接收者和引數都具有相同的形式:一個或多個英數字元的群組。

將語法轉換為程式碼

使用pyparsing函式庫來實作語法解析。首先,定義語法的各個組成部分:

from pyparsing import Word, alphanums, Group, Suppress, OneOrMore, Optional

word = Word(alphanums)
command = Group(OneOrMore(word))
token = Suppress("->")
device = Group(OneOrMore(word))
argument = Group(OneOrMore(word))
event = command + token + device + Optional(token + argument)

Boiler類別實作範例

以Boiler類別為例,展示如何實作具體的裝置控制邏輯:

class Boiler:
    def __init__(self):
        self.temperature = 83  # 攝氏度

    def __str__(self):
        return f"boiler temperature: {self.temperature}"

    def increase_temperature(self, amount):
        print(f"increasing the boiler's temperature by {amount} degrees")
        self.temperature += amount

    def decrease_temperature(self, amount):
        print(f"decreasing the boiler's temperature by {amount} degrees")
        self.temperature -= amount

事件解析與執行

使用parseString()方法來解析事件字串,並根據解析結果執行對應的操作:

boiler = Boiler()
test = "increase -> boiler temperature -> 3 degrees"
cmd, dev, arg = event.parseString(test)
cmd_str = " ".join(cmd)
dev_str = " ".join(dev)

if "increase" in cmd_str and "boiler" in dev_str:
    boiler.increase_temperature(int(arg[0]))
print(boiler)

執行結果:

increasing the boiler's temperature by 3 degrees
boiler temperature: 86

完整實作與擴充套件

完整的實作(見ch05/interpreter/interpreter.py檔案)支援更多事件和裝置。實作步驟如下:

  1. 匯入pyparsing所需模組。
  2. 定義相關類別,如Gate、Aircondition、Heating、Boiler和Fridge。

策略模式(The Strategy Pattern)

在軟體開發中,經常會遇到同一問題有多種解決方案的情況。以排序演算法為例,不同的演算法在不同的場景下會有不同的表現。選擇合適的演算法取決於多種因素,如輸入資料的大小、時間複雜度、空間複雜度、演算法的穩定性以及程式碼的複雜度等。

策略模式的定義

策略模式是一種允許在執行時動態選擇演算法的設計模式。它使得客戶端程式碼無需瞭解演算法的具體實作細節,就能切換不同的演算法。這種模式特別適合於需要根據不同情況選擇不同演算法的場景。

實務範例

一個典型的實務範例是到達機場的方式選擇:

  • 如果想要節省錢且有足夠的時間,可以選擇公車或火車。
  • 如果不介意停車費且有自己的車,可以開車前往。
  • 如果沒有車但時間緊迫,可以選擇計程車。

這些選擇都涉及到成本、時間和便利性的權衡。

在軟體開發中,Python 的 sorted()list.sort() 函式是策略模式的典型應用。它們接受一個名為 key 的引數,這個引數是一個函式,用於實作特定的排序策略。

策略模式的應用場景

策略模式適用於需要動態且透明地應用不同演算法的場景。這裡的不同演算法指的是同一問題的不同實作,它們應該產生相同的結果,但可能在效能和程式碼複雜度上有所不同。除了排序演算法外,策略模式還可以用於建立不同的格式化表示,以實作可移植性或動態改變資料的表示方式。

策略模式的實作

在 Python 中,由於函式是一級公民,可以直接將函式作為變數參照和操作,因此策略模式的實作相對簡單。不需要在不同的類別中實作不同的策略,直接使用函式即可實作策略模式。

def bubble_sort(data):
    # 泡沫排序實作
    n = len(data)
    for i in range(n):
        for j in range(0, n-i-1):
            if data[j] > data[j+1]:
                data[j], data[j+1] = data[j+1], data[j]
    return data

def quick_sort(data):
    # 快速排序實作
    if len(data) <= 1:
        return data
    pivot = data[len(data) // 2]
    left = [x for x in data if x < pivot]
    middle = [x for x in data if x == pivot]
    right = [x for x in data if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)

def sort_data(data, strategy):
    # 使用指定的排序策略對資料進行排序
    return strategy(data)

# 使用泡沫排序
data = [64, 34, 25, 12, 22, 11, 90]
sorted_data = sort_data(data, bubble_sort)
print("泡沫排序結果:", sorted_data)

# 使用快速排序
data = [64, 34, 25, 12, 22, 11, 90]
sorted_data = sort_data(data, quick_sort)
print("快速排序結果:", sorted_data)

內容解密:

此範例展示瞭如何使用策略模式來選擇不同的排序演算法。bubble_sortquick_sort 分別實作了兩種不同的排序演算法。sort_data 函式接受一個資料列表和一個排序策略函式,並使用指定的策略對資料進行排序。這使得客戶端程式碼可以在執行時動態選擇使用的排序演算法。

  graph LR
    A[客戶端] -->|選擇策略|> B[策略介面]
    B --> C[具體策略A: 泡沫排序]
    B --> D[具體策略B: 快速排序]
    C --> E[執行排序]
    D --> E
    E --> F[傳回排序結果]

圖表翻譯: 此圖表呈現了策略模式的基本結構。客戶端根據需求選擇不同的排序策略(具體策略A或具體策略B),然後執行排序並傳回結果。圖中展示了策略模式如何使得客戶端程式碼與具體的演算法實作解耦,從而實作執行時動態選擇演算法的功能。

策略模式(Strategy Pattern)在字串唯一性檢查中的應用

策略模式是一種行為設計模式,允許在執行時根據不同情況選擇不同的演算法或策略來解決問題。本文將透過一個檢查字串中所有字元是否唯一的例項,探討策略模式的應用。

問題描述

假設我們需要實作一個演算法來檢查給定字串中的所有字元是否唯一。例如,對於字串 “dream”,演算法應傳回 True,因為沒有重複的字元;而對於字串 “pizza”,演算法應傳回 False,因為字元 “z” 出現了兩次。

初始實作:排序法

首先,我們實作了一個根據排序的演算法 allUniqueSort()。該演算法先對輸入字串進行排序,然後比較相鄰字元是否相同。如果發現任何一對相鄰字元相同,則傳回 False,否則傳回 True。為了演示策略模式,我們假設這個演算法在處理較長字串時效能不佳,因此在處理長度超過 5 的字串時加入了延遲模擬效能問題。

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

內容解密:

  1. pairs(seq) 函式用於生成序列 seq 中所有相鄰元素對。這裡使用生成器來提高效率。
  2. allUniqueSort(s) 函式首先檢查輸入字串 s 的長度是否超過限制 LIMIT。如果是,則列印警告訊息並延遲 SLOW 秒以模擬效能問題。
  3. 對輸入字串 s 進行排序,並使用 pairs() 函式比較相鄰字元。如果發現任何一對相同的字元,則立即傳回 False

改進實作:集合法

後來,我們提出了一個新的演算法 allUniqueSet(),該演算法使用集合(set)來檢查字元唯一性。該方法避免了排序步驟,直接將字串轉換為集合,然後比較集合大小與原始字串長度是否相等。如果相等,則所有字元唯一,傳回 True;否則,傳回 False

def allUniqueSet(s):
    """檢查字串中所有字元是否唯一(集合法)"""
    if len(s) < LIMIT:
        print(WARNING)
        time.sleep(SLOW)
    return len(set(s)) == len(s)

內容解密:

  1. allUniqueSet(s) 函式檢查輸入字串 s 的長度是否小於限制 LIMIT。如果是,則列印警告訊息並延遲 SLOW 秒以模擬效能問題。
  2. 將輸入字串 s 轉換為集合,並比較集合大小與原始字串長度。如果兩者相等,則傳回 True,表示所有字元唯一;否則,傳回 False

策略模式的應用

為了能夠根據輸入字串的長度動態選擇合適的演算法,我們引入了策略模式。首先,定義了一個通用函式 allUnique(),該函式接受輸入字串 s 和一個策略函式 strategy,並執行該策略函式。

def allUnique(s, strategy):
    """根據給定的策略檢查字串中所有字元是否唯一"""
    return strategy(s)

接著,在 main() 函式中,我們讓使用者選擇要使用的演算法(策略),並根據使用者的選擇呼叫相應的策略函式。

def main():
    """主函式:讓使用者互動選擇演算法並檢查字串唯一性"""
    WORD_IN_DESC = "Insert word (type quit to exit)> "
    STRAT_IN_DESC = "Choose strategy: [1] Use a set, [2] Sort and pair> "
    
    strategies = {"1": allUniqueSet, "2": allUniqueSort}
    
    while True:
        word = input(WORD_IN_DESC)
        if word == "quit":
            print("bye")
            break
        
        strategy_picked = input(STRAT_IN_DESC)
        try:
            strategy = strategies[strategy_picked]
            result = allUnique(word, strategy)
            print(f"allUnique({word}): {result}")
        except KeyError:
            print(f"Incorrect option: {strategy_picked}")

內容解密:

  1. 使用者可以輸入要檢查的字串或輸入 “quit” 離開程式。
  2. 使用者被提示選擇要使用的演算法(策略):使用集合法或排序法。
  3. 根據使用者的選擇,呼叫相應的策略函式來檢查輸入字串的唯一性,並列印結果。

自動選擇最佳策略

在實際應用中,我們希望能夠自動選擇最合適的演算法,而不是由使用者手動選擇。為此,可以修改程式碼,使其根據輸入字串的長度自動選擇最快的演算法。

def main():
    """主函式:自動選擇最佳策略檢查字串唯一性"""
    WORD_IN_DESC = "Insert word (type quit to exit)> "
    
    while True:
        word = input(WORD_IN_DESC)
        if word == "quit":
            print("bye")
            break
        
        # 自動根據字串長度選擇最佳策略
        strategy = allUniqueSet if len(word) > LIMIT else allUniqueSort
        result = allUnique(word, strategy)
        print(f"allUnique({word}): {result}")