每個小孩剛開始走路的時候都是跌跌撞撞的。
我們不自量力,妄圖踩着上帝的步伐前進。結果就是這麼幾個簡單的象白開水似的類。失望嗎?是不是造物試圖模仿造物主本身就是一種可笑的狂妄呢?
别急,讓我們失聲痛哭之前先看看我們這幾步走的是不是一錢不值。
[list]
[b]1。logger可以把資訊列印到log檔案中。[/b]
容易,直接建立一個WriterLogger就好了。
[b]
2。不同的重要程度的資訊也許可以列印到不同的檔案中?象websphere,有error.log, info.log等。
如果這樣,那麼什麼重要程度的資訊進入error.log,什麼進入warning.log,什麼進入info.log也需要決定。[/b]
不同的檔案嗎?好辦啊。就是不同的PrintWriter對象了。
Logger err_log = writer(err_writer);;
Logger warning_log = writer(warning_writer);;
Logger info_log = writer(info_writer);;
各個檔案記錄不同重要程度的資訊是麼?
err_log = filter(ERROR, err_log, nop(););;
warning_log = filter(WARNING, warning_log, nop(););;
info_log = filter(INFO, info_log, nop(););;
最終把三個不同的logger串聯起來就是了:
Logger logger = sequence(err_log, warning_log, info_log);;
[b]3。也許可以象ant一樣把所有的資訊都列印到一個檔案中。[/b]
這就更簡單了,就一個是WriterLogger。
[b]4。每條logging資訊是否要同時列印目前的系統時間?也是一個需要抉擇的問題。[/b]
拿不定主意是麼?沒關系,想好了再告訴我。
反正,如果你需要系統時間,我隻需要
logger = timestamp(logger);;
[b]5。不僅僅是log檔案,我們還希望能夠在标準錯誤輸出上直接看見錯誤,普通的資訊可以列印到log檔案中,對錯誤資訊,我們希望log檔案和标準輸出上都有。[/b]
可以建立針對标準輸出的Logger,然後和列印log 檔案的logger串聯起來。
[b]6。标準輸出上的東西隻要通知我們出錯了就行,大概不需要詳細的stack trace,是以exception stack trace可以列印到檔案中,而螢幕上有個簡短的exception的message就夠了。[/b]
這裡需要對std_logger稍微改寫一下:
用ErrorMessageLogger來改寫對異常的log邏輯。
[b]7。warning似乎也應該輸出到螢幕上。[/b]
好啊。就是把ignore函數裡面的ERROR換成WARNING就好了
std_logger = ignore(WARNING, std_logger, nop(););;
[b]8。不管檔案裡面是否要列印目前系統時間,螢幕上應該可以選擇不要列印時間。[/b]
對std_logger不掉用timestamp就是了。
[b]
9。客戶應該可以通過指令行來決定log檔案的名字。[/b]
這條和logger組合子其實沒什麼關系。
[b]10。客戶可以通過指令行來決定log的細節程度,比如,我們隻關心info一樣級别的資訊,至于debug, verbose的資訊,對不起,不要列印。[/b]
生成那個最終使用的Logger對象的時候,再ignore一下就行了:
logger = ignore(some_level, logger, nop(););;
[b]
11。neptune生成的是一些Command對象,這些對象運作的時候如果出現exception,這些exception會帶有execution trace,這個execution trace可以告訴我們每個調用棧上的Command對象在原始的neptune檔案中的位置(行号)。
這種exception叫做NeptuneException,它有一個printExecutionTrace(PrintWriter)的方法來列印execution trace。
是以,對應NeptuneException,我們就不僅僅是printStackTrace()了,而是要在printStackTrace()之前調用printExecutionTrace()。[/b]
NeptuneExceptionLogger就是給這個準備的呀。
[b]
12。neptune使用的是jaskell語言,如果jaskell腳本運作失敗,一個EvaluationException會被抛出,這個類有一個printEvaluationTrace(PrintWriter)的方法來列印evaluation trace,這個trace用來告訴我們每個jaskell的表達式在腳本檔案中的位置。
是以,對應EvaluationException,我們要在printStackTrace()之前,調用printEvaluationTrace()。[/b]
JaskellExceptionLogger
[b]
13。execution trace和evaluation trace應該被列印到螢幕上和log檔案兩個地方。[/b]
這就是說,上面兩個Logger應該被應用到std_logger和logger兩個對象中。
[b]
14。因為printExecutionTrace()和printEvaluationTrace()本身已經列印了這個異常的getMessage(),是以,對這兩種異常,我們就不再象對待其它種類的異常那樣在螢幕上列印getMessage()了,以免重複。 [/b]
就是說,一旦一個exception被發現是NeptuneException,那麼ErrorMessageLogger就要被跳過了。
final Logger err_logger = new ErrorMessageLogger(writer);;
final Logger jaskell_logger = new JaskellExceptionLogger(writer, err_logger);;
final Logger neptune_logger = new NeptuneExceptionLogger(writer, jaskell_logger);;
return neptune_logger;
這個neptune_logger先判斷異常是不是NeptuneException,如果是,直接處理,否則,傳遞給jaskell_logger。jaskell_logger繼續判斷,如果不是它感興趣的,再傳遞給ErrorMessageLogger來做最後的預設處理。
[b]
15。也許還有一些暫時沒有想到的需求, 比如不是寫入log檔案,而是畫個圖之類的變态要求[/b]。
放馬過來吧。看我們的組合子能不能對付。
[/list:u]
很驚訝地發現,就這麼幾個小兒科似的積木,就似乎全部解決了曾讓我們煩惱的這些需求?
為了給大家一個完整的印象,下面是我實際項目中使用這些組合子應對上面這些需求的代碼:
public class StreamLogger {
private final OutputStream out;
/**
* To create a StreamLogger object.
* @param out the OutputStream object that the log message should go to.
*/
public StreamLogger(OutputStream out); {
this.out = out;
}
/**
* To get the OutputStream that the log messages should go to.
*/
public OutputStream getStream(); {
return out;
}
private static Logger getBaseLogger(PrintWriter writer);{
final Logger nop = new NopLogger();;
final Logger base = Loggers.logger(writer);;
final Logger neptune_logger = new NeptuneExceptionLogger(writer, nop);;
final Logger jaskell_logger = new JaskellExceptionLogger(writer, nop);;
return Loggers.sequence(
new Logger[]{neptune_logger, jaskell_logger, base}
);;
}
private static Logger getEchoLogger(PrintWriter writer);{
return new ErrorMessageLogger(writer);;
}
private static Logger getErrorLogger(PrintWriter writer);{
final Logger err_logger = new ErrorMessageLogger(writer);;
final Logger jaskell_logger = new JaskellExceptionLogger(writer, err_logger);;
final Logger neptune_logger = new NeptuneExceptionLogger(writer, jaskell_logger);;
return neptune_logger;
}
/**
* Get the Logger instance.
* @param min_level the minimal critical level for a log message to show up in the log.
* @return the Logger instance.
*/
public Logger getDefaultLogger(int min_level);{
final PrintWriter writer = new PrintWriter(out, true);;
final PrintWriter err = new PrintWriter(System.err, true);;
final PrintWriter warn = new PrintWriter(System.out, true);;
final Logger all = Loggers.sequence(new Logger[]{
Loggers.ignore(getErrorLogger(err);, Logger.ERROR);,
Loggers.filter(getEchoLogger(warn);, Logger.WARNING);,
getBaseLogger(writer);
}
);;
return Loggers.ignore(all, min_level);;
}
}
為了偷懶,我沒有用配置檔案,就是把這些政策寫死進java了。好在上面的代碼非常declarative,改起來也很容易。
沒習慣讀代碼的朋友。這裡奉勸還是讀一讀吧。很多時候,代碼才是說明問題的最好手段。我相信,隻有讀了代碼,你才能真正嘗到CO的味道。
有朋友問,你這個東西和decorator pattern有什麼差別呀?乍看上去,還真是長得差不多呢。不都是往現有的某個對象上面附加一些功能嗎?
也許是把。我不知道象SequenceLogger這種接受一個數組的,是否也叫做對數組的decorator;也不知道IgnoreLogger接受了兩個Logger對象,這個decorator究竟是修飾誰的呢?
其實,叫什麼名字無所謂。我這裡要講的,是一種從基本粒子推演組合的思路。形式上它也許碰巧象decorator, 象ioc。但是正如workinghard所說(這句話深得我心),思路的切入點不同。
如果你仔細看上面的代碼,也許你會有所感覺:對Logger的千奇百怪的組合本身已經有點象一個程式代碼了。
如果用僞碼表示:
all_logger = ignore messages below ERROR for getErrorLogger(err);;
filter messages except WARNING for getEchoLogger(warn);;
baseBaseLogger(writer);;
ignore messages below lvl for all_logger;
當組合子越來越多,需求越來越複雜,這個組合就會越來越象個程式。
這裡,實際上,(至少我希望如此),我們的思維已經從列印什麼什麼東西上升為在Logger這個級别的組裝了。
這也就是所謂higher order logic的意思。
所謂combinator-oriented,在這裡,就展現為系統性地在高階邏輯的層面上考慮問題,而不是如decorator那樣的零敲碎打的幾個功能子產品。
大量的需求邏輯被以聲明式的方式在高階邏輯中實作,而基本的組合子隻負責實作原字操作。
當然,缺點也是明顯的,對此我不諱言:
[list]高階邏輯不容易調試,當我們使用一個組合了十幾層的複雜的Logger對象的時候(真正用了co這種情況不少見),一旦出現bug,跟蹤的時候我們就會發現象是陷入了一個迷宮,從一個組合子跟蹤進入另一個組合子,繞來繞去。
另外,異常的stack trace也無法反映組合層次關系,造成錯誤定位麻煩。[/list:u]
這也許不是co本身的問題,而是因為java這種oo語言對co沒有提供語言上的支援。但是無論如何,這對在java中大規模使用co造成了障礙。
也許你還無法了解。平時我們在java中用那麼幾個decorator,本身非常簡單,是以debug, trace都不是問題。可是,一旦oriented起來,情況就不同了。街上有兩輛車和成千上萬輛車,對交通造成的壓力截然不同。
還有朋友,對co如何把握有疑問。難道co就是瞎貓碰死耗子麼?
其實,無論co還是oo,對設計者都是有一定要求的。
oo要求你了解需求,并且經驗豐富,也要有一點點運氣。
co也要求經驗,這個經驗是設計組合子系統的經驗。什麼樣的組合子是好的?怎麼才算是足夠簡單?什麼組合規則是合理的?等等,這些,也有規律可循,就像oo的各種模式一樣。同時,也可以refactor。畢竟,怎麼簡單地想問題比怎麼分解複雜問題可能還是要容易掌握一點。
不過,co對經驗的要求稍微小一點,但是對數學和邏輯的基本功要求則多了一點。有了一些數學和邏輯方面的基本功,那麼設計組合子就會輕松的多。
co也要有一點點運氣。是以遇到你怎麼想也想不明白的情況,就别死抗啦,也許這個問題就抽象不出組合子來,或者以我們普通人的智慧抽象不出來。
co是銀彈嗎?當然不是,至少以我的功力沒有這種自信。
遇到複雜的問題我也是先分解需求,面向接口的。隻有問題的規模被控制在了一定的範圍,我才會試圖用co來解決問題。靠着對co的一些經驗和感覺,一旦發現了可以組合子化的概念,成果會非常顯著。
而且,co和oo關注的層面也不同。co是針對一個單獨的概念(這點倒有點象ao),一點一點演繹,構成一個可以任意複雜的系統,一個好的co也會大大減少需要引入的概念數。而oo是處理大量互相或者有聯系,或者沒有聯系的概念,研究怎麼樣把一個看上去複雜的系統的複雜度控制住。是以兩者并不是互相排斥的。自頂向下,自底向上,也許還是兩手一起抓更好些。
這段時間應用co做了幾個軟體後,感覺co最擅長的領域是:
問題域有比較少的概念,概念簡明清晰(比如logger, predicate, factory,command),但是對概念的實作要求非常靈活的場合。
這種時候,用oo就有無處下嘴之感。畢竟概念已經分解了,職責也清楚,就是怎麼提供一個可以靈活适應變化的體系,而對這個,oo并沒有提供一個方法論。基本上就是用po的方法吭哧吭哧硬做。而co此時就可以大展用武之地。彌補這個空隙。
看過聖經的,也許有感覺,舊約裡面的上帝嚴厲,經典,就像是一個純粹的fp語言,每個程式員都被迫按照函數式,組合子的方式思考問題,你感覺困難?那是你不夠虔誠。你們人不合我的心意,我淹死你們!
而新約裡面,明顯添加了很多人情味兒。上帝通過自己的兒子和人們和解了。既然淹死一波,再來一波還是這樣,那麼是不是說大家應該各讓一步呢?
co和oo,既然各自都不能宣稱自己就是銀彈,那麼為什麼不能拉起手來呢?我們不是神,不可能真正按照神的方式用基本粒子組合演化世界,是以就别象清教徒一樣苦苦追求不可能的目标了。但是,上帝的組合之道畢竟相當巧妙,在有些時候荊棘裡面出現火焰的時候,我們又何必固執地拒絕造物主的好意呢?禮貌地說一聲:“謝了!老大”。不是皆大歡喜?
這一節我們繼續講解了這個logging的例子。實際上,logging是一個非常簡單的組合子,下面如果大家對這個例子有疑問,我會盡力解答。
然後,我們會進軍下一個例子。