冒號課堂§4.4:情景范式
冒號課堂
第四課 重溫范式(4)
4.4情景范式——餐館里的編程范式
理論是認(rèn)生的孩童,多陪他玩玩,自會活潑起來 ——題記
關(guān)鍵詞: 編程范式,情景編程
摘要: 編程范式在餐館中的應(yīng)用
?提問
l 什么是閉包?為什么被稱為閉包?它有什么作用?
l 規(guī)則引擎有何用處?
l 能否設(shè)想一個生活中的場景,把介紹的九種編程范式都用上?
:講解
嘆號摘下眼鏡,揉了揉眼:“范式再好,多了也難免有些審美疲勞。”
逗號也搓著太陽穴:“這段時間腦子被灌得沉甸甸的。”
“彼此彼此!你們的腦袋老鬧澇災(zāi),我的喉嚨老鬧旱災(zāi)。”冒號說著,拿起礦泉水瓶一飲而盡。
大伙聽著怪別扭的,這不是拐著彎說我們腦子進水了嗎?
冒號清了清嗓子:“為尊重民意,也為避免消化不良,大家先輕松一下。下面我們來個情景編程。”
“情景編程?沒聽說過,只聽說過情景英語。”問號感到挺新鮮。
“都是學(xué)語言嘛,有何兩樣?”冒號輕描淡寫,“讓我們試著用生活中的實例將一些編程范式串聯(lián)起來。前面提到,OOP可以看作管理一個服務(wù)型公司,現(xiàn)在以餐館為例,你們每人設(shè)計一類對象及其提供的服務(wù)。”
問號來了興致:“我先來吧。構(gòu)造一個前臺接待員,負(fù)責(zé)迎客、引座、送客。”
句號很是不滿:“還真不客氣,上來就把最漂亮的對象搶走了。”
臺下一陣笑聲。
“我來構(gòu)建最常見的服務(wù)員。”逗號一捋袖子,一副準(zhǔn)備開干的樣子,“負(fù)責(zé)斟茶、寫菜、上菜、換盤。”
“嗯,很熟練。”冒號一本正經(jīng)。
句號實在得很:“我設(shè)計收銀員,專管收帳、出具發(fā)票。”
引號頗為自豪:“我造一個技術(shù)含量最高的大廚,專門負(fù)責(zé)烹調(diào)。”
逗號不服:“你倒簡單,那么高的技術(shù)含量,敢情炒肉和燉肉一個做法?。?#8221;
引號自覺理虧:“那就負(fù)責(zé)蒸、煮、炒、燉吧。”
冒號為其辯護:“引號同學(xué)并沒有錯,可惜沒能堅持。廚師只需提供一種服務(wù):把紙上菜變成盤中菜,至于蒸、煮、炒、燉等具體做法純屬實現(xiàn)細(xì)節(jié)。”
嘆號有點委屈:“唉,看來我只好做技術(shù)含量最低的廚工了,負(fù)責(zé)食品預(yù)加工、洗碗、打掃清潔。”
冒號將大家設(shè)計的類翻譯成Java代碼——
// 前臺接待員
Class Receptionist
{
public void receive(Customer customer) {…} // 迎客
public void usher(Customer customer) {…} // 引座
public void send(Customer customer) {…} // 送客
}
// 服務(wù)員
Class Waiter
{
public void pourTea(Customer customer) {…} // 斟茶
public List<Order> write(Customer customer){…} // 寫菜
public void serve(Customer customer, Course course){…} // 上菜
public void exchangePlate(Customer customer) {…} // 換盤
}
// 收銀員
Class Cashier
{
public void charge(Customer customer) {…} // 收帳
public void issueInvoice(Customer customer){…} // 出具發(fā)票
}
// 廚師
Class Cook
{
public Course cook(Order order) {…} // 烹調(diào)
}
// 廚工
Class KitchenHand
{
public void prepareFood() {…} // 準(zhǔn)備食品
public void washDishes() {…} // 洗碗
public void clean() {…} // 打掃清潔
}
“你們造人,我來造物。”冒號構(gòu)造了一個餐館的類——
// 餐館
Class Restaurant
{
// 每當(dāng)有顧客來訪,返回該顧客
private Customer accept() {…}
// 為指定顧客提供所有的餐館服務(wù)
private void serve(Customer customer) {…}
// 餐館服務(wù)
public void service()
{
while (true) // 無限循環(huán),假設(shè)餐館7×24小時營業(yè)
{
final Customer customer;
if ((customer = accept() ) != null) // 某顧客來訪
{
serve(customer); // 為該顧客提供服務(wù)
}
}
}
}
冒號解說道:“這里accept類似Socket的accept,屬于堵塞呼叫,意味著此方法將堵塞進程直至收到新數(shù)據(jù)。為簡單計,把一行顧客當(dāng)作一個Customer。大家對此段代碼有何看法?”
“沒什么,很簡單啊。”逗號說完補充一句,“關(guān)鍵是serve方法的實現(xiàn)。”
“這里我們明顯用到了兩個范式,對象式和過程式。”冒號提示道。
引號會意:“應(yīng)該還需要并發(fā)式。serve如果與service在同一線程中運行,那么餐館只有等服務(wù)完一個Customer后才能服務(wù)后面的,這顯然是荒唐的。”
“對極了!”冒號將“serve(customer);”改寫為——
// serve(customer); // 錯誤地使用單線程!
new Thread // 構(gòu)造一個線程
(new Runnable()
{
public void run(){ Restaurant.this.serve(customer); }
}).start(); // 啟動該線程
冒號道:“這回serve在新線程中運行,不會耽誤Restaurant服務(wù)下一位Customer了。”
問號眼尖:“我注意到聲明customer時前面加上了關(guān)鍵字final,有必要嗎?”
“如果不用線程,是不必要的。”冒號回應(yīng)道,“我們在建造線程時用到了實現(xiàn)Runnable接口的匿名類(anonymous class),它是涉及到局部變量customer的內(nèi)部類(inner class),Java語法要求該局部變量必須是final類型。值得一提的是,這里不僅用到了并發(fā)式,而且與函數(shù)式也密切相關(guān)。”
“函數(shù)式?”逗號奇道。
“不錯。”冒號堅定地點著頭,“我們不是提過函數(shù)式中的函數(shù)是頭等公民嗎?也就是說,函數(shù)與其他基本數(shù)據(jù)類型一樣,可以作為傳遞參數(shù)、返回值或與變量名綁定。閉包(closure)便是這樣一種函數(shù),并且還能保留當(dāng)初創(chuàng)建時周圍的環(huán)境變量。以上匿名類本質(zhì)上是函數(shù)serve的包裝,經(jīng)實例化后作為參數(shù)傳入Thread的構(gòu)造函數(shù),并且記住了外部類的局部變量customer——這也是為什么它必須是final以保證不被重新賦值的原因。應(yīng)該說這是一種OO化的閉包形式,預(yù)計在Java 7中它的用法會更簡潔,這也意味著Java代碼中將飄進更多的函數(shù)式風(fēng)味。”
引號喃喃道:“以前只聽說過數(shù)學(xué)里有個閉包的概念。”
冒號略加指點:“可以這么理解:所謂包,指函數(shù)與其周圍的環(huán)境變量捆綁打包;所謂閉,指這些變量是封閉的,只能為該函數(shù)所專用。如果你真懂?dāng)?shù)學(xué),就會發(fā)現(xiàn)它們本質(zhì)上是相通的。”
嘆號隱隱約約地覺得:“把函數(shù)與變量捆綁起來,怎么聽起來像是OOP???”
“嗯,的確相似。”冒號微頷,“不妨認(rèn)為閉包就是封裝了環(huán)境變量的隱形對象的方法——通常是匿名方法。我們用一段JavaScript代碼來加深印象——”
/* 返回函數(shù)f的近似導(dǎo)函數(shù) */ function derivative(f) { // dx最好作為參數(shù)傳入,放在此處主要是為了說明閉包的用法 } /* 返回一個數(shù)的平方數(shù) */ function square(x) { return x * x; } /* 返回一個數(shù)的雙倍數(shù) */ function double(x) { return 2 * x; } /* 對任何一個不大的數(shù)值變量x(比如小于100),下列函數(shù)的返回值應(yīng)該非常接近于零 */ function testSquareDerivative(x) { return derivative(square) (x) - double(x); }
var dx = 0.00001; // dx越小,近似度越高
return function(x) { return (f(x + dx) - f(x)) / dx; };
嘆號感到有點頭痛:“怎么跑出了微積分?大學(xué)學(xué)的那點高數(shù)早就還給老師了。”
冒號笑著安慰他:“還給老師沒關(guān)系,我再借給你一點:平方函數(shù)的導(dǎo)數(shù)是雙倍函數(shù)。因此,函數(shù)derivative(square)應(yīng)該很接近函數(shù)double的作用效果。作為檢驗,testSquareDerivative能將任何一個不大的數(shù)映射到近似于零的數(shù)[1]。”
引號這下徹底明白了:“在求導(dǎo)函數(shù)derivative中,傳入的參數(shù)f和返回值都是函數(shù),這是函數(shù)作為頭等公民的特征。其中返回的匿名函數(shù)就是閉包,它附著了兩個環(huán)境變量:外層函數(shù)的傳入?yún)?shù)f和局部變量dx。”
“完全正確!”冒號作出積極的肯定,“如果不是閉包,這兩個環(huán)境變量在derivative返回后就會失去效用。由此可見,合理地使用閉包能使代碼更加簡潔清晰,散發(fā)出函數(shù)式特有的優(yōu)雅氣質(zhì)。”
句號有些按捺不住編程的沖動,自告奮勇:“我來具體實現(xiàn)餐館的serve方法吧。”
得到冒號的默許,句號在黑板上寫下——
private void serve(Customer customer)
{
// 找一個空閑的接待員
Receptionist receptionist = findReceptionist();
receptionist.receive(customer);
receptionist.usher(customer);
// 找一個空閑的服務(wù)員
Waiter waiter = findWaiter();
waiter.pourTea(customer);
List<Order> orders = waiter.write(customer) ;
// 將菜單交給一位廚師
Cook cook = waiter.pass(orders);
for (Order order : orders) // 廚師照單做菜
{
Course course = cook.cook(order);
// 找一個空閑的服務(wù)員
waiter = findWaiter();
// 服務(wù)員上菜
waiter.serve(customer, course);
// 顧客開始享用
customer.eat(course);
}
// 顧客用餐完畢。。。
// 找一個空閑的收銀員
Cashier cashier = findCashier();
cashier.charge(customer);
cashier.issueInvoice(customer);
// 找一個空閑的接待員
receptionist = findReceptionist();
receptionist.send(customer);
}
句號寫畢又復(fù)查一遍,拍拍手上的粉筆灰,心滿意足地走下臺來。
嘆號提意見:“我的廚工沒派上用場,應(yīng)該在廚師烹調(diào)前調(diào)用KitchenHand的prepareFood方法。”
問號挑出另外的毛?。?#8220;在for循環(huán)中,廚師、服務(wù)員和顧客的行為應(yīng)該在不同的線程中,廚師不可能等服務(wù)員上完一道菜或顧客吃完一道菜后才做下一道。”
“可能更復(fù)雜呢!”逗號也來湊熱鬧,“一位顧客點的幾樣菜可能分別由幾位廚師同時做,每位廚師都在不同的線程中工作。”
引號更嚴(yán)謹(jǐn):“還應(yīng)有一個后臺線程,讓服務(wù)員(Waiter)隨時換盤(exchangePlate),讓廚工(KitchenHand)隨時洗盤(washDishes)和清潔(clean),這樣所有服務(wù)人員提供的服務(wù)都用上了。”
句號倒抽涼氣:“估不到漏洞這么多,并發(fā)式真是無處不在啊。”
冒號繼續(xù)點撥:“換盤子有兩種方式:一種是服務(wù)員主動換,一種是客人要求換。前者是輪詢,后者是通知。”
“哦,事件驅(qū)動式!”句號迅即反應(yīng)過來,“客人是事件源,服務(wù)員是事件處理器,客人不定期地招手呼喚是在發(fā)表事件以通知服務(wù)員??腿伺c服務(wù)員是多對多的松耦合關(guān)系。”
冒號點點頭,又指著引號:“剛才有人不滿你的大廚職責(zé)過于簡單,現(xiàn)在你來實現(xiàn)一下,也好顯顯技術(shù)含量。”
引號在臺上摸了半天頭,編出一段代碼——
Class Cook
{
public Course cook(Order order)
{
// 根據(jù)菜單查食譜
Recipe recipe = lookupRecipe(order);
// 找到食譜的烹調(diào)步驟
List<Instruction> instructions = recipe.getInstructions();
for (Instruction instruction : instructions)
{
follow(instruction); // 按食譜的指令操作
}
}
}
“堂堂大廚原來是靠查食譜做菜的。”逗號揶揄道。
引號為難地說:“這不是在編程嘛,好端端的人腦,不得不去模擬電腦,完全搞倒了。”
“要設(shè)計會烹調(diào)的機器人,興許還真得這樣呢。”冒號笑道,“不過由于各種菜式組合繁多,如果每種菜都配菜譜未免太龐雜,如何精簡呢?”
句號建議:“菜式成千上萬,烹調(diào)技法相對少許多,不妨以技法為主線。”
“好主意!”冒號挑起大拇指,“如果把待加工的菜看作數(shù)據(jù),技法看作算法,將數(shù)據(jù)與算法分離,以算法為中心,那是什么范式?”
“泛型式!”大家異口同聲。
“至此我們已涉及了過程式、對象式、并發(fā)式、函數(shù)式、事件驅(qū)動式和泛型式。”引號扳著手指算著,“還差邏輯式、元編程和切面式了。”
冒號把目光轉(zhuǎn)向逗號:“寫菜單并不容易,如果客人不直接點菜,你的服務(wù)員如何向他推薦?”
逗號答:“最簡單的方法是報菜名,并一一詢問客人。”
冒號皺眉:“這樣你是簡單了:一個迭代就完事,可客人也該發(fā)火了。”
逗號趕緊修正:“先詢問客人的口味、忌諱等等,再向他建議一些菜式。”
“這還差不多。”冒號眉頭舒展開來,“考慮到客人的口味、忌諱等各有不同,餐館的菜單也隨時可能變化,如果把這些都硬編碼(hardcode),再加上層層疊疊的if-else語句,代碼將成為懶婆娘的裹腳——又臭又長又難維護。”
引號提議:“可以把這些信息預(yù)先存入數(shù)據(jù)庫,屆時用SQL查詢。”
“想法很好,只是有一點難度。”冒號提醒道, “這些信息并非簡單的對應(yīng)關(guān)系,包含一些邏輯推理,甚至需要一些模糊判斷。”
句號一拍大腿:“前面不是提到領(lǐng)域特定語言DSL嗎?將所有規(guī)則用自定義的DSL編寫,再利用元編程轉(zhuǎn)換成C、Java之類的通用語言,不是很好嗎?”
“棒極了!”冒號不吝贊詞,“不過還有一種思路。我們可以搜集餐館的菜式、顧客口味、忌諱以及各種菜與口味、忌諱之間的關(guān)系等等一系列事實和規(guī)則,用規(guī)則語言(Rule Language)來描述,通過規(guī)則引擎(Rule Engine)來導(dǎo)出符合顧客需求的菜肴。這種方式將業(yè)務(wù)規(guī)則與應(yīng)用程序分離、將知識表示與邏輯實現(xiàn)分離,是SoC原理的一種應(yīng)用,同時也是一種邏輯式編程。”
問號關(guān)心地問:“這些規(guī)則引擎與Java程序兼容嗎?”
冒號回答:“不少規(guī)則引擎用Java實現(xiàn)或?qū)?/span>Java平臺設(shè)計,如Jess、Drools、JLisa、JRules等,另外Sun還發(fā)布了javax.rules API (JSR 94)以統(tǒng)一對各類引擎的訪問接口。”
引號頗感意外:“既然是邏輯式編程,為什么不采用代表語言Prolog呢?”
冒號準(zhǔn)備了一大段理由等著他:“剛才提到的規(guī)則引擎都是基于Rete算法[2]的,主要采用數(shù)據(jù)驅(qū)動的(data-driven)正向推理(forward chaining)法,而Prolog引擎采用目標(biāo)驅(qū)動的(goal-driven)逆向推理(backward chaining)法。正向推理自底向上,利用推理規(guī)則從已有的事實數(shù)據(jù)推出更多的數(shù)據(jù),直到達成目標(biāo);逆向推理正相反,自頂向下,從目標(biāo)出發(fā)尋找滿足結(jié)論的事實[3]。相比而言,正向推理適合針對不同輸入作出不同反應(yīng),而逆向推理適合回答查詢?,F(xiàn)在是服務(wù)員根據(jù)客人的喜好提建議,當(dāng)然用正向推理更合適。再說這類引擎與Java的集成更加方便,因此我們沒有選擇Prolog。”
講到此處,每個人都意識到,只剩下最后一個范式了。
冒號提出一個新問題:“假如餐館經(jīng)理接到顧客投訴,反映服務(wù)人員態(tài)度不好,衛(wèi)生狀況也不理想,應(yīng)該怎么辦?”
問號搶先說:“首先我的接待員在迎客(receive)時要笑容可掬地對顧客說:‘歡迎光臨!’,在送客(send)時要對顧客鞠躬:‘請慢走,歡迎下次再來’”
逗號接著說:“我的服務(wù)員在上完菜后應(yīng)對客人說:‘請慢用’,句號的收銀員也應(yīng)加些禮貌用語,讓人家高高興興地掏錢。”
句號補充道:“服務(wù)員在上菜(serve)前、廚師在烹飪(cook)前應(yīng)洗手,廚工在洗碗(washDishes)后應(yīng)對餐具消毒。”
冒號緊接著問:“如果餐館對禮貌規(guī)范或衛(wèi)生標(biāo)準(zhǔn)做修改,必然要牽扯不同類中的不同的方法,維護起來很不方便,怎樣才能有效地解決這個問題呢?”
答案已經(jīng)昭然若揭了。
冒號干脆自問自答:“不錯,正是用切面式編程。只要創(chuàng)立兩個Aspect:Etiquette和Sanitation,分別負(fù)責(zé)禮貌規(guī)范和衛(wèi)生標(biāo)準(zhǔn)方面的事務(wù)。一旦某一方面的要求發(fā)生變化,比如餐館來了外賓,或者碰上非典或禽流感,只需在相應(yīng)的Aspect模塊中作調(diào)整:將禮貌用語換成英語或者提高衛(wèi)生標(biāo)準(zhǔn)等等。如果采用runtime AOP,甚至還可在運行期選擇激活或禁用這些Aspect。”
下面開始有些騷動,大伙早已腦中滿滿而腹中空空,有點頭重腳輕了。
冒號見狀,遂發(fā)出激動人心的號召:“今天的課到此結(jié)束,讓我們從虛擬的餐館中走出,到真實的餐館中去吧!”
眾人齊聲歡呼。
,插語
[1] 若輸入數(shù)過大,則需要設(shè)定更小的dx。此外,還可能產(chǎn)生計算溢出。
[2] Rete算法是一種高效的模式匹配算法,用于實現(xiàn)規(guī)則生成系統(tǒng)(production rule system)。
[3] 用邏輯的語言來說,正向推理順著從前件(即if語句)到后件(即then語句)的方向,逆向推理順著從后件到前件的方向。
。總結(jié)
l 閉包是一種能保留當(dāng)初創(chuàng)建時的環(huán)境變量的函數(shù)。它通常以匿名的方式存在,多用于函數(shù)式編程中,能使代碼更加簡潔清晰。Java中的匿名類可以看作OO化的閉包形式。
l Jess、Drools、JLisa、JRules等規(guī)則引擎主要基于正向推理,能無縫地與Java平臺集成。它們提供了邏輯式編程環(huán)境,能有效地將業(yè)務(wù)規(guī)則從應(yīng)用程序中分離出來,提高了軟件的靈活性和可維護性。
l 每種編程范式都能在生活中找到它的應(yīng)用,它們本來就是人類思維方式的投影。
“”參考
[1] Wikipedia.Closure (computer science).http://en.wikipedia.org/wiki/Closure_(computer_science)
[2] Ernest Friedman-Hill.Jess 7.1p2 manual.http://www.jessrules.com/jess/docs/Jess71p2.pdf
[3] Mark Proctor等.Drools Documentation.http://downloads.jboss.com/drools/docs/4.0.7.19894.GA/html_single/index.html
課后思考
l 同樣一個問題用不同的語言來編程,代碼可能會有極大的差異。你認(rèn)為這種差異的主要根源是語言還是范式?
l 認(rèn)真研究本課中的編程范式匯總表,并補充新的內(nèi)容,如各范式的關(guān)鍵詞、理論基礎(chǔ)、最佳實踐、注意事項等等。
l 掌握編程范式對語言學(xué)習(xí)和編程設(shè)計有何實際意義?
posted on 2008-12-21 23:18 鄭暉 閱讀(3109) 評論(1) 編輯 收藏 所屬分類: 冒號課堂