論面向組合子程序設計方法 之 燃燒的荊棘

唧唧歪歪一大堆。肯定早有人不耐煩了。
"你丫還有沒有點實在的東西呀?"
要是我,可能也早就忍不住了。

好,好。我其實並沒有忘記前面說的那個logging的例子。賣了這麼長時間的關子,除了有想形而上的虛榮心外,也是想給大家多一點時間來嚼一下這個例子,讓熟悉OO的朋友肚子裏面多少有個腹稿。

下面,我來繼續上回書說到的這個logging。

前面列舉了那麼一大堆亂七八糟的需求,不知道是不是有人和我一樣看着這些繁雜的條目鬧心。我在做的時候其實只想了五六條需求,就已經開始煩了。何況還有一些暫時不知道如何抉擇的幾個疑問點。最初Kiss出來的那個logger實現明顯不能用了。refactor的價值也有限。

怎麼辦?智慧果也許給了我們智慧,但是這智慧畢竟是有限的。側頭看去,驀然看見荊棘裏跳動的火焰,那是上帝的光芒麼?

好,既然這條路看來比較坎坷,換條路試試看也未嘗不可。走這條路的好處在於,我們可以暫時不用理會這些討厭的“需求”了。CO這個方法論不是以需求爲中心,而是從底層的組合子出發,就像小孩子擺弄積木,我們只管發揮自己的想象力,看看我們能擺弄出些什麼東西來就好了。


如果一切順利,我們希望能夠跨越瀚海高山,走到流着奶和蜜的土地。所有這些亂七八糟的需求,我們希望他們能夠被簡單地配置,或者至少能夠通過聲明式的方法來“聲明”,而不是通過過程式的方法來“實現”。

出發之前,鼓舞以下士氣先。
前面charon說,這種東西類似公理系統。確實,非常非常類似。不過,門檻並不像這個名字聽來那麼嚇人的高。我們並不需要一下子就完全把完備性考慮清楚,我們不是上帝,沒有這麼大本事。

類似於xp + refactoring,我們的組合子的建造也一樣可以摸着石頭過河,走一步看一步的。

比如,假如我的Logger接口定義的時候沒有print函數而只有println,那麼後面我們會發現在製造打印當前時間的組合子的時候會出現麻煩。不過,沒關係,等到意識到我們需要一個print()函數的時候,回過頭來refactor也是可以的。


爲了說明這點,我們現在假設Logger接口是這樣:

interface Logger{
println(int level, String msg);;
printException(Throwable e);;
}


好,路漫漫而修遠兮,我們就這樣按照上帝的昭示上路吧,雖然前路雲封霧鎖。


首先,任何組合子系統的建立,都起步於最最基本最最簡單的組合子。然後再一步步組合。

比如,布爾代數必然起步於true和false;自然數起步於0,1,然後就可以推演出整個系統。

那麼,對Logger來說,有0和1嗎?

有的。

0就是一個什麼都不做的Logger。

class NopLogger implements Logger{
public void println(int level, String msg);{}
public void printException(Throwable e);{}
}


(注,不要小看這個什麼都不做的Logger,它的作用大着呢。你能想象一個沒有0的整數系統嗎?)

什麼是1呢?一個直接向文件中打印信息的應該就是一個1了。
僞碼如下:

class FileLogger implements Logger{
public void println(int level, String msg);{
write msg to log file.
}
public void printException(Throwable e);{
write exceptioon to log file.
}
}

那麼,怎麼實現這個類呢?

這裏,我們遇到一個東西要抉擇,文件打開是要關閉的。那麼誰負責打開文件?誰負責關閉?是否要在構造函數中new FileOutputStream()?,然後再提供一個close()函數來close這個stream?

答案是:不要。

無論是ioc也好,還是CO的組合方法也好,一個原則就是要儘量保持事情簡單。那麼,什麼最簡單?我們此處真正需要的是什麼?一個file麼?
不,其實一個PrintWriter就足夠了。
當然,最終文件必須還是要通過代碼打開,關閉。但是,既然反正也要打開文件,我們這裏不去管也不會增加什麼工作量,對麼?那麼爲什麼不樂得清閒呢?
“先天下之憂而憂”這種思想是最最要不得的。


於是,最簡單的1應該是這樣:

class WriterLogger implements Logger{
private final PrintWriter writer;
public void println(int level, String msg);{
writer.println(msg);;
}
public void printException(Throwable e);{
e.printStackTrace(writer);;
}
WriterLogger(PrintWriter writer);{
this.writer = writer;
}
}


“哈!還說什麼CO。你這不是剽竊ioc pattern嗎?”
完了,被抓個現形。還真是ioc pattern。其實,世界上本來沒有什麼新東西,所謂“方法論”,本來是一種思考方法,而不是說形式上必然和別人不同。不過,說我剽竊,我就剽竊吧。


好。最簡單的原子寫出來了。希望到現在,你還是會覺得:“介,介有什麼呀?”


下面來看組合規則。都可以弄些什麼組合規則呢?讓我來掰着腳指頭數一數。
有一點作爲thumb of rule需要注意的:每個組合規則儘量簡單,並且[b]只做一件事[/b]。

1。順序。把若干個logger對象順序寫一遍,自然是一種組合方式。

class SequenceLogger implements Logger{
public void println(int level, String msg);{
foreach(l: loggers);{
l.println(level, msg);;
}
}
public void printException(Throwable e);{
foreach(l:loggers);{
l.printException(e);;
}
}
private final Logger[] loggers;
SequenceLogger(Logger[] ls);{
this.loggers = ls;
}
}



2。邏輯分支。當消息的重要程度等於某一個級別的時候,寫logger1,否則寫logger2。
class FilteredLogger implements Logger{
private final Logger logger1;
private final Logger logger2;
private final int lvl;
public void println(int level, String msg);{
if(level==lvl);logger1.println(level, msg);;
else logger2.println(level, msg);;
}
public void printException(Throwable e);{
if(lvl==ERROR); logger1.printException(e);;
else logger2.printException(e);;
}
}

爲了簡潔,下面的例子我就不寫構造函數了。


3。忽略。當消息的重要程度大於等於某一個值的時候,我們寫入logger1,否則寫入logger2。
class IgnoringLogger implements Logger{
private final Logger logger1;
private final Logger logger2;
private final int lvl;
public void println(int level, String msg);{
if(level>=lvl);logger1.println(level, msg);;
else logger2.println(level, msg);;
}
public void printException(Throwable e);{
if(lvl<=ERROR); logger1.printException(e);;
else logger2.printException(e);;
}
}


其實,2和3本來可以統一成一個組合規則的,都是根據消息重要程度來判斷logger走向的。如果我用的是一個函數式語言,我會毫不猶豫地用一個predicate函數來做這個抽象。
可惜,java中如果我要這麼做,就要引入一個額外的interface,還要多做兩個adapter來處理ignore和filter,就爲了節省一個類和幾行代碼,這樣做感覺不是很值得,所以就keep it stupid了。


對了。我說“CO不看需求”了麼?如果說了,對不起,我撒謊了。
CO確實不是一個以需求爲中心的方法論。它有自己的組合系統要關心。
但是需求也仍然在CO中有反映。我們前面提出的那些需求比如打印系統時間什麼的不會被無中生有地實現。
只不過,在CO裏面,這些需求不再影響系統架構,而是被實現爲一個一個獨立的組合子。同時,我們拋開需求之間複雜的邏輯關係,上來直奔主題而去就好了。

4。對exception直接打印getMessage()。

class ErrorMessageLogger implements Logger{
private final PrintWriter out;
private final Logger logger;
public void println(int level, String msg);{
logger.println(level, msg);;
}
public void printException(Throwable e);{
out.println(e.getMessage(););;
}
}


5。對了,該處理NeptuneException了。如果一個exception是NeptuneException,打印execution trace。

class NeptuneExceptionLogger implements Logger{
private final PrintWriter out;
private final Logger logger;
public void println(int level, String msg);{
logger.println(level, msg);;
}
public void printException(Throwable e);{
if(e instanceof NeptuneException);{
((NeptuneException);e);.printExecutionTrace(out);;
}
else{
logger.printException(e);;
}
}
}


6。對EvaluationException,照貓畫虎。
class JaskellExceptionLogger implements Logger{
private final PrintWriter out;
private final Logger logger;
public void println(int level, String msg);{
logger.println(level, msg);;
}
public void printException(Throwable e);{
if(e instanceof EvaluationException);{
((EvaluationException);e);.printEvaluationTrace(out);;
}
else{
logger.printException(e);;
}
}
}


7。在每行消息前打印系統時間。
class TimestampLogger implements Logger{
private final Logger logger;
public void println(int level, String msg);{
logger.println(level, new Date();.toString();+": " + msg);;
}
public void printException(Throwable e);{
logger.println(ERROR, new Date();.toString();+": ");;
logger.printException(e);;
}
}

這個類其實可以再注射近來一個DateFormat來控制日期格式。但是這不是重點,我們忽略掉了它。
可是,這個類有一個很彆扭的地方,在printException中,我們必須要把系統時間單獨打印在一行,然後再打印異常信息。

這可不是我們想要的效果。我們本來是想讓系統時間出現在同一行的。
怎麼辦?回頭看看,如果Logger接口多一個print函數,一切就迎刃而解了。重構。

class TimestampLogger implements Logger{
private final Logger logger;
private boolean freshline = true;
private void printTimestamp(int level);{
if(freshline);{
logger.print(level, new Date();.toString();+": ");;
}
}
public void print(int level, String msg);{
printTimestamp(level);;
logger.print(level, msg);;
freshline = false;
}
public void println(int level, String msg);{
printTimestamp(level);;
logger.println(msg);;
freshline = true;
}
public void printException(Throwable e);{
printTimestamp(ERROR);;
logger.printException(e);;
freshline = true;
}
}


當然,前面的一些組合子也都要增加這個print函數。相信這應該不難。就留給大家做個練習吧。


好啦。到現在,我們手裏已經有兩個基本組合子,7個組合規則。應該已經可以組合出來不少有意義沒意義的東西了。

爲了避免寫一大堆的new和冗長的類名字,我們做一個facade類,來提供簡短點的名字,節省我們一些鍵盤損耗。

class Loggers{
static Logger nop();{return new NopLogger();;}
static Logger writer(PrintWriter writer);{
return new WriterLogger(writer);;
}
static Logger writer(OutputStream out);{
return writer(new PrintWriter(out, true););;
}
static Logger filter(int lvl, Logger l1, Logger l2);{
return new FilteredLogger(lvl, l1, l2);;
}
static Logger ignore(...);{...}
static Logger timestamp(...);{...}
...
}


這樣,在用這些組合子的時候大概代碼能夠好看一點吧。


好了,走了這麼遠,(雖然很多人可能還是覺得根本沒看見什麼激動人心的東西吧?都是簡單得不能再簡單的小類,有什麼了不起的呢?)。讓我們停下腳步,看看這麼信馬由繮把我們帶到了哪裏吧。


下一回,我們會試着用這些組合子和組合規則來看看能不能對付前面提出的那些亂七八糟的需求。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章