论面向组合子程序设计方法 之 创世纪

发现老庄的连载方法很好.又能吸引眼球又能好整以暇.于是从善如流.


这几天在完善我的neptune系统和jaskell语言。顺手发现了一个logging的需求。如获至宝阿。

为什么呢?不是因为这个需求多么难,或者我的解决方法多么巧妙,而是因为,这个例子足够简单,直观,要说明它,背景知识几乎不大需要,三两句话大家就明白需要达到什么效果。这种例子可不是随便就想得到的。

而同时,它又对实现提出了一定程度的灵活性要求,正好方便我展示我叫做“面向组合子”的程序设计方法。


说到这里,不禁又有点沮丧,我也挺想象别人那样,先高举高打,玄之又玄,弄些哲学思辨,什么佛法呀,道德经阿,西游记亚,以及各位西方先哲的亟语,甚至量子力学的悖论。这样才能吸引眼球,增加人气呀。
可是,等而下之的工匠气作祟,说着说着就要拐到具体例子上去了。不争气呀。


算了,不想那么多了。论道不是俺这种俗人所擅长的,还是鼓捣“器”吧。


大致的背景是这样:
我的neptune是一个build system,在build的过程中会产生很多log信息。这些信息分为不同的重要级别。

说到这里,肯定有人已经按奈不住:用Log4j!

先不要急,我们这里不是要告诉你怎么处理你得程序中的logging需求,而是要通过这样一个容易理解的例子来说明以下“面向组合子”的编程方法。所以,这里让我们先假设我们不知道什么log4j。什么是log4j呀?

当然,大致的思路总归差不多的。因为我的neptune系统只需要一个logging的工具,而不关心这个logging工具是什么,这就是一个perfect的依赖注射的场合。

先定义接口Logger,然后从构造函数传递近来一个Logger实例,接着就直接调用Logger就是了。

public interface Logger{
void print(int level, String msg);;
void println(int level, String msg);;
void logException(Throwable e);;
}


print用来输出信息,但是不折行,println可以折行。
logException用来直接纪录异常。这样,对异常是直接printStackTrace,还是println(e.getMessage())就是由具体的Logger实现来决定,我的neptune只需要把遇到的异常报告给Logger就是了。


好。接下来我吭哧吭哧地把neptune完成了,剩下的就是从哪里找一个Logger实现了。

最简单的Logger实现自然就是直接往屏幕上打印了:

class SimpleLogger implements Logger{
public void print(int lvl, String msg);{
System.out.print(msg);;
}
public void println(int lvl, String msg);{
System.out.println(msg);;
}
public void printException(Throwable e);{
e.printStackTrace();;
}
}



直接把这个SimpleLogger注射进我的neptune,整个系统就可以工作了。
no big deal,对么?


好了,下面我们开始真正实现完整的logging系统了。经过分析,我们大致有以下的需要:

[list]1。logger可以把信息打印到log文件中。

2。不同的重要程度的信息也许可以打印到不同的文件中?象websphere,有error.log, info.log等。
如果这样,那么什么重要程度的信息进入error.log,什么进入warning.log,什么进入info.log也需要决定。

3。也许可以象ant一样把所有的信息都打印到一个文件中。

4。每条logging信息是否要同时打印当前的系统时间?也是一个需要抉择的问题。

5。不仅仅是log文件,我们还希望能够在标准错误输出上直接看见错误,普通的信息可以打印到log文件中,对错误信息,我们希望log文件和标准输出上都有。

6。标准输出上的东西只要通知我们出错了就行,大概不需要详细的stack trace,所以exception stack trace可以打印到文件中,而屏幕上有个简短的exception的message就够了。

7。warning似乎也应该输出到屏幕上。

8。不管文件里面是否要打印当前系统时间,屏幕上应该可以选择不要打印时间。

9。客户应该可以通过命令行来决定log文件的名字。

10。客户可以通过命令行来决定log的细节程度,比如,我们只关心info一样级别的信息,至于debug, verbose的信息,对不起,不要打印。

11。neptune生成的是一些Command对象,这些对象运行的时候如果出现exception,这些exception会带有execution trace,这个execution trace可以告诉我们每个调用栈上的Command对象在原始的neptune文件中的位置(行号)。
这种exception叫做NeptuneException,它有一个printExecutionTrace(PrintWriter)的方法来打印execution trace。
所以,对应NeptuneException,我们就不仅仅是printStackTrace()了,而是要在printStackTrace()之前调用printExecutionTrace()。


12。neptune使用的是jaskell语言,如果jaskell脚本运行失败,一个EvaluationException会被抛出,这个类有一个printEvaluationTrace(PrintWriter)的方法来打印evaluation trace,这个trace用来告诉我们每个jaskell的表达式在脚本文件中的位置。
所以,对应EvaluationException,我们要在printStackTrace()之前,调用printEvaluationTrace()。

13。execution trace和evaluation trace应该被打印到屏幕上和log文件两个地方。

14。因为printExecutionTrace()和printEvaluationTrace()本身已经打印了这个异常的getMessage(),所以,对这两种异常,我们就不再象对待其它种类的异常那样在屏幕上打印getMessage()了,以免重复。

15。也许还有一些暂时没有想到的需求, 比如不是写入log文件,而是画个图之类的变态要求。[/list:u]

大致上,我目前遇到的需求也就是这些了。

好了,允许我卖个关子,下回分解的时候再说怎么用“面向组合子”和依赖注射的方法来解决这个问题吧。

在本节结束之前,我稍微提一下“面向组合子”的来历。


组合子,英文叫combinator,是函数式编程里面的重要思想。如果说OO是归纳法(分析归纳需求,然后根据需求分解问题,解决问题),那么“面向组合子”就是“演绎法”。通过定义最基本的原子操作,定义基本的组合规则,然后把这些原子以各种方法组合起来。我最近一段时间做的东西,jaskell不用说了,函数式语言。yan, neptune, jparsec全是用面向组合子的思想开发的。

OO就像是猜谜,给你一个苹果,然后问你:这个苹果是怎么得到的呢?然后你分析一番,说:我认为这个苹果是由分子组成的,这些分子如此这般排列,然后分子又由原子组成,如此这般排列...

而CO(面向组合子),就等于是说:这有H, C, O三种原子,强弱两种作用力,你来看看能做点什么出来吧,然后你就像搭积木一样,把这三种原子,两种作用力搭建出这大千世界,什么毛毛虫,狗熊,周星星,不小心,一下就做出了一个苹果。

OO的关键是需求。
所谓"refactor",不过也是强调需求,让你不要自作聪明地瞎假设需求而复杂化设计。时刻着眼于当前的需求。这样,一旦需求变更,所浪费的力气可以保证最小,而且,船小才好调头嘛。
如果需求分析的不好,一切就歇菜了,虽然因为一些比如ioc之类的设计方法能让你不至于推到重来,但是需求仍然是重中之重。


那些什么上下文没有,上来就说“怎么用OO来做一个人骑车呀?”,“是人.骑(车)呀?还是车.被骑(人)?”纯粹是没头没脑地瞎掰。

而CO的关键则是组合子和组合规则的设计。这些组合方法必须非常精巧,尽量正交。组合子的设计既要简单(越简单才越容易被组合),还要完整。
比如说,对整数这个组合子,我们有+-*等组合方法,这样只要有了0,1这两个组合子,我们就可以构造出整个整数世界。
可是,精巧的组合子设计也不是那么容易的。需要有一点点数学的感觉和严密的逻辑思维基础。


有人说,上帝是用OO设计的世界,可要我是上帝,我宁可用CO。
设计几个简单的基本粒子,几个简单的相互作用力,然后让这些东西自己组合,随意发展,不是比事必躬亲,先想透彻了自己想让世界是什么样子,然后一张一张图纸地具体设计,一个一个人地造,
“撒旦,你去干坏事,往死了整这帮贱人!”
“天使,你去干好事,打个巴掌再给他们点甜枣吃。”
“儿子,你下去混混,看丫敢不敢钉死你!”
来的容易美妙?

哈。终于形而上起来了,爽!

你可能感兴趣的:(论面向组合子程序设计方法 之 创世纪)