天天看點

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

每個小孩剛開始走路的時候都是跌跌撞撞的。

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

别急,讓我們失聲痛哭之前先看看我們這幾步走的是不是一錢不值。

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

然後,我們會進軍下一個例子。

繼續閱讀