解釋器模式
對(duì)每個(gè)應(yīng)用來(lái)說(shuō),至少有以下兩種不同的用戶(hù)分類(lèi)。
- 基本用戶(hù):這類(lèi)用戶(hù)只希望能夠憑直覺(jué)使用應(yīng)用。他們不喜歡花太多時(shí)間配置或?qū)W習(xí)應(yīng)用的內(nèi)部。對(duì)他們來(lái)說(shuō),基本的用法就足夠了。
- 高級(jí)用戶(hù):這些用戶(hù),實(shí)際上通常是少數(shù),不介意花費(fèi)額外的時(shí)間學(xué)習(xí)如何使用應(yīng)用的高級(jí)特性。如果知道學(xué)會(huì)之后能得到以下好處,他們甚至?xí)W(xué)習(xí)一種配置(或腳本)語(yǔ)言。
· 能夠更好地控制一個(gè)應(yīng)用
· 以更好的方式表達(dá)想法
· 提高生產(chǎn)力
解釋器(Interpreter)模式僅能引起應(yīng)用的高級(jí)用戶(hù)的興趣。這是因?yàn)榻忉屍髂J奖澈蟮闹饕枷胧亲尫浅跫?jí)用戶(hù)和領(lǐng)域?qū)<沂褂靡婚T(mén)簡(jiǎn)單的語(yǔ)言來(lái)表達(dá)想法。然而,什么是一種簡(jiǎn)單的語(yǔ)言?對(duì)于我們的需求來(lái)說(shuō),一種簡(jiǎn)單的語(yǔ)言就是沒(méi)有像編程語(yǔ)言那么復(fù)雜的語(yǔ)言。
一般而言,我們想要?jiǎng)?chuàng)建的是一種領(lǐng)域特定語(yǔ)言(Domain Specific Language,DSL)。DSL是一種針對(duì)一個(gè)特定領(lǐng)域的有限表達(dá)能力的計(jì)算機(jī)語(yǔ)言。很多不同的事情都使用DSL,比如,戰(zhàn)斗模擬、記賬、可視化、配置、通信協(xié)議等。DSL分為內(nèi)部DSL和外部DSL(請(qǐng)參考網(wǎng)頁(yè)[t.cn/zHtEh5t]和網(wǎng)頁(yè)[t.cn/hBfQ2Y])。
內(nèi)部DSL構(gòu)建在一種宿主編程語(yǔ)言之上。內(nèi)部DSL的一個(gè)例子是,使用Python解決線(xiàn)性方程組的一種語(yǔ)言。使用內(nèi)部DSL的優(yōu)勢(shì)是我們不必?fù)?dān)心創(chuàng)建、編譯及解析語(yǔ)法,因?yàn)檫@些已經(jīng)被宿主語(yǔ)言解決掉了。劣勢(shì)是會(huì)受限于宿主語(yǔ)言的特性。如果宿主語(yǔ)言不具備這些特性,構(gòu)建一種表達(dá)能力強(qiáng)、簡(jiǎn)沽而且優(yōu)美的內(nèi)部DSL是富有挑戰(zhàn)性的(請(qǐng)參考網(wǎng)頁(yè)[t.cn/Rqr3B12])。
外部DSL不依賴(lài)某種宿主語(yǔ)言。DSL的創(chuàng)建者可以決定語(yǔ)言的方方面面(語(yǔ)法、旬法等),但也要負(fù)責(zé)為其創(chuàng)建一個(gè)解析器和編譯器。為一種新語(yǔ)言創(chuàng)建解析器和編譯器是一個(gè)非常復(fù)雜、長(zhǎng)期而又痛苦的過(guò)程(請(qǐng)參考網(wǎng)頁(yè)[t.cn/Rqr3B12])。
解釋器模式僅與內(nèi)部DSL相關(guān)。因此,我們的H標(biāo)是使用宿主語(yǔ)言提供的特性構(gòu)建一種簡(jiǎn)單但有用的語(yǔ)言,在這里,宿主語(yǔ)言是Python。注意,解釋器根本不處理語(yǔ)言解析,它假設(shè)我們已經(jīng)有某種便利形式的解析好的數(shù)據(jù),可以是抽象語(yǔ)法樹(shù)(abstract syntax tree,AST)或任何其他好用的數(shù)據(jù)結(jié)構(gòu)(請(qǐng)參考[GOF95,第276頁(yè)])。
現(xiàn)實(shí)生活的例子
音樂(lè)演奏者是現(xiàn)實(shí)中解釋器模式的一個(gè)例子。五線(xiàn)譜圖形化地表現(xiàn)了聲音的音調(diào)和持續(xù)時(shí)間。音樂(lè)演奏者能根據(jù)五線(xiàn)譜的符號(hào)精確地重現(xiàn)聲音。在某種意義上,五線(xiàn)譜是音樂(lè)的語(yǔ)言,音樂(lè)演奏者是這種語(yǔ)言的解釋器。下圖展示了音樂(lè)例子的圖形化描述,經(jīng)www.sourcemaking.com準(zhǔn)許使用(請(qǐng)參考網(wǎng)頁(yè)[t.cn/Rqr3Fqs])。
軟件的例子
內(nèi)部DSL在軟件方面的例子有很多。PyT是一個(gè)用于生成(X)HTML的Python DSL。PyT關(guān)注性能,并聲稱(chēng)能與Jinja2的速度相娘美(請(qǐng)參考網(wǎng)頁(yè)[t.cn/Rqr1vlP])。當(dāng)然,我們不能假定在PyT中必須使用解釋器模式。然而,PyT是一種內(nèi)部DSL,非常適合使用解釋器模式。
Chromium是一個(gè)自由開(kāi)源的瀏覽器軟件,催生出了Google Chrome瀏覽器(請(qǐng)參考網(wǎng)頁(yè)[t.cn/hMjLK])。Chromium的Mesa庫(kù)Python綁定的一部分使用解釋器模式將C樣板參數(shù)翻譯成Python對(duì)象并執(zhí)行相關(guān)的命令(請(qǐng)參考網(wǎng)頁(yè)[t.cn/Rqr1zZP])。
應(yīng)用案例
在我們希望為領(lǐng)域?qū)<液透呒?jí)用戶(hù)提供一種簡(jiǎn)單語(yǔ)言來(lái)解決他們的問(wèn)題時(shí),可以使用解釋器模式。不過(guò)要強(qiáng)調(diào)的第一件事情是,解釋器模式應(yīng)僅用于實(shí)現(xiàn)簡(jiǎn)單的語(yǔ)言。如果語(yǔ)言具有外部DSL那樣的要求,有更好的工具(yacc和lex、Bison、ANTLR等)來(lái)從頭創(chuàng)建一種語(yǔ)言。
我們的目標(biāo)是為專(zhuān)家提供恰當(dāng)?shù)木幊坛橄?,使其生產(chǎn)力更高;這些專(zhuān)家通常不是程序員。理想情況下,他們使用我們的DSL并不需要了解高級(jí)Python知識(shí),當(dāng)然了解一點(diǎn)Python基礎(chǔ)知識(shí)會(huì)更好,因?yàn)槲覀冏罱K生成的是Python代碼,但不應(yīng)該要求了解Python高級(jí)概念。此外,DSL的性能通常不是一個(gè)重要的關(guān)注點(diǎn)。重點(diǎn)是提供一種語(yǔ)言,隱藏宿主語(yǔ)言的獨(dú)特性,并提供人類(lèi)更易讀的語(yǔ)法。誠(chéng)然,Python已經(jīng)是一門(mén)可讀性非常高的語(yǔ)言,與其他編程語(yǔ)言相比,其古怪的語(yǔ)法更少。
實(shí)現(xiàn)
我們來(lái)創(chuàng)建一種內(nèi)部DSL控制一個(gè)智能屋。這個(gè)例子非常契合如今越來(lái)越受關(guān)注的物聯(lián)網(wǎng)時(shí)代。用戶(hù)能夠使用一種非常簡(jiǎn)單的事件標(biāo)記來(lái)控制他們的房子。一個(gè)事件的形式為command->receiver->arguments。參數(shù)部分是可選的。并不是所有事件都要求參數(shù)。不要求任何參數(shù)的事件例子如下所示。
open->gate
要求參數(shù)的事件例子如下所示:
increase -> boiler temperature -> 3 degrees
->符號(hào)用于標(biāo)記事件一個(gè)部分的結(jié)束,并聲明下一個(gè)部分的開(kāi)始。實(shí)現(xiàn)一種內(nèi)部DSL有多種方式。我們可以使用普通的正則表達(dá)式、字符串處理、操作符重載的組合以及元編程,或者一個(gè)能幫我們完成困難工作的庫(kù)/工具。雖然在正規(guī)情況下,解釋器不處理解析,但我覺(jué)得一個(gè)實(shí)戰(zhàn)的例子也需要覆蓋解析工作。因此,我決定使用一個(gè)工具來(lái)完成解析工作。該工具名為Pyparsing,是標(biāo)準(zhǔn)Python3發(fā)行版的一部分心。要想獲取更多Pyparsing的信息,可參考Paul McGuire編寫(xiě)的迷你書(shū)Getting Started with Pyparsing。如果你的系統(tǒng)上還沒(méi)安裝Pyparsing,可以使用下面的命令來(lái)安裝。
pip3 install pyparsing
下面的時(shí)序圖展示了用戶(hù)執(zhí)行開(kāi)門(mén)事件時(shí)發(fā)生的事情。對(duì)于其他事件,情況也是相似的,不過(guò)有些命令更復(fù)雜些,因?yàn)樗鼈円髤?shù)。
在編寫(xiě)代碼之前,為我們的語(yǔ)言定義一種簡(jiǎn)單語(yǔ)法是一個(gè)好做法。我們可以使用巴科斯-諾爾形式(Backus-Naur Form,BNF)表示法來(lái)定義語(yǔ)法(請(qǐng)參考網(wǎng)頁(yè)[t.cn/Rqr1ZrO])。
event ::= command token receiver token arguments command ::= word+
word ::= a collection of one or more alphanumeric characters token ::= ->
receiver ::= word+ arguments ::= word+
簡(jiǎn)單來(lái)說(shuō),這個(gè)語(yǔ)法告訴我們的是一個(gè)事件具有command -> receiver -> arguments 的形式,并且命令、接收者及參數(shù)也具有相同的形式,即一個(gè)或多個(gè)字母數(shù)字字符的組合。包含數(shù)字部分是為了讓我們能夠在命令increase -> boiler temperature -> 3 degrees中傳遞3 degrees這樣的參數(shù),所以不必懷疑數(shù)字部分的必要性。
既然定義了語(yǔ)法,那么接著將其轉(zhuǎn)變成實(shí)際的代碼。以下是代碼的樣子。
word = Word(alphanums)
command = Group(OneOrMore(word)) token = Suppress("->")
device = Group(OneOrMore(word)) argument = Group(OneOrMore(word))
event = command + token + device + Optional(token + argument)
代碼和語(yǔ)法定義基本的不同點(diǎn)是,代碼需要以自底向上的方式編寫(xiě)。例如,如果不先為word賦一個(gè)值,那就不能使用它。Suppress用于聲明我們希望解析結(jié)果中省略->符號(hào)。
這個(gè)例子的完整代碼(文件interpreter.py)使用了很多占位類(lèi),但為了讓你精力集中一點(diǎn),我會(huì)先只展示一個(gè)類(lèi)。書(shū)中也包含完整的代碼列表,在仔細(xì)解說(shuō)完這個(gè)類(lèi)之后會(huì)展示。我們來(lái)看一下Boiler類(lèi)。一個(gè)鍋爐的默認(rèn)溫度為83攝氏度。類(lèi)有兩個(gè)方法來(lái)分別提高和降低當(dāng)前的溫度。
class Boiler:
def __init__(self):
self.temperature = 83 #
def __str__(self):
return 'boiler temperature: {}'.format(self.temperature)
def increase_temperature(self, amount):
print("increasing the boiler's temperature by {} degrees".format(amount))
self.temperature += amount
def decrease_temperature(self, amount):
print("decreasing the boiler's temperature by {} degrees".format(amount))
self.temperature -= amount
下一步是添加語(yǔ)法,之前已學(xué)習(xí)過(guò)。我們也創(chuàng)建一個(gè)boiler實(shí)例,并輸出其默認(rèn)狀態(tài)。
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()
print(boiler)
獲取Pyparsing解析結(jié)果的最簡(jiǎn)單方式是使用parseString()方法,該方法返回的結(jié)果是一個(gè)ParseResults實(shí)例,它實(shí)際上是一個(gè)可視為嵌套列表的解析樹(shù)。例如,執(zhí)行print(event. parseString('increase -> boiler temperature -> 3 degrees'))得到的結(jié)果如下所示。
[['increase'], ['boiler', 'temperature'], ['3', 'degrees']]
因此,在這里,我們知道第一個(gè)子列表是命令(提高),第二個(gè)子列表是接收者(鍋爐溫度),第三個(gè)子列表是參數(shù)(3攝氏度)。實(shí)際上我們可以解開(kāi)ParseResults實(shí)例,從而可以直接訪問(wèn)事件的這三個(gè)部分。可直接訪問(wèn)意味著我們可以匹配模式找到應(yīng)該執(zhí)行哪個(gè)方法(即使不可以直接訪問(wèn),也只能這樣做)。
cmd, dev, arg = event.parseString('increase -> boiler temperature -> 3 degrees')
if 'increase' in ' '.join(cmd):
if 'boiler' in ' '.join(dev):
boiler.increase_temperature(int(arg[0]))
print(boiler)
執(zhí)行上面的代碼片段會(huì)得到以下輸出。
python3 boiler.py
boiler temperature: 83
increasing the boiler's temperature by 3 degrees
boiler temperature: 86
interpreter.py的完整代碼與我剛描述的沒(méi)有什么大的不同,只是進(jìn)行了擴(kuò)展以支持更多的事件和設(shè)備。
from pyparsing import Word, OneOrMore, Optional, Group, Suppress, alphanums
class Gate:
def __init__(self):
self.is_open = False
def __str__(self):
return 'open' if self.is_open else 'closed'
def open(self):
print('opening the gate')
self.is_open = True
def close(self):
print('closing the gate')
self.is_open = False
class Garage:
def __init__(self):
self.is_open = False
def __str__(self):
return 'open' if self.is_open else 'closed'
def open(self):
print('opening the garage')
self.is_open = True
def close(self):
print('closing the garage')
self.is_open = False
class Aircondition:
def __init__(self):
self.is_on = False
def __str__(self):
return 'on' if self.is_on else 'off'
def turn_on(self):
print('turning on the aircondition')
self.is_on = True
def turn_off(self):
print('turning off the aircondition')
self.is_on = False
class Heating:
def __init__(self):
self.is_on = False
def __str__(self):
return 'on' if self.is_on else 'off'
def turn_on(self):
print('turning on the heating')
self.is_on = True
def turn_off(self):
print('turning off the heating')
self.is_on = False
class Boiler:
def __init__(self):
self.temperature = 83 # in celsius
def __str__(self):
return 'boiler temperature: {}'.format(self.temperature)
def increase_temperature(self, amount):
print("increasing the boiler's temperature by {} degrees".format(amount))
self.temperature += amount
def decrease_temperature(self, amount):
print("decreasing the boiler's temperature by {} degrees".format(amount))
self.temperature -= amount
class Fridge:
def __init__(self):
self.temperature = 2 # in celsius
def __str__(self):
return 'fridge temperature: {}'.format(self.temperature)
def increase_temperature(self, amount):
print("increasing the fridge's temperature by {} degrees".format(amount))
self.temperature += amount
def decrease_temperature(self, amount):
print("decreasing the fridge's temperature by {} degrees".format(amount))
self.temperature -= amount
def main():
word = Word(alphanums)
command = Group(OneOrMore(word))
token = Suppress("->")
device = Group(OneOrMore(word))
argument = Group(OneOrMore(word))
event = command + token + device + Optional(token + argument)
gate = Gate()
garage = Garage()
airco = Aircondition()
heating = Heating()
boiler = Boiler()
fridge = Fridge()
tests = ('open -> gate',
'close -> garage',
'turn on -> aircondition',
'turn off -> heating',
'increase -> boiler temperature -> 5 degrees',
'decrease -> fridge temperature -> 2 degrees')
open_actions = {'gate':gate.open, 'garage':garage.open, 'aircondition':airco.turn_on,
'heating':heating.turn_on, 'boiler temperature':boiler.increase_temperature,
'fridge temperature':fridge.increase_temperature}
close_actions = {'gate':gate.close, 'garage':garage.close, 'aircondition':airco.turn_off,
'heating':heating.turn_off, 'boiler temperature':boiler.decrease_temperature,
'fridge temperature':fridge.decrease_temperature}
for t in tests:
if len(event.parseString(t)) == 2: # no argument
cmd, dev = event.parseString(t)
cmd_str, dev_str = ' '.join(cmd), ' '.join(dev)
if 'open' in cmd_str or 'turn on' in cmd_str:
open_actions[dev_str]()
elif 'close' in cmd_str or 'turn off' in cmd_str:
close_actions[dev_str]()
elif len(event.parseString(t)) == 3: # argument
cmd, dev, arg = event.parseString(t)
cmd_str, dev_str, arg_str = ' '.join(cmd), ' '.join(dev), ' '.join(arg)
num_arg = 0
try:
num_arg = int(arg_str.split()[0]) # extract the numeric part
except ValueError as err:
print("expected number but got: '{}'".format(arg_str[0]))
if 'increase' in cmd_str and num_arg > 0:
open_actions[dev_str](num_arg)
elif 'decrease' in cmd_str and num_arg > 0:
close_actions[dev_str](num_arg)
if __name__ == '__main__':
main()
opening the gate
closing the garage
turning on the aircondition
turning off the heating
increasing the boiler's temperature by 5 degrees
decreasing the fridge's temperature by 2 degrees
執(zhí)行上面的例子會(huì)得到以下輸出。
python3 interpreter.py opening the gate
closing the garage
turning on the aircondition
turning off the heating
increasing the boiler's temperature by 5 degrees
decreasing the fridge's temperature by 2 degrees
如果你想針對(duì)這個(gè)例子進(jìn)行更多的實(shí)驗(yàn),我可以給你提一些建議。第一個(gè)會(huì)讓例子更有意思的改變是讓其變成交互式。目前,所有事件都是在tests元組中硬編碼的。然而,用戶(hù)希望能使用一個(gè)交互式提示符來(lái)激活命令。不要忘了Pyparsing對(duì)空格、Tab或意料之外的輸出都是敏感的。例如,如果用戶(hù)輸入turn off -> heating 37,那會(huì)發(fā)生什么呢?
另一個(gè)可能的改進(jìn)是,注意open_actions和close_actions映射是如何用于將一個(gè)接收者關(guān)聯(lián)到一個(gè)方法的。使用一個(gè)映射而不是兩個(gè),可能嗎?這樣做有何優(yōu)勢(shì)?
小結(jié)
本章中,我們學(xué)習(xí)了解釋器設(shè)計(jì)模式。解釋器模式用于為高級(jí)用戶(hù)和領(lǐng)域?qū)<姨峁┮粋€(gè)類(lèi)編程的框架,但沒(méi)有暴露出編程語(yǔ)言那樣的復(fù)雜性。這是通過(guò)實(shí)現(xiàn)一個(gè)DSL來(lái)達(dá)到H的的。
DSL是一種針對(duì)特定領(lǐng)域、表達(dá)能力有限的計(jì)算機(jī)語(yǔ)言。DSL有兩類(lèi),分別是內(nèi)部DSL和外部DSL。內(nèi)部DSL構(gòu)建在一種宿主編程語(yǔ)言之上,依賴(lài)宿主編程語(yǔ)言,外部DSL則是從頭實(shí)現(xiàn),不依賴(lài)某種已有的編程語(yǔ)言。解釋器模式僅與內(nèi)部DSL相關(guān)。
樂(lè)譜是一個(gè)非軟件DSL的例子。音樂(lè)演奏者像一個(gè)解釋器那樣,使用樂(lè)譜演奏出音樂(lè)。從軟件的視角來(lái)看,許多Python模板引擎都使用了內(nèi)部DSL。PyT是一個(gè)高性能的生成(X)HTML的 Python DSL。我們也看到Chromium的Mesa庫(kù)是如何使用解釋器模式將圖形相關(guān)的C代碼翻譯成Python可執(zhí)行對(duì)象的。
雖然一般來(lái)說(shuō)解釋器模式不處理解析的工作,但是在12.4節(jié),我們使用了Pyparsing創(chuàng)建一種DSL來(lái)控制一個(gè)智能屋,并且看到使用一個(gè)好的解析工具以模式匹配來(lái)解釋結(jié)果更加簡(jiǎn)單。
第13章將演示觀察者模式。觀察者模式用于在兩個(gè)或多個(gè)對(duì)象之間創(chuàng)建一個(gè)發(fā)布—訂閱通信類(lèi)型。