|
最近聽到不少人總是叨念著"細節決定成敗"這句話,頗像是每天清晨必修的那句"all money go my home"一樣。決定成敗的因素很多,為什么細節能夠成為壓倒一切的關鍵。
拿產品來說吧,產品的細節處很重要,可能客戶的取舍在毫厘之間。但這是否是事實之全部。其實客戶需要的是滿足自己的價值目標,他真的那么在意主要目標之外
的細節(甚至是刻意造作的細節)嗎。制造細節的目的只是為了制造與同類產品的不同,這是市場進入完全競爭狀況的標志。只是有意思的是,凡是中國人涌入的領
域,很快就能造成完全競爭的局面。就像我們小區里的煎餅攤,今天剛開張,明天周圍馬上冒出兩家一樣的,你兩元一份,我兩元一份加一個雞蛋,他兩元一份加一
個雞蛋再送一碗豆漿。很快,經過一場血拼,幾家都難以維系,最終撤攤了事,于是小區內就再也沒煎餅賣了。再過些時日,有人支起一個狀元餅的爐子,于是一段
新的血戰征程又開始了。最大的利益永遠源于創造。當然,創造是需要成本的,現在財大氣粗的主多半沒有創造的激情,而被創造力沖昏頭腦的家伙卻多半在社會的
底層掙扎。
項目的細節處也很重要,也許見了領導少哈一個腰,過節的時候少送了一份禮,就將項目引至黑暗的深淵。為什么世界是如此的不穩定,要受到細節的擺布。在國
內,操作多不規范,因為利益關系,兩兩聯系,因人而異的情況很多,缺乏一種外在的制度性的保障,而個人的好惡卻能在現有的體系中不斷放大。結果做人優先于
做事。
對軟件程序來說,細節會決定程序的生死,一個不經意的指針異常就能讓整個系統崩潰。但程序員也不總是戰戰兢兢,如履薄冰。一種職業的素養可以消解細節的危
險。當我們養成良好的編程習慣之后,對這樣的細節多半就視而不見了。更理想的方法是引入一種封裝機制,例如智能指針使得我們再也不用考慮AddRef和
Release的精確配對了。而java引入的則是新的世界,野指針這個細節在新世界中被消滅了,我們也不需要這方面的什么個人素質了。細節處千變萬化,
無一定之規。也許我們最需要的不要應對細節的技巧,而是能夠屏蔽和規范細節的規則。細節不是我們的目標,
一組統一簡明的游戲規則才決定著所有博弈參與者的成敗。
在我看來,軟件開發就是一個從二進制指令構造出一些高級結構的過程(from
binary chaos to artificial
intelligence)。這種構造依賴于我們控制各種結構的能力。結構化編程向我們展現了一個機械化的分解與合成的世界,但這個世界與我們的真實世界
卻差異良多。于是,面向對象編程試圖直接跳躍到真實的世界,依賴于我們對真實世界中結構的控制能力,直接對真實的結構建模。早期面向對象技術的陳述中充斥
著這種烏托邦式的理想圖景。但是這種隱喻是含混的,兩個世界的巨大差異造成了必然的轉換成本,我們只能壓縮這種成本而不可能完全拋棄它,我們必須要經歷一
系列的中間過程,經歷一個對結構問題進行深刻思考的過程。近幾年軟件技術在控制抽象結構方面有了很大進展,模板,AOP, xml,
SOA等等技術并不是傳統意義上的面向對象,而更像是對結構化編程時代的回歸。而我們對于面向對象技術的應用也不再僅僅關注于對真實世界的建模,而是將這
種技術作為一種普適的建模方法應用于軟件的方方面面。我們在編程的時候不再斤斤計較于一個Class的定義是否反映了事物的本質特征,而僅僅在意它是否有
助于我們對于程序結構的控制。這就象是神經網絡和演化計算等所謂人工智能技術,早期的興起源于一個讓人心血澎湃的理想:向生物世界億萬年的智慧學習,但近
幾年的發展則越來越明顯的表現為一種對數學的回歸。
EJB和JSP
Tag都是很好的技術,它們的最終形態都使我們擁有了更強的結構控制能力。但問題是,它們的構造成本過高,而限制了其意義的進一步引申。輕量級容器的興起
才真正開發了AOP技術的潛力,使得Meta Object
Protocol的思想得到真正的發揮。witrix平臺中的tpl模板技術將自定義tag的構建成本降到了最低:沒有配置文件,使用tpl自身來構造
tag。這些努力使得tpl技術不再是象是一個幫助庫,而成為一種獨立的語言。應用tpl模板,我們可以隨意的將小段html文本封裝為有意義的tag
(這在jsp tag中是被明確反對的實踐), 從而獲得一種嶄新的抽象能力。實際上,我認為JSF基于jsp
tag技術,在基本結構的構造方面成本過高,無論它的IDE怎樣發展,最終只能成為一種界面庫而不會是真正引領未來方向的技術。只有突破成本閾值,才能發
展出新的天地。
代碼復用包括兩個方面:概念復用和實現復用。這兩者在C++的虛擬函數設計中是合二為一的,結果概念上的模糊往往造成繼承機制的濫用。為了復用我們往往在
基類中塞入過多的職責,并在程序中制造了過多的層次。java的interface是純粹的概念復用機制,實現方面的復用我們一般通過Impls類或者
Utils類來進行,即將代碼片斷寫為靜態函數。一般應該避免在類中寫特別多的幫助性成員函數,因為成員函數隱含的通過成員變量相關著,比靜態函數要更加
難以控制。
類是一個整體的概念,整體概念失效了,類也就不存在了。從這一點上來說,它未必是比靜態函數更加穩定。概念與實現是兩個不同層面的東西。實際上它們一般也
是多對多的關系。同一個概念可能換用多種不同的實現,而同一段功能代碼也可能在多個類中使用。
代碼復用的意義不僅僅在于減少工作量。實際上復用是對軟件的一種真正的檢驗,而測試僅僅是一種模擬的檢驗而已。每一次復用都是對代碼的一次拷問。在不斷使
用中感受到不同使用環境中的各種壓力,才能實現概念的不斷精化并確保實現的正確性。
我們開發程序的目的是為了完成業務功能, 理想的情況下程序中的每一條語句都應該是與業務直接相關的,
例如程序中不應該出現連接數據庫, 讀取某個字段等純技術性的操作, 而應該是得到用戶A的基本信息等具有業務含義的操作. dao(data
access object)層存在的意義在于將與數據持久化相關的函數調用剝離出去, 提供一個具有業務含義的封裝層. 原則上說,
dao層與utils等幫助類的功能非常類似, 只是更加復雜一些, 需要依賴更多的對象(如DataSource,
SessionFactory)等. 如果不需要在程序中屏蔽我們對于特定數據持久層技術的依賴, 例如屏蔽對于Hibernate的依賴,
在dao層我們沒有必要采用接口設計. 一些簡單的情況下我們甚至可以取消整個dao層, 而直接調用封裝好的一些通用dao操作函數,
或者調用通用的EntityDao類等.
程序開發的過程應該是從業務對象層開始的, 并逐步將純技術性的函數調用剝離到外部的幫助類中,
同時我們會逐漸發現一些業務操作的特定組合也具有明確的含義, 為了調用的方便, 我們會把它們逐步補充到service層中. 在一般的應用中,
業務邏輯很難穩定到可以抽象出接口的地步, 即一個service接口不會對應于兩個不同的實現, 在這種情況下使用接口往往也是沒有必要的.
在使用spring的情況下原則上應該避免使用getBean的調用方式, 應該盡量通過注入來獲得依賴對象, 但有時我們難免需要直接獲取業務對象, 在不使用接口的情況下可以采用如下方式
class TaskService{
public static TaskService getInstance(){
return (TaskService)BeanLoader.getBean(TaskService.class);
}
}
在程序中我們可以直接使用TaskService.getInstance()來得到TaskService對象.通過命名規范的約定,
我們可以從類名推導出spring配置文件中的對象名, 因而不需要使用一個額外的硬編碼字符串名.
jsp模型為web程序提供了page/request/session/application這四個基礎性的變量域.
這種變量域的劃分很大程度上是純技術性的, 與我們的業務應用中需要的scope支持相去甚遠. 當我們把業務對象的生命周期映射到這些變量域的時候,
經常出現不適應的情況. 例如我們可能被迫選擇把與某項業務相關的所有數據放置在session中并在各處硬編碼一些資源清理代碼.
為了實現與愈來愈復雜的應用開發的契合, 我們需要能夠在程序中定義與應用相關的變量域并實現對這些變量域的管理,
即我們需要一種自定義scope的支持而不是使用幾個固定的scope.
JBoss的Seam項目 http://www.jboss.com/products/seam 中引入了一種所謂declarative application state management的機制
http://blog.hibernate.org/cgi-bin/blosxom.cgi/Gavin%20King/components.html,
其中的關鍵是增加了business process和conversation這兩個應用直接相關的scope, 它們都是可以根據需要自由創建的.
business process context使用jBPM支持long running的狀態保持. 而conversation
context是對session使用的一種精細化, 與beehive項目中的page flow所需的scope支持非常類似 http://beehive.apache.org/docs/1.0m1/pageflow/pageflow_overview.html. 但目前seam中的scope支持仍是非常原始的, 不支持嵌套的context, 這意味著對于復雜應用尚無控制和管理能力.
在無侵入性的前臺頁面控件設計方案中, 我們需要一種簡便的方法迅速定位頁面中的某一節點(dom
node). 使用xpath是非常誘人的一個技術選擇, 但是在實際使用中, 我們卻發現xpath并不是那么方便. xpath的能力非常強大,
它支持絕對定位, 例如//input[@id='3'], 也支持相對定位, 例如 ./input[0], 甚至支持根據節點內容定位,
例如//a[contains(., 'partial text')].
問題是在一個復雜的界面控件中, html節點本身的結構與界面展現結構并不是一致的,例如一個特定效果的邊框可能需要多個html元素互相嵌套才能夠實現, 因此xpath的相對路徑選擇能力往往派不上用場(除非是提供 http://www.backbase.com/那
樣的界面抽象層), 而根據內容定位的方式過于靈活, 難以維護一個穩定的概念層. 相比較而言,
css的選擇符所提供的節點定位方式要比xpath更加簡單直觀, 它的適用性也早已在大量的實踐中得到了證實.
基于css選擇符實現behaviour機制是一種更加可行的方案. 參見 http://prototype.conio.net/
在軟件設計中分層應該是越少越好, 過度分解一般都是有害的.
雖然說復雜的事物分解之后一般可以得到一些較簡單的組成成分, 但這并不是必然有用的. 分析學成功的關鍵在于分解之后的組分能夠出現大量重疊的情況,
參見軟件中的分析學 http://canonical.blogdriver.com/canonical/555330.html
當分解到一定程度之后我們未必能夠發現可以重用的部分. 而且即使分解后系統中所有的基元都是簡單的,
也并不意味著整個系統就是簡單的. 生物遺傳密碼由四種堿基構成, 但是我們理解了ATGC決不意味著我們理解了生命. 在理論上存在一種連接主義,
認為真正的復雜性蘊含在元素之間的關系之中而不在于元素自身的復雜性.
例如神經網絡的研究中可調的參數多半是神經元之間的連接權重,而不是神經元本身的模型參數.
在理想中, 我們希望系統的功能劃分能夠涇渭分明,
一個類負責一個獨立的功能實現或者一個功能側面(aspect). 但是這只是一種烏托邦式的理想. 在物理的世界中,
我們未必能夠為每一個我們思維中獨立的概念找到一個穩定的物質載體. 就像是水中的漩渦, 看上去它也有固定的形狀, 一定的穩定存在時間,
但是你無法說是哪些水分子參與了漩渦的構成, 實際上波的傳播掠過了整個水面. 同樣, 在軟件中功能的歸屬和聚合很多時候并不是那么穩定的.
敏捷(Agile)開發的靈魂是演化(evolution),其具體的過程表現為迭代(iteration),迭代的每一步就是重構
(refactor),而單元測試(unit test)與持續集成(continuous
integration)模擬了程序生存的環境(約束),是merciless
refactoring的技術保障。從數學上我們知道迭代總有個收斂問題。一些重型方法將變化(無論是正方向還是反方向的)等價于風險,而傾向于消除開發
中的不確定性,其中的迭代是趨于迅速收斂的。敏捷的迭代是開放式的,強調擁抱變化。敏捷編程排斥過度設計,除了過度設計會增加成本之外,另一個原因就是過
度設計會阻礙重構,阻礙變化。敏捷的目標不是僵化的穩定性而是靈活的適應性。當然敏捷迭代本身并不能保證系統持久的適應性,即使是自然界中的迭代和演化,
失敗的案例也是比比皆是。大量的生物物種在經歷了歷史的輝煌之后最終仍然難免被歲月所埋葬。
在哲學上,一個悖論式說法是有存在于無中,或者說簡單才能更復雜。杯子是空的,所以能包容萬物。現在什么都沒做,將來才能根據需要決定如何去做。所謂魚與
熊掌不可兼得,一旦做出了選擇,可能意味著必須放棄將來進行其他選擇的機會。簡單的目的不僅僅是為了最快的完成當前的任務,而且要為將來保留變化的可能。
過分強調目的性,我想是違背了演化的本質。高手過招,最忌把招數用老。我們所要做的是盡量推遲決定的時刻,并切實的保證自己隨時擁有選擇的權利。
多樣性是在演化中生存的關鍵。但多樣性不是后天的。生物學的實驗證實,物種的變異并不是環境變化后發生的,而是始終存在著并隱藏著,環境僅僅起了檢選和倍
增的作用。適應性的系統總要允許一定的灰色地帶,有時do something for nothing.
Agile批評過度設計(over-engineering)的聲音很大,但對于設計不足(under-engineering)同樣是持堅決的否定態度
的。修改過度設計的應用比修改設計不足的程序要容易的多。因為簡化的途徑是明確的,而走向復雜的途徑卻往往是難以控制的。Refactoring To
Patterns試圖引入一些經驗,但這些可預見的調整多半只在細節處,其影響是局部的。一個復雜性低層次的設計要支持一個復雜性高的應用,所需的代碼量
不是線性的堆砌,而是幾何級數式的增長,重構的時候需要做出的改變往往也是影響全局的。而事實上,設計不足是比過度設計更加常見的情況。真實的情況也許
是,在真正需要我們做出創造性設計的地方我們因為無知和無能而設計不足,而在那些渴求簡單的地方,我們卻自詡為先知而加上很多華麗的設計來維護虛幻的可擴
展性。這里的度是很難把握的。高段位的棋手可以比低段位的棋手預見到更多的步數,而一個優秀的軟件架構師也需要比普通的程序員更早的預見到系統發展的障
礙。在我們明確可預見的范圍內,當然是要把所有的設計做好,而在我們思維的邊界處,"行"就變得比"思"重要了。
大談"over-engineering"的主多半都有著豐富的過度設計的經驗,千萬不要把他們回顧時的話語當成是普遍的真理。所謂大巧若拙,精煉的小詩
可比長篇大論難寫的多了。有時采用一種簡單的處理方式,是因為我們感覺到它不會成為障礙,雖然此時并沒有明確的設計過程。你必須有能力進行過度設計,才能
真正理解簡單設計的精妙之處
循環結構是imperative
language的重要組成部分,一般也是程序中比較難以理解的部分。特別是沒有軟件技術背景的朋友,明顯對于循環的理解力較弱。在Von
Neumann體系結構中,賦值語句是必須的,因而引出了存儲概念,也引入了時間概念,因為我們可以區分出賦值前和賦值后的時刻。引入時間之后,本質性的
影響是程序串行化,而強迫我們從并行思考轉入串行處理。很多時候這是一種不自然的情況,在我們的自然思維中,我們看到的或想到的也許只是一組靜態結構,但
在程序中表達的時候卻往往不可避免的需要引入一個動態過程。而我們控制動態結構的能力總是不足的。最近對于函數式語言及處理風格的越來越強烈的要求可能也
從側面反映了大家對這種結構失配的不滿。 但是串行思維毫無疑問也是我們正常思維模式的一部分(當然這種思維模式在多大程度上是因為Von
Neumann
體系造成的,也是個很有趣的問題)。例如在頁面渲染的時候,我們可能希望預先把所有用到的數據都轉載到內存中,賦予不同的變量名,然后在頁面模板中我們只
要知道如何把這些數據變量表現出來就可以了。先做完A再做B,這是分層的思想,也是典型的串行思維。而基于數據進行處理,也是Von
Nenuman體系的基本思想。但是如果處處要求預先計算并賦值,往往增加了很多額外的步驟(glue
code),并且增大了對內存(計算空間)的需求。分層之后,還存在著一個各個層次之間結構匹配的維護問題。 面向對象在結構表達方面是一種
巨大的進步。經過多年的發展,我們在表達靜態結構關系方面已經是駕輕就熟了。通過屬性關聯,我們可以沿著對象圖進行結構遍歷。如果使用成員函數,在這種遍
歷過程中還可以包容更多的動態特性。而在數據持久化方面,ORM的盛行也在一定程度上證明了對象圖的有效性。使用對象圖可以大大降低對賦值語句的需求,減
輕了明確建模的壓力(每一次賦值都要求著一個明確的變量名,一個概念),也緩解了Von Neuman體系結構的束縛。例如,我們不再需要 var user = loadUser(userId); var userOrgnization = loadOrgnization(user.orgId); var userOrgnizationName = userOrgnization.name 而是直接使用 user.orgnization.name
目前面向對象所表達的大多數結構還是基于數據語義的,而我們對于函數等高階結構的控制能力仍然較弱。設計模式在這方面提供了一些經驗,但還是遠遠不夠的。
在我們經驗不多的時候,我們需要依賴于明確的實體數據,而在我們的理解逐步深入之后我們就可以通過Visitor,
Iterator等模式支撐起整個結構。高階結構比低階結構難以控制,一方面是因為動態性本身比靜態性難以理解,另一方面函數對信息的使用和流動是一種主
動約束,如果約束的不正確,會造成結構的失效。而數據的使用是完全開放的,很多決定都可以延遲到使用時刻決定。當然,開放性帶來的問題也早就眾所周知了:
不受限制的使用將導致無法控制的困境。在基礎的數據層封裝方面,一般我并不提倡大量使用domain
model似的具有豐富語義的數據對象。因為數據是共享的,應該存在一個開放的數據層,在其上可以建立業務對象。混雜在一起會限制系統的演化。
|