版本:
2009.01.09 翻譯第一版
2009.01.15 根據作者的回函,修訂了一些地方。尋找2009.01.15可找到重要的修改處。
文章:The Universal Design Pattern 通用萬能的設計模式
日期:2008.10.20
作者:Steve Yegge
作者的部落格(2006至今):Stevey's Blog Rants
作者舊的文章(2004與2005):Stevey's Drunken Blog Rants
作者簡介:目前任職於Google,之前任職於Geoworks與Amazon。程式語言生涯中有過兩次非常關鍵性的轉換,一次是組合語言,一次是Java跟Perl,因為發現解決語言本身設計帶來的問題所花的時間,竟然比真正用在開發軟體系統的時間還多。其文章以長度聞名,長到應該稱為論文而非部落格,兼帶詼諧筆風,發表頻率大約一個月一到兩篇,作者總是說這些是在凌晨三罐啤酒下肚後的誇誇其談,但每一篇都是經過長時間醞釀,內容充實有見地的傑作。
獨立以Java/JPyton開發多人線上遊戲Wyvern,可讓玩家自行創建擴充遊戲內容。其工作團隊將Rails移植到Rhino上,Rhino是運作於JVM平台上的JavaScript引擎,Rails(Ruby on Rails)為一套受到廣泛喜愛使用的網站開發模組;正為Emacs撰寫完整的JavaScript環境,期望在Emacs上可以有一套JavaScript IDE,以及將來可用JavaScript而非elisp來開發Emacs extensions。
譯者推薦文章:
Execution in the Kingdom of Nouns,討論Java會什麼這麼囉嗦。
Blogger's Block #4: Ruby and Java and Stuff
The Pinocchio Problem,優良的軟體系統有何共通性質。
The Next Big Language,提出下一個熱門語言應該具備那些特性。
Dynamic Languages Strike Back
譯者寫在前面:我會保留大量的原文術語,就算是某些專業術語的翻譯已經被普遍接受。若有任何意見,還請留言。
通用萬能的設計模式(The Universal Design Pattern)
在特殊例中具有普遍性,這樣的概念有著深遠的重要性。 This idea that there is generality in the specific is of far-reaching importance. — Douglas Hofstadter, Godel, Escher, Bach |
注意: 今天這篇是技術性文章:並不有趣,就算你笑了也不用謝謝我。
更新,2008年10月20日: 我加上了更新訂正 ,我會追蹤重要的回應,至少持續一個禮拜左右,目前為止有三次。
目錄
- 序言
- 三大軟體塑模(Software Modeling)學派
- 大腦與思想(Brains and Thoughts)
- 誰在用屬性模式(Properties Pattern)呢?
- 屬性模式(Properties Pattern)綜觀
- 表示法(Representations)
- 繼承(Inheritance)
- 效能(Performance)
- 暫時屬性(Transient properties)
- 持久化(Persistence)
- 型別系統(Type systems)
- 工具組
- 困難處
- 延伸閱讀
- 更新訂正
- 最後一些想法
序言
今天我想應該來談談一個很棒卻沒得到太多關愛的設計模式:屬性模式(Properties Pattern)。更正式一點時也被稱為雛型模式(Prototype Pattern)。
到處可見有人在用這模式,我也會列出一些不錯的實例。不論使用哪一種程式語言此設計模式都相當有用,等會我們就會看到,在一般用途的持久化策略(persistence strategy),它也非常有用。
奇怪的是此模式雖近乎萬能,但大家卻甚少討論,我想原因是它相當有彈性以及容易適應於各種情況;Properties Pattern被人冠上"不實際"的設計或塑模方法,特別是被某些物件導向設計(在程式語言領域)或關聯式設計(在資料庫領域)極端熱情的擁護者看作是某種可恥的偷吃步,這些善心人士傾向將Properties Pattern當作『只不過是name/value pairs』–某種一時便捷卻引領你卡在困境的權宜之計。
我希望在此提出一種不同且具多面性的觀點,如果還能加上一點小運氣的話,或許這篇能啟動流程讓Properties Pattern再次成為時尚潮流之一,就留給時間證明吧。
三大軟體塑模(Software Modeling)學派
在開講屬性模式(Properties Pattern)之前,讓我們複習一些當今用來塑模軟體問題最流行的方法。
我必須先指出,這些技巧沒一個是一定要跟"靜態型別(static typing)"或"動態型別(dynamic typing)"綑綁在一起的,不論有沒有靜態型別檢查(static checking),每個方法都可應用,也就是說,塑模(Modeling)跟static typing是互不影響 的,所以不論你對於static checking的觀感如何,你都要能看出每一個方法本身的價值所在。
類別塑模(Class Modeling)
不用我多說你已經很瞭解了,以類別為基礎的物件導向設計(Class-based OO design類別為基礎的物件導向設計是隻八百磅的大猩猩,訴求一種自然的匹配原則,也就是我們早已運用在日常生活歸類事物的方法。一開始是需要一點點練習,但對大部分人來說,Class Modeling很快地就成為第二天性。
雖然業界酷愛OO design,但在學界卻不是那麼特別受喜愛的話題,因為,OO design沒有一套數學原理來當作根基—至少在某人創建出邊際效應side effects的正式模型(formal model)之前是沒有的,物件導向程式設計OOP的概念源頭不是數學而是模模糊糊的直覺。
就某方面而言這解釋了它的受歡迎程度,同時也解釋了為什麼物件導向程式設計在實際運用上有著這麼多細微差別的流派:要不要(以及如何)支援多重繼承(multiple inheritance)、靜態成員(static members)、方法多載(method overloading) vs. 豐富的函式參數(rich signatures)等等,業界從沒一致同意過何為OOP,不過倒是都愛它。
關聯式塑模(Relational Modeling)
關聯式資料庫(relational database modeling)稍微困難一點,需要多一點的學習時間,因為它的威力來自於它的數學基礎,Relational Modeling是有可能很直覺的,取決於處理的是哪一方面的領域,不過大部分的人都會同意這不會是必然的:需要一些技巧才能學會如何把任何一個問題塑模在關聯式的輪廓中。
物件塑模(Object modeling)跟關聯式塑模產生出來的設計藍圖差異相當大,各有千秋;在我們這一行最難處理的問題當中,物件-關聯式的對應關係(object-relational mapping ORM)總是榜上有名,這問題相當要緊,某些人可能已經讓你覺得這沒什麼,或是說這問題已經被一些開發框架(frameworks)自動處理掉了,例如Rails或是Hibernate,但更清楚問題的人知道,ORM在現實社會中的產品與系統上是有多麼的難。
XML塑模
XML提供了另一種方法來解問題,通常XML用在資料上,但也可用在程式碼上,例如,建設在XML之上的frameworks,譬如Apache Ant和XSLT提供計算的機制:迴圈(loop)或遞迴(recursion)、條件式敘述(conditional expressions)以及設定變數
在某些領域,程式員在還沒深入想一想類別模型前就決定要用XML,因為在那些領域上,XML確實提供了最便利的方式來思考問題。
容易用XML塑模的資料傾向不容易以關聯式的方法塑模,反之亦然;根據經驗,XML跟關聯式的對應關係(XML/relational mapping)跟ORM幾乎一樣難搞。
至於說到XML跟物件導向的對應關係(XML/OO mapping),我們大部分人某種程度上都將之視為一個已解的問題,然而在實際上用來處理XML/OO mapping有著數種不同的方式,相互競爭著,W3C的DOM和SAX廣為使用,但兩者皆相當笨重,其他的選擇,例如JDom和REXML(以及其他),漸漸地得到一定數量的使用者。
我提到這個不是要挑起一場筆戰,僅是用來闡明XML也是個塑模方法,跟關聯式塑模與類別塑模一樣,有自然平滑也有凹凸不平的一面,很合理的預期。
其他學派
我並非聲稱塑模學派只有這三個–絕對不是!兩個顯目的候選人是Functional modeling(從Functional Programming的角度來看;以lambda calculus為根基)和Prolog-style logical modeling,兩者皆為成熟的塑模策略,各有優缺點,各自或多或少與其他塑模方式有重疊的地方。另外還有很多很多其他的學派,大概有幾打吧。
希望你會帶走一個重點,沒有一個塑模學派是"優於"其他的,本質上每一種塑模方法都可以運用在任何一種問題上。
每一門都有其需要考量的tradeoffs,很明顯吧—不然現在就會只剩下一個了。
譯註:我沒有(也不會)翻譯tradeoff。意思是某些要求是沒辦法同時達到的。例如有句話是"速度價格跟品質,選兩個吧",你沒辦法讓三者都達到最高,三者之間有排他性的關係存在,當你要求速度快,品質自然就會降低。例如某個語言可以讓你很快的寫出想要的程式,但可能很難維護或是沒有可讀性別人都看不懂。所以困難點在於因情況的不同來選擇哪個是關鍵的,哪幾個會相互影響,哪些是別太差可以接受就好。
尋找最佳打擊點
有時候在一個問題上使用多種方法是很合理的,你或許想要混合式的XML/relational資料設計,或是以Functional的觀點來作以類別為基礎的物件導向設計,或是在系統內嵌一套規則引擎(rules engine)。
選擇正確的方法要以便利性(convenience)為依歸,任何一個碰得到的實際問題,很可能有一或兩個塑模學派是最便利的,到底要用哪一個取決於問題的特殊細節。
我說的所謂便利(convenient),可能跟你想的不同,對我而言,一個便利的設計方案是讓此設計方案的使用者感到便利,而且它也要能很便利地來進行表達(express),在越少越好的原則下:也就是說假定其他條件皆相同,越小的設計方案比較大的好;用另一個方式說,設計方案要對它自己本身有便利性!
(2009.01.15)譯註:所謂"設計方案要對它自己本身有便利性!",詢問作者之後,其意為:具有簡潔優雅的設計核心,可依此不斷發展擴充成適合用來解決某問題的設計方案。例子是Lisp,有人說,不論哪種問題,Lisp都不適用,但卻可以Lisp為核心,輕易地發展出一套特別適合的語言來解問題。
很不幸地,大部分的程式員(包括我自己)傾向使用恰恰錯誤的便利性定義:選擇一個對他們自己方便的塑模方法;若是他們只對一或兩個學派有經驗,猜猜看每次碰上問題時,他們會拿出哪個方法呢?
這問題貫穿了整部電腦運算史,對一件事情總存在著某個"最佳"工具來解決,但假若程式員不知道,那他們將會選出較差的工具因為他們認為時程不允許有段學習曲線,長遠來看他們反而拖慢了時程,然而身陷溝渠中的人又怎麼能看出這點來呢。
塑模學派(modeling schools)就像是程式語言(programming languages)、網站開發模組(web frameworks)、編輯環境(editing environments)和其他零零總總的工具:你不會懂得正確選擇為何,除非你對全部的選項都有相當程度的了解,若是能加上實際的操作運用經驗會更好。
要記得的重點是,所有的塑模學派都是"first class",因為都可以用來解決任何一個問題,還有,沒有一個塑模學派可以成為任何情況的完美選擇;在解決某問題時,某個方法你很熟悉,但這不能表示那就是最好的辦法,最好的程式員致力於精通所有可用的方法,於是乎有較佳的機會作出正確的選擇。
屬性塑模(Property Modeling)
有了這些來龍去脈後,我現在主張屬性模式(Properties Pattern)也是一種塑模方法,具備著它獨特的長處和tradeoffs,於是跟我以上所提到的方法區隔開來,跟它們並駕齊驅,因為,它也能夠塑模同樣廣的問題。
在我們談完Properties Pattern各種細節後,我認為到那時我就可以說服你這個模式是居於主流塑模學派的地位,希望你會開始感覺到有哪些種類的問題是適合用它來解–運用之廣有時候比其他學派更多,即使是你最愛的那個。
但在跳進技術細節前,讓我們先概要地看一份,以屬性為基礎的塑模方法(Property-based modeling)跟以類別為基礎的物件導向設計(class-based OO design)之間的比較,這是一份我認為有著某種真實力量在背後的非技術論點。
大腦與思想(Brains and Thoughts)
Douglas Hofstadter終身都在想著我們是如何思考的,關於這議題,在前世紀他寫下了比任何人都要多的著作文章,即使有人可以用文字的量來取勝,也沒人可以在寫作風格上或對程式員的影響上跟他相匹敵。
他的書蘊含著驚人的豐富想像力,有著一卡車的閱讀樂趣,假若你是程式員而還沒讀過Godel, Escher, Bach: An Eternal Golden Braid(通稱為"GEB"),我羨慕你:你真的賺到了,趕快去買然後沉浸於史上最有趣、令人惱怒、讓人起敬畏之心、以及就是有趣的書之一,得到普立茲獎(Pulitzer Prize)也不算真的反映出它應有的地位,這是從古至今可以想像出來的最偉大、最最無與倫比的著作之一。
Hofstadter在GEB中提出一個令人嘆服的論點(30年前!),以屬性為基礎的塑模方法(property-based modeling)是我們大腦運作的基本法則,在Chapter XI ("Brains and Thoughts"),其中有三小節,標題是Classes and Instances,The Prototype Principle,和The Splitting-off of Instances from Classes,合在一起構築出Properties Pattern概念的礎石,在這些小節的論述中,Hofstadter說明了Prototype Principle是如何與傳統class-based modeling產生關聯。
但願我能全文貼上他的論述—僅三頁而已—但我必須鼓勵你自己去讀它;他的命題是這樣的:
最特殊的項目可當作此類項目中的一般例。
The most specific event can serve as a general example of a class of events.
Hofstadter提供了好幾個例子來支持這個命題,但我將改述我的最愛之一,大致上如下所說。
想下你正在聽一場NFL美式足球賽廣播,播音員正在介紹一位你一無所知的新選手,現在這位新人–假定他叫做L.T.–是個類別(class)"美式足球選手(football player)"的instance,沒有一丁點差別。
譯註:我不知道該怎麼翻譯instance;把花當做class,那麼我們可以說玫瑰是花的instance之一;在Java中,Book x = new Book();,Book是一個class,我們說x是Book的instance。
播音員提到L.T.有點像Emmitt Smith,速度快平衡度高,擅長在防禦人群中找到洞鑽。
現在,L.T.基本上是Emmitt Smith的一個"instance"(或是clone):他繼承了所有Emmitt的屬性(properties),至少是那些你熟悉的屬性。
然後播音員又補充說L.T.也擅長於接球,所以有時他會打外接員(wide receiver)這個位置,喔對了,他有戴護眼罩,他跑起來像Walter Payton,諸如此類。
每當播音員加上各種特性,新人L.T.逐漸地變成一個獨特的實體,越來越少依存在父類別(parent class)"美式足球選手(football player)"上,他變成了非常非常特殊的美式足球選手。
但重點來了,即使他是個非常特殊的instance,現在你可以把他當作一個類別(class)來使用!若是下個球季來了個新人Joe,播音員或許會這麼介紹:"Joe很像L.T.",就跟之前一樣,Joe繼承了所有L.T.的屬性(properties),每一項都可以被更動,進而把Joe轉變成美式足球選手中特殊且獨一無二的instance。
這就叫做prototype-based modeling:Emmitt Smith是L.T.的prototype,而且L.T.變成Joe的prototype,之後Joe還可成為其他人的prototype。Hofstadter說The Prototype Principle:
"在特殊例中具有普遍性,這樣的概念有著深遠的重要性。"
"This idea that there is generality in the specific is of far-reaching importance."
再說一次,我剛帶過了三頁的論述,你應該自己去讀一讀,碼的,你應該念過整本書:這是最偉大的書籍之一,沒什麼好說的,而且每一個程式員都應當熟讀此書。
希望在我將Hofstadter的論點扼要概述後,已經說服了你,我將此模式稱之為"通用萬能的設計模式(The Universal Design Pattern)"不是只可能是個小花招吸引你來看我的部落格,而且也讓你覺得接下來的文章值得一讀。
很不幸地Properties Pattern大到要用一整本書來談,而事實上,把一整個塑模學派稱之為"設計模式(design pattern)"是在炒短線而沒有遠見的。
因此,若這篇看起來是這麼臭哄哄的長,那是因為我試著將整本書塞進一篇部落格中,如同我往常一樣;但看看好的一面,我幫你節省了一大段時間,看這篇總比念一本書快吧。
即便如此,也不要因為需要分幾次才能看完這篇而感到沮喪,它仍然包含著一大堆的資訊,我曾考慮要分開成三篇,但最後我決定刪掉一半的內容。(給Jeff和Joel:真的,我刪掉50%。)
誰在用屬性模式(Properties Pattern)呢?
我假定你已被說服這個模式是值得花點心思來學一學的,要不來你已經離開了;所以接下來的應用案例不是用來吸引你,而是告訴你有多種不同的方式來使用此模式。
有成百的例子可選,但我會集中火力在少數幾個真實案例,希望這樣就足以展現出它的廣泛性與彈性。
開始之前有兩點要先記住,第一,稱呼的不同,因為即使它已相當普遍,但卻沒有太多相關的文章著作,有文章稱之為Do-It-Yourself Reflection,另一份稱之為Adaptive Object Modeling,可到延伸閱讀看看我挖到的一些網址。
不論你叫它那個名字,一旦懂得如何辨別它的存在,你就會開始看出此模式到處都有,只不過隱藏在各種偽裝之下罷了;現在我們有了一個共通的稱呼,之後要認出它就更容易了。
第二點,Properties Pattern之運用可小可大:使用程度的窄或廣由你決定,可以是很簡單地將property lists附加到一些類別(classes)上,於是使用者可以自由地加上擴充意義,甚至大到是一套以雛型為基礎(prototype-based)全方位的framework,據以用來塑模你系統中所有的一切。
所以我給的案例有小小的也有很大的。
Eclipse
一個不錯的小例子是Eclipse Java Development Tools (JDT):一組用來塑模Java程式語言的類別(classes),包含抽象語法樹(abstract syntax tree)、符號圖(symbol graph)與其他後設資料(metadata);有了這些,Java原始程式碼就能被看做成一組有結構的資料,Eclipse後端才能依此做出所有跟你的Java程式碼有關的神奇效果。
你可以看看在javadoc中這一系列的類別,先到help.eclipse.org,然後點選JDT Plug-in Developer Guide,然後Programmer's Guide,然後JDT Core,然後
org.eclipse.jdt.core.dom
,這一套件定義了採用強型別檢查(strongly-typed)的類別(classes)與介面(interfaces),Eclipse用來塑模Java程式語言。如果你點到任何繼承
ASTNode
的類別(class),將看到它會有個property list,在ASTNode
的javadoc註解中這麼說著:"每個AST節點(node)都能夠帶有由client方定義且可自由增減的一堆屬性(properties),新產生的節點沒有這些東西,可利用getProperty
與setProperty
來存取這些屬性。"
我喜歡這個例子,有幾個理由,首先,它很單純地應用了Properties pattern,此例沒有牽扯到prototypes、序列化(serialization)、後設程式設計(metaprogramming)與其他很多我等會將略略談到的技法,所以這是個引入門的好例子。
第二,它被包捆起來放在一個非常非常strongly-typed的系統當中,顯現出Properties pattern跟一般statically-typed classed-based modeling絕非互斥的,兩者可以巧妙地產生互補作用。
第三,此例的屬性系統(property system)本身是相當strongly typed的:定義了一組支援類別例如
StructuralPropertyDescriptor
、 SimplePropertyDescriptor
和ChildListPropertyDescriptor
來加上一些限制在client的屬性值(property values)上,我不是太傾向這樣的手法,因為我覺得這樣一來會使得他們的API變得相當笨重,但這絕對是一個合理的選擇;當你也想照這個樣子去實作此模式時,可作為一個不錯的借鏡。JavaScript
可以運用到"多遠多廣"呢,讓我們看看光譜另一端的JavaScript程式語言,將Prototype Principle和Properties Pattern置放在整個語言的核心。.
人們喜歡將各個dynamic languages全部混在一起談,經常將JavaScript視為某種次級版本的Perl或Python或Ruby,慚愧,過去大約十年來我也這麼想。
但JavaScript跟大部分其他的dynamic languages(甚至Lisp)有實質上的不同,因為它的塑模機制是以Properties Pattern為中心,大體上是從一個叫做Self的語言傳承過來的,以及參考其他一些現代的語言(特別是Io跟其他我等下會談到的語言),這些語言也採用prototypes與properties而非傳統的類別(classes)。
在JavaScript系統中,每個可跟使用者產生互動的物件都是繼承自
Object
,其內建了一個property list,雛型繼承(Prototype inheritance)(回想一下,Emmitt Smith instance可當做L.T. instance的prototype)是一項具first-class地位的語言機制,而且JavaScript提供了數種語法來支援存取屬性(properties)與宣告property lists(以"object literals"方式出現)。把JavaScript形容成世界上被誤解最深的程式語言真是太貼切了;加上剛剛瞭解的,我們可以開始用不同的方式看待JavaScript,唯有深入體驗這個全新的塑模學派,才能靈活運用JavaScript,如果你單單把JavaScript看待成(比如說)Java或是Python的替代品來試用,你會覺得很難用,礙手礙腳的。
既然大部分人對property-based modeling只擁有相當稀少的實戰經驗,而實情的確就是如此,所以JavaScript受到嚴厲的批評也沒什麼好奇怪的。
再往前推進一點
在你的系統中除了可依照接近核心程度的區別來打算怎麼運用Properties pattern,你還能決定遞迴(recursive)的深度:你的屬性有明確的後設屬性(meta-properties)嗎?要有metaprogramming hooks嗎?你將提供多少內建的reflection呢?
JavaScript提供了少許的metaprogramming hooks,其中之一是最近才出來的
__noSuchMethod__
,當嘗試呼叫物件中一個其值為函式卻不存在的屬性,可以讓你攔截到這種失敗的動作。可惜JavaScript並不提供夠多我所喜歡的hooks,例如,考慮到一致性的話, 應該有卻沒有的__noSuchField__這樣一個hook,於是乎多少限制了整體的彈性,此外,也沒有標準的屬性變更(roperty-change)事件通報(event notification)的機制,也沒有合理的方式做出這樣的機制,所以說,JavaScript幾乎快到了終點,卻在一小段距離前停下來,可能是考量到效能吧,要不然的話,就可以提供有完整延展性的metaprogramming system了,就好像SmallTalk所提供的,就某個程度而言,Ruby也算有。
這模式漸漸有樣子了...
在進行到其他案例前,讓我們先藉由跟其他成功語言做個比較,把JavaScript(以及它把Properties pattern當做中心思想這點)好好檢視一番。
首先,JavaScript不是我最喜愛的語言,過去兩年多來在客戶端(client-side)與伺服端(server-side),我寫了一卡車JavaScript的程式,所以對它的熟悉程度就跟其他到現在我還有在使用的語言一樣。
以JavaScript目前展現出的形象來說,在很多地方它不是最佳的工具,例如,建立APIs,還有在Unix scripting方面做不到像Perl跟Ruby那麼強,沒有一套建立函式庫或套件的系統,沒有命名空間(namespaces),缺乏很多新的好東西,如果你正在找個一般用途(general-purpose)的程式語言,JavaScript沒辦法滿足你。
但JavaScript在很多其他地方的確是最佳的工具,舉個例子好了,JavaScript在撰寫unit tests方面表現傑出—不論是寫給它自己或其他語言的測試碼,因為使用Properties Pattern而可以把所有的物件(跟類別)當做一個裝著屬性(properties)的袋子,於是creation of mock objects夢想成真了,對object literals提供語法支援讓美夢更美,從Java或C++甚或是Python看到的那些呆子般的frameworks,你一個也不需要。
而且,JavaScript是地球上兩個最好的腳本語言(scripting languages)之一,怎麼說呢?若果從最正確的方向來看這個詞彙"scripting language":也就是,一套語言專門特別設計用來內嵌(embed)在較大的宿主系統(host system)內而且用來操作或"script"宿主系統內的物件。這是當初JavaScript的設計方向,它加上一些延伸性後仍適度地夠小,它有一份相當精鍊非正式的規格,而且它有一套精心打造的介面(interface)用來跟宿主系統的物件做交流。
相較而言,Perl、Python、Ruby像是雜草叢生,每個都嘗試著(就好像C++跟Java)成為全方位的最佳語言,主流語言中唯一可跟JavaScript匹敵且適用於各種宿主系統的是Lua,在遊戲業界裡是相當有名的scripting language。
而且你不知道吧,Lua也把Properties Pattern放在設計理念的中心位置,處在它核心的
Table
結構跟JavaScript內建的Object
真是像極了,且Lua也使用prototypes而非classes。所以,世界上最成功的兩個scripting languages都是prototype-based,只是個巧合嗎?抑或一個經過適當設計的class-based的語言也能夠一樣成功呢?
很難說,我以Jython當做embedded scripting language已經有很長一段時間,運作良好,但我個人已經開始相信Properties Pattern真的就是比class-based modeling更容易發揮出延展性(extensibility),而prototype-based比class-based的語言更適合當做extension languages,那恰恰就是embedded scripting所要的:終端使用者發展且延伸宿主系統(host system)。
事實上在認識JavaScript之前我就下了這個結論;讓我們看看另一個"誰在用?"的有趣案例:Wyvern。
Wyvern
我在自行開發的多人線上遊戲Wyvern中相當深遠地運用上了Properties Pattern,雖然運用的地方跟我們討論過的有些不同;遠在聽過Self或Lua跟學習JavaScript之前,我就在設計Wyvern了,回頭想想,我的設計跟它們如此相像還真是令人驚訝。
Wyvern是以Java開發出來的,但根類別
GameObject
帶有property list,就好像JavaScript的Object
基礎類別同個意思,Wyvern有prototype inheritance機制,但既然我那時還沒聽過prototypes,我把它們叫做archetypes,在Wyvern中,任何一個遊戲物件都可以當其他任何一個遊戲物件的archetype,而且property的存取跟繼承方式大概就跟JavaScript一樣。為了建構出具有終極延展性的遊戲,我抓頭抓了好幾個月後才生出這樣的設計方案(1996年底),我希望能夠由玩家來創作全部的遊戲內容,為此提出好幾打詳盡的使用案例use cases,就是為了能讓玩家能以嶄新的方式來擴展遊戲機能,最後的設計方案是一組設計模式(design patterns)交叉式結合在一起,包含了一套強大的命令列系統,一套很棒的hooks/advice系統,以及其他數個子系統,改天有空我會樂意寫出來。
但,核心的資料模組是Properties Pattern。
就某些地方而言,Wyvern的實作比JavaScript功能更強,Wyvern提供更多metaprogramming的工具,例如可隨時被中斷(vetoable)的property變更通報機制,這讓遊戲中的物件在跟遊戲環境互動時有著超乎想像般的彈性,Wyvern也支援暫時(transient)與永久(persistent)屬性(properties),這項我下面會討論。
而其他方面Wyvern做了不一樣的選擇,比較大的一個是Wyvern的屬性值(property values)是statically typed的,property 名(names)固定是字串,跟JavaScript相同,但值可以是各種一般型別(ints、longs、booleans、strings,等等),或是函式(functions)(Java不容易做到這點),或甚至也可以是archetypes。
不看這些差異的話,Wyvern核心部分那一塊的property-list架構就幾乎像是JavaScript、Self跟Lua的,而且十多年來我仍然很滿意這樣的設計,已經達到或超出我原先在玩家延展性所做的期許,特別是它能夠讓玩家線上動態隨時修改遊戲世界內的各種行為反應,而不需要重新啟動遊戲,這被證實為是一個超具威力的特色(且廣受玩家喜愛)。
Wyvern有個很明顯的瑕疵,缺乏語法(syntax)的支援,一旦我決定遊戲要用Properties pattern為中心設計,我就應該採用一套適用於實作此模式的程式語言:理論上一套能夠從頭到腳支援此模式的語言。
最後,我的程式碼有一大部分採用了Python(正確地說,Jython)來寫,而且遠比任何其他用Java寫的部份還要簡潔跟有彈性,但當時我愚蠢地擔心會出現效能問題,於是乎,我把至少一半的高階遊戲邏輯用Java來寫,堆砌出幾千幾百行的
getProperty
和setProperty
的程式碼,如今整個系統已經難以進行最佳化了;如果當初我能徹底分開遊戲引擎以及"scripty"程式碼,現在就會容易的多。(2009.01.15)譯註:"scripty"程式碼,指的是玩家自行創建的遊戲內容,包含關卡設計與怪物AI等等。
即使我用Python來寫整個遊戲,我依然要實作出prototype inheritance framework,方能夠讓任一物件擔任其他任一物件的prototype。
我知道我還沒有真正解釋為什麼prototype inheritance是這麼好用,除了約略帶到unit testing的mock objects,但為了讓這一篇可被吸收消化,我必須刪掉好幾頁詳盡的解說案例,例如,藉由以可程式化的步驟來增加新屬性到任何一個已經存在的怪物而產生出來的"Chieftain Monsters"。
我說過這個模式足可大到一本書的份量,我是說一本很大的書;由於沒法舉出簡單易懂的例子,我只能這麼說,當初若是採用JavaScript/Rhino(或Lua,一旦它出現在JVM上)應該會讓我的日子好過點,或者,碼的,當面對一套又大又有野心的系統時,說不定搞出一套我自己的語言才是最佳解答。
無論如何,活到老學到老,邊作邊學,Wyvern是一大堆程式碼沒錯啦,但仍是套properties-based與prototype-based的系統,而且結果它有著很不錯的彈性。
現在我們已經看過了兩個大型應用案例(Wyvern和JavaScript),再談幾個案例後我就要結束"有誰在用"這個章節。
Lisp
Lisp本身就有個小尺度的Properties Pattern範例:它的symbols有property lists,在Lisp內Symbols是first-class的,它就如同是你目前命名空間(namespace)內的名字(names)一樣,像是full-qualified的Java類別名。
如果所有Java類別都有property lists的話,仍只能算是Properties pattern小尺度運用,但對Java程式員而言那將會釋放出大量設計上的可能性,同樣的,Lisp在進行到讓所有的東西都有property list之前停了下來,但就其提供property lists的範圍而言,已經是非常卓越的設計工具了。
有鑑於Emacs Lisp數千個組態設定中,每一組設定基本上都算是一個在全域命名空間(global namespace)中的屬性(property),我們可說它重度地使用了Properties Pattern,它支援暫時(transient) vs. 永久(persistent)的屬性(properties)的概念,藉由緩衝區局部變數)buffer-local variables)它提供了受到限制的屬性繼承(property inheritance)。
可惜Emacs不支援任何關於prototypes的觀念,事實上它一點也沒有任何物件導向的機制,有時候我想要在Emcas中塑模某東西,想要使用帶有彈性property lists的物件(objects),這種時候會發現,我希望有JavaScript可以用,但即使沒有prototypes,把properties用來表示資料致使Emacs也能擁有相當顯著的延展性。
當你要決定使用Properties pattern到多大程度時,有很多重要的tradeoffs要緊記在心;我會略微談到他們。在這裡我不是在批評挖苦任何一個系統或語言所做的取向;因為使用了此模式,它們都得到了改善,不論使用的程度有多廣。
再看看XML
早先我將XML描述成一個first-class塑模學派,現在掌握了更多的背景知識後,就允許我們將XML視做一種具體呈現Properties Pattern的方式,因為XML的底層結構根本就是Properties Pattern。
XML採取二維的方式:properties在XML中可以attributes或是elements的形式出現,分別擁有不同的語法跟語意的限制,很多人苛責這套雙property子系統造成不必要且多餘的複雜度,但以結果來看,其實沒那麼要緊,因為有兩套總比沒有好。
對於static checking,之前的例子各自有不同的作法:Eclipse(嚴格且強制)、JavaScript/Lua(極少)以及Wyvern(中等程度)。
XML提供的作法我認為是最理想的,它讓你自己決定,在塑模問題初期(也就是在"prototyping"階段—這個字眼現在可以代表更有趣的意義了),你可以先走非常弱的型別檢查,除了well-formed的檢查外什麼都沒有;對很多問題來說,這種static checking足以滿足需求,所以有這個作法可選是好的。
你的模型越來越複雜後,你可以選用DTD做額外的確認檢查動作(validation),而且如果想要更高強度的話,你可以過渡到功能完善強大的XML Schema或是Relax NG schema,視你的需求而定。
XML已被證實為是一個很受歡迎的塑模工具,特別是對Java世界來說—遠遠超過在動態語言的社群中,這是因為Java基本上沒有提供任何輔助讓我們來導入Properties Pattern,當Java程式員需要這個模式時,目前最簡便的方式就是使用XML。
Java與XML的組合被證實是相當強大的,即使沒有語法上的整合,以及兩者協調上的有一些困難處;使用XML來做塑模通常是比用Java類別來做還合適的,Eclipse的AST property lists若是以XML跟DOM來做塑模可能會更好:工作量會較少,介面會讓人容易上手,還有Apache Ant:若拿JSON-style JavaScript objects來當做build files會恰恰好就是他們所需要的,但是當領悟到他們需要一套plug-in系統時,傷害已經造成了。
當Mozilla Rhino的說明文件越來越完善後,當Java程式員開始察覺把JSON當做一種輕量型的XML是多麼有用後,JavaScript就有機會拉近差距,Rhino提供Java無縫隙的Properties Pattern,優於任何一樣XML方案,我之前也已經提到說,在unit tests跟mock test data方面,JavaScript是相當卓越的(即使是相較於XML)。
但真正的好處還不只是unit testing,每一個夠大用Java寫的程式且只要大於中型的程式,都需要一套scripting engine,不論開發者有沒有察覺到,軟體總是成長到瀏覽器或試算表或是文書處理器那樣子的大小後,開發者才終於領悟到有必要提供一套scripting機制,不過實際上,即使是小程式也能經由提供scripting的機制從中得益,而XML在此並不適合。這又是一個例子,程式員總是因為知道某一個塑模學派所以就選它,而不是學著因時地來選擇正確的工具。
事情就是這樣。
Bigtable
最後一個範例:Google的Bigtable,提供給很多Google applications一個大量、可大可小、高效能的資料儲存場所(線上有部分的說明文件–有興趣就點一下鏈結),這一個Properties Pattern的具體呈現是一個多維表格結構,keys是字串,leaf values是opaque blobs。
硬派關聯式資料塑模師有時會宣稱大系統將因缺乏一套牢固的schema而徹底惡化,也說這樣的系統也不可能有好的效能表現,Bigtable剛好正是一個反例。.
雖是這麼說,但在很多問題上,有一套明確的schema的確是有用的;我將會稍微討論到這跟Properties Pattern有何關係。
現在應該是個好時機談談Amazon的Simple Storage Service,但我對它一點概念都沒有,只聽說它用了name-value pairs。
不論如何,我希望這些應用案例(Eclipse AST classes、JavaScript、Wyvern game objects、Lisp symbols、XML與HTML以及Bigtable)已經說服了你,Properties pattern是到處可見的、強大的、多重用途的,而且它應該列入每個程式員或設計師的考量選項之一。
接下來讓我們更深入探討如何來實作Properties Pattern,有何trade-offs,以及關於這個有彈性的設計方案的其他面向。
屬性模式(Properties Pattern)綜觀
概略看來,每一個Properties Pattern的實作都有著相同的核心API,任何將names對應到values的資料集合,其核心API為:
- get(name)
- put(name, value)
- has(name)
- remove(name)
所以最簡單的實作會是某種的Map,你系統中的物件是Maps而內容物是Properties。
讓它更有威力的下一步是將某個特別的property name保留下來表示(非必須的)parent link,你可以稱呼它為"parent"、"class"、"prototype"、"mommy"或其他,都可以,如果有的話,它應指到另一個Map。
現在你有了parent link,可以開始增強get、put、has和remove的意涵,當某一個property不在目前物件的property list中,就可以沿著parent link指的方向去找,大體上來說作法就是如此,但還有小地方等下會討論,但不用太多的思索你應該就可以開始想像該怎麼進行。
哇,你已經有了一個相當完善的Prototype Pattern實作,所需要的就是一個parent link!
從這裡開始,這模式可朝很多面相來發展,餘下的篇幅我會談及到一些比較吸引人的。.
表示法(Representations)
在實作底層有兩大考慮重點:如何表示property keys,用何種資料結構來儲存key/value pairs。
鍵(Keys)
Properties pattern幾乎一定用字串String當做keys,是可以採用任何一種物件,但用字串當keys更好,因為這樣一來,要實作prototype inheritance就易如反掌了。("繼承"一個key為某種opaque blob的property是很難處理的–我們通常是這麼想的,繼承關係中包含了一組從父親那來的有名字的東西。)
JavaScript允許你使用任何一種物件當做keys,但在檯面下它們其實會被轉成字串,且遺失了原本面貌擁有的特性,這表示,JavaScript
Object
的property lists,在想要以任何一種物件都可當做keys的情況下,是不能勝任general-purpose hashtable角色的。某些系統允許以字串或數字當做keys,如果你的keys是正整數,那你的Map看起來就像是一個陣列(Array),想一想,陣列(Arrays)跟Maps的根本形式是相同的(映成(surjection),不多說了),在某些語言中,明顯的例子是PHP,從使用者的角度是看不出這兩者有何差異。
JavaScript可用數字當keys,允許你指定要用字串或數字,如果你的物件符合型別(Array)的要求,你可以用陣列索引的語法來存取這些數字keys。
Quoting
JavaScript的語法有個特別nice的地方(跟Ruby與Python做對比的話),你可以使用unquoted的keys,例如,你可以這麼寫
var person = { name: "Bob", age: 20, favorite_days: ['thursday', 'sunday'] }而且symbols name、age和favorite_days不~~~會被當做需藉由symbol table做解析的識別字,上面的寫法就跟下面一樣:
var person = { "name": "Bob", "age": 20, "favorite_days": ['thursday', 'sunday'] }你也須決定是不是需要quoting values,兩者擇一,例如,XML要求attribute values必須要quoted,但HTML沒有要求(假設沒有空白字元在values內)。
鍵不在的情況(Missing keys)
你須要決定如何處理表示"property不存在"的情況,最簡單的方式,如果key不在list內,那麼這個property就不存在(請看繼承(Inheritance)做更多討論)。
如果一個property常常反覆地被移除以及新增,那麼把key留著但把value設成null或許比較合理,但在某些系統,
null
可能需要被當做一個合法的property value,如此一來,你就需要找另一個不一樣(且被保留下來)的value來表示property不存在,這樣才能達到這項小小的最佳化。資料結構(Data structures)
最簡單的property list實作採用linked list,你可以把keys跟values輪流放到格子內(Lisp是這麼做的),或是每一格是個指標(pointer)指到存放key跟value的地方。
使用linked list的合適情況是:
- 你使用這個模式只是為了讓使用者把他們的擴充意義加到物件上
- 你預期使用者不需要加上太多擴充意義,不論是那個物件上
- 你不會導入繼承、序列化(serialization)或後設屬性(meta-properties)到這個模式中
下一個常見的實作選項是hashtable,進行find/insert/remove時可以有償還型常數時間複雜度(amortized constant-time)的表現,儘管記憶體需求較大,而且每次存取會有個較高的固定成本(hash function的計算成本)。
在大部分的系統中,如果properties數目不算多,最多兩到三打,那麼使用hashtable的負擔就太高了,一個常用的解法是採用混合式的作法,一開始property list只是個簡單的array或linked list,當它大到某個門檻(可能是40到50個),就把properties移到hashtable去。
請注意到我們常常用"property lists"(或簡寫成"plists")來表示property sets,這是因為一般都用lists來實作,但次序性(order)幾乎是無關緊要的,當在極少情況下需要有次序性時,有兩種可能:names需要保持當初放進來的次序(insertion order),或是names需要被排列(sort)。
如果你需要常數時間(constant-time)的存取效能以及保持insertion order,你不能做的比LinkedHashMap更好,一個非比尋常極妙的資料結構,唯一可能比它還好的就是有個concurrent version;但是,可惜啊。
如果需要讓property names照某種要求排列(sort),你會想要用ordered-map來實作,一般說來會用ordered binary tree例如splay tree或是red/black tree;使用splay tree會是個好選擇,因為insertion、lookup跟deletion的額外固定成本很低,但tradeoff是它的最壞情況的效能(worst-case performance)理論上跟linked list一樣差;當一組properties不會被均勻地存取時,splay tree會特別有用:如果物件的N個properties中有一組小子集合M比較常被存取,那麼償還型效能變成O(log M),有點類似LRU cache。
注意,你可以得到一個懶鬼型的splay tree(僅就跟LRU相似的部分而言,亦即,最近被存取的項目會漸漸地跑到list的前面去),方法是,使用一個linked list,當進行存取動作時就把那一個移到list的最前面去,這是個constant-time的動作,令人驚訝的很多實作沒有採用這個簡單的技巧:對大部分的property lists而言這幾乎是不用付出就能獲得的效能提升。
繼承(Inheritance)
有了prototype inheritance後,每個property list會有一個parent list,當你要找物件(object)中的某個屬性(property)時,首先檢查物件"local"的property list,然後再去看它上面的parent list,沿著一直往上。
譯註:"尋找(lookup)"是很基本的動作,因為get/set/add/delete等等動作都要靠lookup才能完成。
如同我在屬性模式Properties Pattern綜觀所言,實作繼承inheritance最簡單的方式是保留下一個特別名字給指向parent property list的property使用:"prototype"、"parent"、"class"還有"archetype"都是常見的選擇。
運用Properties pattern時通常不會(但這是做得到的)導入多重繼承(multiple-inheritance),若要做的話,parent link就會是一個list而不是單單一個值,要做property lookup時,由你來決定用什麼樣的規則去走過上頭的parents(多個)。
有繼承關係時property lookup演算法很簡單:找找我的list,如果property不在這,去找找我的parent的list,如果我沒有parent,回傳
null
。可用遞迴(recursive)的方式來寫,程式碼會較少,但用迭代(iterative)的方式通常是比較明智的選擇,除非你的語言提供tail-recursion elimination。在Properties Pattern系統中,這個動作Property lookups會是最昂貴的瓶頸,所以花點心思想想如何增進效能(僅這一次)並不會嫌太早。刪除(deletion)有這麼難嗎?
從物件刪除(delete)一個property後,後續如果對這個property進行存取,你通常會想要得到"not found"的回答,若沒有繼承(inheritance),要進行deletion就直接把property的key跟value從資料結構移除即可。
若有inheritance,有點難搞,因為鍵不存在(missing key)並不表示"not found"–它是說"找找我的parent看看是否我有繼承這個property"。
讓我說清楚一點,假定有個prototype list叫做Cat,其中有個property叫做"friendly-to-dogs",其value預設為boolean
true
,假使你有個cat instance叫做Morris,它的prototype是Cat:var Cat = { friendly_to_dogs: true } var Morris = { prototype: Cat }假使Morris跟某隻dog吵架,現在他恨所有的dogs,因此我們想在執行期間更新他的friendly-to-dogs property,第一個會想到的主意可能會是delete這個property,因為在回傳值會被當做真假值來解釋時,missing key或是null value通常被當做
false
。(即使在class-based的語言,例如C++或Java,這點也是對的,函式hasFooBar
會回傳true
如果內部的fooBar
欄位是non-null
。)。然而,在Morris的local list中並沒有拷貝一份"friendly-to-dogs":這是他從Cat繼承過來的,所以如果你的
deleteProperty
method只是在Morris的local list內把property刪除,他仍然還是繼承著"friendly-to-dogs",你會進入無窮苦惱迴圈直到發現這個bug為止。你不能從Cat的property list把"friendly-to-dogs"刪除,要不然你所有的cats突然間都變成恨dog恨的要命,然後你就要當和事佬。(注意,在某些狀況下,這樣的行為正是你所要的;這個故事告訴我們在彈性與安全性之間存有著亙古不變的trade-off。)
解法是弄出一個特殊的"NOT_PRESENT" property value,原本
deleteProperty
要做刪除動作但當property是繼承來的時,改為將property設定成這個特殊的值來表示已被刪除,應該用很輕量的物件來表示這個特殊的值,這樣你就可以用指標比較(pointer comparison)來檢查是不是被刪除了。所以為了能夠刪除繼承的properties,我們必須修改property-lookup的演算法,在local list的部份,(a) 是一個missing key,(b) 是null value,或是(c) 標記為NOT_PRESENT,符合其中之一的條件時,我們就當做這個property不存在。【注意:null values的意涵由系統設計者決定,你不是一定要把
null
值當做"not there",兩種作法都是可行的。】讀寫的不對稱性(Read/write asymmetry)
根據我們定義的prototype inheritance可以用邏輯推理出讀(read)跟寫(write)將有著不同的行為,例如,讀一個繼承來的property時,你沿著繼承鏈從祖先那得到value,但如果你寫(write)一個繼承來的property時,value會被寫入到物件的local list,不是寫到祖先那裡去。
讓我加以闡明,試著在Cat prototype加入一個"night-vision" property,以正整數值來表示cat在黑暗中的視力,假使預設值是5,但Morris可是敢吃胡蘿蔔的勇士,所以我們希望設定為7。
設定時,
setProperty
的程式碼並不需要去檢查繼承鏈:直接把key/value pair {"night-vision", 7}加進Morris的local property list內就可以了,如果我們在Cat內設定這個property,那所有的cats就會擁有Morris的超級視力,那可不是我們原先的意圖。不要覺得此不對稱性很奇怪,回到我們之前的例子L.T. / Emmitt Smith,當L.T.增加新的properties時,我們並不希望Emmitt也被更動!這個模式就是這樣子:你藉由新增local properties來override繼承來的值,即使當override是deletion也一樣。
只能讀的plists(Read-only plists)
很多Properties pattern的實作提供"被冰凍起來"的property lists,如果可以將整個property list設為唯讀,有時候(例如在除錯時)是很有用的,Ruby藉由內建於
Object
class的"freeze" method來提供這種功能。任一個夠大的健全實作中,你都應該提供這樣的機能。如果你提供了"冰凍(freeze)",那應該思考一下是否也要提供"解凍(thaw)",取決於你想要提供給程式員更多的保護機制呢,還是你只是想要把門鎖上後就把鑰匙丟掉。
我個人的觀點是,Java程式員剛開始接觸Ruby時傾向過度使用"freeze" function,喔對了,他們在寫Java程式時也傾向過度使用"final"。我之前提過在flexibility與safety之間存在著trade-off,當你決定使用Properties Pattern,你很清楚地意識到這麼做等於你決定了flexibility比safety還重要,在很多情況下這是對的;事實上,safety可以被看做一種最佳化(optimization):某種理論上應該包起來放在較低層、在佈景之後運作的部份,而不是穿插混在APIs跟資料模型之間。
關於safety和flexibility有一個不錯(而且容易實作)的折衷方案,提供
唯讀(ReadOnly)
的property attribute,如同JavaScript做的,的確有一些properties(例如parent pointer)不太可能隨著系統的成長而改變,所以把它們早一點就固定下來基本上是OK的。針對每一個property的特性來做不同的決定聽起來比較不嚇人,更好的是,你可以考慮把ReadOnly
property attribute規定為無法繼承(non-inheritable),所以子型別可以有他們自己的一套規定,而不會連累到完整性。關於inheritance大概說的差不多了:並不是很複雜,有一些跟繼承相關的設計議題,我會在之後的章節談到。
效能(Performance)
效能是使用Properties Pattern後隨之而來最大的trade-offs之一,很多工程師是這麼地擔心掛念於效能(以及伴隨而來的悖論與謬誤),以致於拒絕考慮使用Properties pattern,不論何種情況。
有很多聰明的方法可以改善與緩和此模式的效能問題,我不會全部都提到,但我會談到一些典型的跟一兩個新的方法。
軟禁字串(Interning strings)
確定你的string keys都被intern了,大部分的語言都會提供某些機制來intern字串,因為它大大增進了效能,所謂軟禁interning指的是把字串以一份標準的副本來取代:只有一個、無法更動、可共享的字串副本,那麼lookup演算法在檢查keys時,就可以比較pointer就好,而不用逐字比對字串的內容,所以固定成本低了很多。
唯一的缺點是,若你是在執行時動態建構出property name,interning幫助不大,因為你還是要做hash才能intern。
缺點很少,所以所有的keys都應該被intern,任何系統中,有很大比例的property names都是以原始碼中的string literals形式存在(或是放在configuration file內,當讀出來時全部都可被intern),interning在這些常見的情況都有效。
推論:不要使用大小寫不分的keys,這簡直是自殺,大小寫不分時,字串比較跟龜速一樣,特別是在Unicode的環境中。
完美雜湊(Perfect hashing)
如果你能夠在編譯期間(或是在執行期間的早期)就知道某個plist會有哪些properties,那麼你可以考慮使用"完美雜湊函式產生器(perfect hash function generator)"來產生一個理想的hash function專門給那個list用,幾乎可以確定的一點是做的比賺的要多,除非你的profiler顯示出那個list吃掉你很大比例的運算資源;但的確是有這種generators(例如gperf),專門這種情況而特製的。
Perfect hashing並不會跟Properties pattern系統易於延伸擴充的特質相衝突,你可能會有一組特別的prototype objects(例如內建好的monsters、weapons、armor等等),有清楚的定義而且一般來說在系統運作時不會更動,在這種時候使用perfect hash function generator可以加速lookups,如果在執行期間它們被更動了,你只要把那一個property list改回用原本一般的hashing作法即可。
讀取時順便拷貝過來當快取(Copy-on-read caching)
如果記憶體夠多,而且你的leaf objects繼承自一些在執行期間不太可能改變的prototype objects,你或許可以試試看copy-on-read caching,最簡單的作法就是,不論何時你從parent prototype chain那去讀一個property,把value拷貝到物件的local list中。
這種作法最大的壞處是,如果在prototype object被你拷貝過的property改變了,那在leaf object舊的value會是錯的。
在這裡為了簡便,讓我們稱呼call copy-on-read caching為"竊取(plundering)",如果Morris拷貝了prototype Cat的"favorite-food" property(value: "9Lives"),那Morris是"竊取者"而Cat是被竊取的物件。
對於這種stale-cache問題最常見的解法是,使用另外一份資料結構來記錄被竊取的物件跟竊取者,必須用weak references才不會阻礙garbage collection,(如果你是用C++,那麼願上帝憐憫你的靈魂。)每當被竊取的物件更動時,你就需要到所有竊取者那,移除他們的那一份拷貝,前提是從拷貝之後都沒被更動過。
嘿,有這麼多東西要追蹤,所以只有在極度絕望時才考慮用plundering,但如果效能是重要的關鍵,而且其他方法都無效時,那麼plundering可能會有所幫助。
重構成欄位(Refactoring to fields)
另一個增進效能的技巧是,把N個最常被使用的properties轉成instance variables。注意,在instance variables跟properties有區別的語言中才可利用這技巧,所以Java可以但JavaScript不行。
聽起來非常吸引人吧,特別是你在做效能最佳化的第一回合,警告:這作法充滿了各種隱藏的危機,所以(其他幾乎所有效能最佳化的技巧也一樣)只有在你的profiler證明了好處蓋過付出的代價時才用。
第一項代價的是API不相容性(incompatibility):突然間,原本很一致的單一properties存取介面
getProperty/setProperty
,現在你有了某些特定欄位(fields)有各自的getters跟setters,這很可能對系統造成大衝擊(畢竟,這些是最常被存取的properties)。而且除非你用Emacs,不然你的重構編輯器(refactoring editor)大概沒那麼聰明幫你依照的參數值(argument value)來進行rename-method。有個治標的方法,維持原來的
get/setProperty
interface,但現在多出一項檢查動作,檢查參數看看是否為那些特定欄位,這樣會增加一堆的switch-statement(或同樣作用的程式碼),所以你犧牲API code maintenance換來API simplicity,這樣做也使得欄位存取的速度慢了一截,抵消掉了因使用欄位(field)而得到的效能提昇。第二項代價是系統複雜度:你的系統中,需處理Properties pattern的每一部分,其程式碼將變成兩倍份量,繼承(inheritance)仍然照原先的方式運作嗎?關於序列化(serialization)呢?暫時屬性(Transient properties)?Metaprogramming?使用者存取跟修改property lists的介面呢?你面臨了程式碼大膨脹的問題,因為你把存取property切開成兩部分:plist properties和instance variables。
下一項代價是,萬一做錯了呢:你怎麼知道那些properties最常被存取?你可以做執行期間的profiling來求得一組答案,但隨著時間的演進,你系統的特性可能會改變,致使答案完全不一樣;真的要做的話,你必須定期地驅使系統進行追蹤記錄,哪些properties被存取的頻率超過可以容忍的門檻,然後你就可以把它們轉成fields。這不是一個做一次就搞定的最佳化技巧。
但這些代價都比不上這個大條的,也就是延展性(extensibility):instance variable像是刻在石頭上一樣硬而沒彈性,龐大的工作量將會出現在當你試著給予它們跟plist properties同等的性質:change-list notification、可以被override或remove、等等。因為使用fields你最後很可能要犧牲掉一些彈性。
所以使用這個最佳化技巧時,要極端慎重啊。
冰箱(Refrigerator)
最後一項效能最佳化跟節省記憶體比較有關,而非CPU。如果你擔心每個物件都有一個property-list欄位所造成的負擔,即便它經常是
null
,那麼你可以使用分離出來放在外部的資料結構來實作property lists。我不清楚通常怎麼叫它,所以在這裡我叫它作冰箱(Refrigerator),因為基本上你會在它上面貼上一堆黃色標籤;概念是這樣的,當系統中只有極少數量的物件會有property lists時,你不需要負擔每個類別中都有一個欄位存放property list,相反的,你有一個global hashtable,keys為object instances,values為相對應的propert lists。
當需要某個物件的property list時,到Refrigerator看看是否在那,property list可以利用任何我在上面表示法(Representations)提到的方式來實作。
這個概念,我大約在一場2001年Damien Conway的演講第一次聽到,那時他說正考慮用在Perl 6上,我認為相當聰明,我不記得他那時叫它做什麼,我也不知道他到底有沒有用它,但把這個點子當做他送你的禮物吧,謝謝,Damien!
還沒放上來(REDACTED)
關於Properties Pattern的效能最佳化,在一月時Brendan Eich告訴過我一堆超級棒的點子,我已經準備好發表成文章,但我告訴他會等他先寫上部落格,每一次他通知我然後跟我說"快了"。
嘿,Brendan,已經十月囉,可惡!
自己來弄(Rolling your own)
不消多說,我只言及關於Properties pattern效能最佳化的皮毛,你可以無限制地深入,我一直試著說明的一點是,當你發現運用Properties pattern後系統真是慢的不行,你也不應該喪失信心;若果真如此,不要恐慌,把flexibility丟掉–進行最佳化吧!跟最佳化搏鬥是有趣且會有回報的。
不過要記得,真的需要時才動手!
暫時屬性(Transient properties)
在開發Wyvern時,我發現,試圖更動persistent property list是個造成大災難的絕佳途徑。
譬如說某個玩家施放了抵抗魔法增強術(Resist Magic),會增加她的"魔法抵抗力(resist-magic)"30(30%),然後在法術仍然有效時,自動儲存機制啟動了(將她增強過後的property value"resist-magic"以及其他屬性寫入硬碟),然後咧,遊戲當掉了。
哇靠–現在這個玩家的魔法抵抗力永遠都有30%提昇!
也不一定要當掉才會這樣,任何一個奇怪的bug或exception condition(資料庫打個嗝,網路出了搥,宇宙射線)都可能導致這種情況,本來是一夜情卻變成天長地久的愛,還有,當你開發一個遊戲,設計成可以同時被好幾十個玩家邊玩邊改,你很快就學會預料出各種可能出現的奇怪bug或exception conditions。
我的解法是引入暫時屬性(transient properties),每個物件有(邏輯上來講)兩個property lists:一個是persistent properties,另一個是放transient properties,唯一的不同處是,當進行serializing/saving時,玩家(或怪物,或其他)的transient properties不會被儲存。
Wyvern的property-list使用有型別(typed values)的值,我還沒有談到Properties Pattern的型別系統(type systems),但概括地說,我的property values可以是ints、longs、doubles、strings、booleans、archetypes(基本上可以是任何一個遊戲物件)或是"bean"(JavaBean)。
我早先的實驗結果顯示出一個有趣的規則,非數字型的transient properties會蓋過persistent value,但數字型的會跟它的persistent value作結合(或加成)。
舉個簡單例就夠了,假設有一個persistent boolean property "我恨山怪hates-trolls"(誰都討厭呢,對吧?),而且你不小心吞下一瓶山怪之愛的藥水,於是把這個transient value設定成{"hates-trolls",
false
},它蓋過了persistent value,不是結合,它取代了原先的值。然而,property"魔法抵抗力(resist-magic)"是個整數,如果你戴上了一個增強魔法抵抗力30%的戒指,它應該(預設行為)加上你的目前數值,也就是結合天生的抵抗值以及從魔法物品跟符咒得來的增強值。
這個原則"數字型property有加成性",遍佈在整個遊戲內以及處理property的程式碼,所以我把它內建到Wyvern的property lists的lookup規則,
getIntProperty("foo")
會把"foo"的transient跟persistent(可能是繼承而來的)values加成起來然後才回傳。我實驗了不同的方式來表達transient properties,原先作法有點類似匈牙利命名法,把@字元放在transient property名字之前("@foo"),跟persistent properties放在同一個hashtable;使用"@"的一個好處是,這在XML attribute names是一個不合法的字元,這樣一來我就不可能粗心地serialize一個transient property。
最後,我的作法是將它們放在另一個(有需要才產生)表中,這樣做會讓處裡interning names時容易一點(不用每次都要判斷"@"),而且大致來說也減少要記錄的瑣事。我記不得這樣的設計相關的trade-offs(七年前的事了),所以你要自己造橋鋪路如果決定要使用transient properties。
刪除(deletion)有這麼難嗎?(混合版)
使用transient properties導致要重新思考deletion,你不能只從transient list刪掉property就算了,因為lookup演算法會去persistent list那裡找,而且你也不想把它從persistent list移除,那跟使用transient的原意相左。
解決方法跟刪除繼承來的properties差不多:你置入一個特殊的保留值到transient list,表示"NOT_PRESENT",只要這個值在那邊,那就好像說這個物件沒有那個property。
注意這意味著會有兩個相似的API:
removeTransientProperty
從transient list刪除transient property和transientlyRemoveProperty
把在persistent list的一個property暫時性地隱藏起來。持久化(Persistence)
將property lists作持久化(persisting)動作是個很大的題目;我只會觸及基本面。
先從這開始吧,XML和JSON(說到這,應該把s-expressions也列進去才完整)都是用來作serialization很棒非常合適的格式,你大概可以想像的出來怎麼做,所以我就不多說了。
文字為基礎的格式(Text-based formats)勝在有很高的可讀性、容易維護以及初期開發速度(你不用建造特別的工具來讀寫這些格式)。
考慮到效能–網路傳輸量跟硬碟空間–你可能想要設計一個壓縮型的二進位格式,有一個簡單的測試方法可以測出會不會快很多;這是一種過渡時期的方式,把你的資料都用gzip壓縮,比較前後大小是否差距很大,以及這樣做之後可不可以在速度方面看出明顯的增進。
起先Wyvern將檔案系統規劃成樹狀結構來儲存資料,可是一旦物件的數目成長到數十萬時,我必須改用資料庫(database)。
你可以使用關聯式資料庫管理系統(RDBMS),可是如果你試著把Properties pattern以relational schema表達出來,你等於走在荊棘的世界中,非常難搞,大概不是想要自己動手解決的難題。
我最後的作法是用RDBMS,將property list先以XML格式序列化,然後硬塞入一個text/clob column,區分出二十到三十個我需要query的欄位放進各自的columns。這讓我過了關,但不是個好方法。
你真的會想要的是,特別為鬆散樹狀結構而最佳化的階層式資料儲存架構:簡言之,XML資料庫。在我設計Wyvern的peristence時(大概1998年吧),所謂XML資料庫只是蒸汽軟體(vaporware),即使過了幾年後,仍然還在研究階段而且不穩定。
但今天已經不一樣了,有很多不錯的XML資料庫可供選擇,從100%免費(例Berkeley DBs)到100%昂貴(例Oracle XML)。
你可能也想看看Object databases,但有使用經驗的人除了傷痕累累之外,我還沒聽過有什麼心得。
查詢策略(Query strategies)
Query跟persistence是成對出現的好朋友:一旦你有了一堆物件存放著,你就會想要問它們問題。列出最高得分表是個好例子:所有在資料庫中的玩家資料,你想要根據某些properties來做計算。
如果你只是使用檔案系統,你免不了要用grep或在Windows上跟它類似的軟體,那會慢到受不了,千萬別這麼做。
如果你用RDBMS,而且你把property lists做serialize動作,一個object變成一個row,而且只有單一column(clob或text)的話,那麼你有(My)SQL的LIKE跟RLIKE運算子可利用,或者有同樣作用的運算子,來做全文搜尋(free-text seach)。
然而,你的property lists很可能是階層式的(例如玩家的物品欄是個袋子,有它自己的properties以及它裡面裝的一堆物件),而free-text seach沒有階層概念。所以這個方法其實只能算是快一點的grep。
Querying是使用XML資料庫最大的誘因,因為你有XPath跟XQuery當做表達語言用在XML資料上,就好像SQL用在關聯式的資料上。
因為你好運氣地活在"這個時代"(2008+)而非"那個時代"(1998),你現在有個不錯的選項,JavaScript/JSON和JQuery,我不是很瞭解,但就我所知似乎滿有前途的。
最後提一個,似乎沒辦法用在大堆資料上,除非你找到方法平行化parallelize;把所有的物件載入某台伺服器的記憶體中,然後用寫程式的方式來查閱這些物件手動找出你query的答案,雖然這需要多寫一些內部架構的程式(以及不要讓它搞當你的系統,一旦物件變得很多後),這法子的主要好處在於你有整個程式語言的功能可用,若是你的query很複雜時這滿方便的,特別是你的XPath/XQuery經驗還不強時。
回填修補(Backfills)
資料完整性(Data integrity),又名安全性(Safety),是運用Properties pattern時兩大trade-offs(另一個是performance)之一,如果沒有規劃好,你的系統就會因bug或使用者犯錯(例,打錯字)所造成的property-list corruption而受害。
有趣的是,在我以前工作過採用strong schema constraints的大公司,他們仍然會遭遇data-integrity問題,所以到底schema有多少幫助是不清楚的,有schema肯定可以幫助navigability跟performance,但沒有一個schema可以完全避開data corruption問題。
一但你的資料壞了,你就需要內行人所謂的"回填修補(backfill)":你必須檢查每份資料然後訂正錯誤的地方。有時情況很簡單,在單一column跑一個SQL update即可,有時麻煩到要寫個複雜的程式,仔細地反推這樣的資料錯誤是哪個改死的操作程序造成的。
有時候backfills只能得過且過,因為丟失的資料可能是無法復原的,你只能利用啟發式方法把傷害降到最小,不論你是如何存放資料的,不論你多小心的作副本與備份,這種事在各種規模的系統都會發生。
關於backfill,Properties pattern並沒有帶來新的東西,原先的技術都可用;你只要留心一點,使用者犯的錯(特別是打錯property names)會讓backfills更常發生,所以你應該有這樣的計畫,為系統花點時間開發方便使用的backfill infrastructure。
我應該要提一下,這有點尷尬,另外一個我用的很廣的作法,我稱之為"lazy backfill"。有時我發現一個data-corruption需要修正,但不值得我花一整天把它全部一次修好,所以我在玩家登入跟載入區域地圖時裝了一個小子系統:檢查所有property lists,當中若有被我標記(寫死在code裡面)為"bad data"的properties,那麼我就呼叫backfill相關的函式當場修復。
這很明顯是個權宜之計,也在登入跟載入區域地圖的部份強加了些微的效能負擔(用profiling大概偵測不出來),但老實說:這方法對我來說很好用,用它修正了至少20%的data-corruption問題。
型別系統(Type systems)
我先前已經這裡一點那裡一點地觸及這個議題,Eclipse的AST property lists使用了一個有趣的型別系統(type system)為每個property提供相當數量的metadata,美中不足(我認為)的是沒能讓properties可擁有它們自己的property lists。
JavaScript properties有少許固定數量的metadata,每個property有一組旗標(flags),包括
ReadOnly
(不能修改value)、Permanent
(可修改value但無法刪除key)、DontEnum
(key不會在iterators出現但可以被直接讀取),還有其他因不同實作而異的。Wyvern有它自己一套類似Java那種的typed properties,主因是我以Java來開發遊戲,而且那時可是遠在auto-boxing出現前,所以我需要一個合宜的方式來處理primitive types vs. object types。如果重頭再來一遍的話,我大概不會走那條路,我應該會想要某種metaproperties(又叫作"property attributes")的機制—每一個物件有一個metaproperty-list,或許。但我會簡化interface,將所有因為要處理primitive type而特別多出一份的函式介面has/get/set inherited/persistent/transient給擺脫掉。
我言盡於此,最後兩句話,(a) 你可以有任何強度的type system,本質上Properties Pattern並不跟它們犯沖,以及(b) 當系統圍繞著Properties Pattern為中心來設計時,Duck Typing變得相當重要,所以如果你用的語言有支援的話,會有幫助。
工具組
Wyvern有地圖編輯器(Map Editor)讓你新增與編輯物件,既然遊戲中的一切物件都是以prototype inheritance pattern為準繩所產生出來具有property lists的物件,那麼就沒辦法採用一般通行的JavaBean,因為JavaBean API(可說是或多或少應運此類問題而生的,若不看instance fields的話)使用Java reflection,而我設計的屬性並沒有個別的getters and setters。
Wyvern最後有了一個非常類似JavaBeans property editor的編輯器,甚至,還知道如何讀寫以及顯示GameObject的property lists。
撰寫屬性編輯器並不是一件龐大的工程,但卻是在評估要不要使用Properties pattern時你該考慮的因素之一;如果你希望透過圖形化視窗介面來編輯物件,那你就免不了要花一些力氣在客製使用者介面上。
困難處
我已經談過Properties pattern主要的困難點:效能(performance)與資料完整性(data integrity),以及navigability/queryability,這些都是trade-offs;在這些地方做出一些犧牲以求在使用彈性與開放式的未來延伸性獲得更大的回報,特別是給使用延伸擴充功能的人,你可能一輩子都不會認識他們。(在unit-testability還有建立雛形(prototyping)的速度這兩部分你也贏了,不過我將之視為很明顯而毋須贅言的好處。)
另外一個困難點是reversability:一旦開始投入使用Properties pattern就很難抽身而退,一旦有人開始增加properties,特別是利用programmatic construction of key names的方式,那麼若你想要重構整個系統改用instance fields and getters/setters,麻煩就大條了,所以開始用這模式之前,系統應先經過一段prototyping phase(這名字好像有點諷刺喔)來決定當系統規模變大後此模式還能勝任嗎。
延伸閱讀
我找不到太多相關資料,這裡有些值得關注的文章。
Dealing with Properties [Fowler 1997]
Prototype-based programming (Wikipedia)
Do-it-yourself Reflection [Peter Sommerlad, Marcel Ruedi] (PDF)
Prototype pattern (Wikipedia)
Self programming language (Wikipedia)
Refactoring to Adaptive Object Modeling: Property Pattern [Christopher G. Lasater]
更新訂正
2008年10月20日: 這篇文章的回應真是不錯,指出很多延伸閱讀,還告訴我一些以此模式為核心的著名範例(Atom,RDF)。謝啦各位,太棒了。
2008年10月20日: Martin Fowler寄給我一篇他在1997寫的相關文章,網址在延伸閱讀那,值得一讀。他提出一些我漏掉的重要議題:
- (靜態)依存問題The (static) dependencies problem —大體而言編譯器(compiler)不能幫助你找出properties之間的相依性,甚至不能告訴你系統使用了那些property names。他建議將可用property names造冊登錄,這是一種方法,我認真地想過要用在Wyvern,但最後我採用了一種動態追蹤與靜態分析混合型的方式,來幫我得到相依圖,到目前為止對我來說"還夠正確";特別提一點,執行時動態合成property names的發生頻率大概跟在Java需要Reflection的時候一樣(不)常見,所以大多數情況下,相依性的困難度就跟在Java一樣:"算是可處理"。
- 置換動作(Substituting actions) Fowler說,若已經有程式碼直接進行存取property,想要以一個函式來取代之是困難的,這只發生在以Java實作時(因此,我的遊戲也是),在對getters/setters有支援property-access語法的語言,例如Python、Ruby和JavaScript 1.5,不存在這個問題。
要記得的是你必須知道那是什麼才跳進去,這有幾分兒算是我這篇文章的主題。
2008年10月20日: Google的夥伴Joe Beda提到IE4原先有支援HTML elements可以有arbitrary attributes,這大大延伸了網頁開發師可有的彈性。今天沒一個瀏覽器有支援,雖然John Resig聲稱HTML 5 會修正這問題,在那之前,開發者們使用fake css classes跟hidden elements;這很混亂,事實上我刪掉了很多關於此問題的討論,但,是的,這是個問題。當你不提供Properties Pattern,使用者會自行找到很糟糕的替代方案,這很糟,不論你的直接支援作得多差,這都要比那糟糕很多。(Joe提到這會在cache造成嚴重的技術問題,所以我並不假設把這樣的支援回頭加進瀏覽器中是很簡單的。)
最後一些想法
我還沒能談及這模式所有的議題,同步化(concurrency)、存取控管(access-control)(例如,Wyvern,某些properties例如email address,只能被高階Wizards讀取)、說明文件(documentation),以及一堆其他需考慮的事項。
讓我總結一些你應該帶走的。
首先:這是一個么壽重要的模式,我稱它為"通用萬能(Universal)"設計模式因為它是(到目前為止)用來設計開放性系統的已知方法中最好的,換句話說,設計出來的系統可以存活的久而不太淘汰。
你大概不認為你正在開發那種系統,但如果你想要系統可以成長,有很多使用者,像野火一樣蔓延,那麼你就是在建造那種系統,只是還沒發覺吧了。
第二:雖然人們極少談論這個模式,它其實遍地可見,出現在strongly-typed系統例如Eclipse,出現在程式語言,出現在data-declarative語言,出現在平常用的應用軟體中,在作業系統中,甚至是strongly typed的網路協議(network protocols),雖然我今天沒談到。(有個典型範例,我知道有個使用CORBA的團隊,把一個XML parameter加進所有的CORBA API call,type system根本就沒有作用,但卻使得他們可以升級interface,而不用搞壞client端。棒極了!)
第三:此模式的效能是不錯的!或者至少是,"夠快了",潛在的最佳化技巧幾乎沒有止境,而且夠努力的話,你可以把所有的運算動作都降低到常數時間(constant time)。
最後,令人驚訝的,這模式可適用在這麼多的地方,可以用在小地方來增強已存系統中一個小小的元件,或是徹底地運用在任何事物上,或是介於這兩者之間的任何程度。你可以先玩小的,更熟悉有信心之後再大的。
Properties Pattern不"僅是" name/value pairs,雖然我們的確可以把name/value pair當做是這個模式的心臟。
如果你相信Hofstadter,Properties Pattern(使用Prototype Principle)是一種塑模的方式,與class-based modeling互補:兩者皆為我們大腦處理符號資訊的方式。
我想如果你能讀到這,你將開始看見Properties Pattern到處都是,它被內嵌在很多流行的模式與系統中,例如Dependency Injection、LDAP、DNS,當老系統掙扎著進化成更user configurable與programmable,你會看到有越來越多你喜愛的的軟體系統(而且,我希望,程式語言也是)導入這個模式,只是程度不同罷了。
再次強調:我之前沒辦法找到太多關於這模式的相關文章,如果你碰巧知道任何講的更清楚更細節的書籍、論文或文章,請留下網址在留言區!
我希望你認為這篇技術性文章能引起興趣而且對你有幫助,比起我其他一般性的文章,這篇肯定是花了更多時間精力,所以如果它不受到歡迎的話,我會很高興回到詼諧筆風。看看囉。
No comments:
Post a Comment