論面向組合子程序設計方法 之 新約

每個小孩剛開始走路的時候都是跌跌撞撞的。
我們不自量力,妄圖踩着上帝的步伐前進。結果就是這麼幾個簡單的象白開水似的類。失望嗎?是不是造物試圖模仿造物主本身就是一種可笑的狂妄呢?

別急,讓我們失聲痛哭之前先看看我們這幾步走的是不是一錢不值。
[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串聯起來。

Logger std_logger = writer(new PrintWriter(System.err););;
std_logger = ignore(ERROR, std_logger);;
logger = sequence(std_logger, logger);;



[b]6。標準輸出上的東西只要通知我們出錯了就行,大概不需要詳細的stack trace,所以exception stack trace可以打印到文件中,而屏幕上有個簡短的exception的message就夠了。[/b]

這裏需要對std_logger稍微改寫一下:
PrintWriter out = new PrintWriter(System.err);;
std_logger = new ErrorMessageLogger(out, writer(out););;
std_logger = ignore(ERROR, std_logger, nop(););;

用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是一個非常簡單的組合子,下面如果大家對這個例子有疑問,我會盡力解答。

然後,我們會進軍下一個例子。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章