迄今,发现典型的几种疑问是: 
1。组合子的设计要求正交,要求最基本,这是不是太难达到呢? 
2。面对一些现实中更复杂的需求,组合子怎样scale up呢?

其实,这两者都指向一个答案:重构。

要设计一个完全正交,原子到不可再分的组合子,也许不是总是那么容易。但是,我们并不需要一开始就设计出来完美的组合子设计。

比如,我前面的logging例子,TimestampLogger负责给在一行的开头打印当前时间。 
然后readonly提出了一个新的需要:打印调用这个logger的那个java文件的类名字和行号。

分析这个需求,可以发现,两者都要求在一行的开始打印一些东西。似乎有些共性. 
这个"在行首打印一些前缀"就成了一个可以抽象出来的共性.于是重构:

Java代码  
  1. interface Factory{
  2. String create();;
  3. }
  4. class PrefixLogger implements Logger{
  5. private final Logger logger;
  6. private final Factory factory;
  7. private boolean freshline = true;
  8. private void prefix(int lvl);{
  9. if(freshline);{
  10. Object r = factory.create();;
  11. if(r!=null);
  12. logger.print(lvl, r);;
  13. freshline = false;
  14. }
  15. }
  16. public void print(int lvl, String s);{
  17. prefix(lvl);;
  18. logger.print(lvl, s);;
  19. }
  20. public void println(int lvl, String s);{
  21. prefix(lvl);;
  22. logger.println(lvl, s);;
  23. freshline = true;
  24. }
  25. public void printException(int lvl, Throwable e);{
  26. prefix(lvl);;
  27. logger.printException(lvl, e);;
  28. freshline = true;
  29. }
  30. }

这里,Factory接口用来抽象往行首打印的前缀。这个地方之所以不是一个String,是因为考虑到生成这个前缀可能是比较昂贵的(比如打印行号,这需要创建一个临时异常对象)

另外,真正的Logger接口,会负责打印所有的原始类型和Object类型,例子中我们简化了这个接口,为了演示方便。

然后,先重构timestamp:

Java代码  
  1. class TimestampFactory implements Factory{
  2. private final DateFormat fmt;
  3. public String create();{
  4. return fmt.format(new Date(););;
  5. }
  6. }

这样,就把timestamp和“行首打印”解耦了出来。

下面添加TraceBackFactory,负责打印当前行号等源代码相关信息。

Java代码  
  1. interface SourceLocationFormat{
  2. String format(StackTraceElement frame);;
  3. }
  4. class TraceBackFactory implements Factory{
  5. private final SourceLocationFormat fmt;
  6. public String create();{
  7. final StackTraceElement frame = getNearestUserFrame();;
  8. if(frame!=null);
  9. return fmt.format(frame);;
  10. else return null;
  11. }
  12. private StackTraceElement getNearestUserFrame();{
  13. final StackTraceElement[] frames = new Throwable();.getStackTrace();;
  14. foreach(frame: frames);{
  15. if(!frame.getClassName();.startsWith("org.mylogging"););{
  16. //user frame
  17. return frame;
  18. }
  19. }
  20. return null;
  21. }
  22. }

具体的SourceLocationFormat的实现我就不写了。

注意,到现在为止,这个重构都是经典的oo的思路,划分责任,按照责任定义Factory, SourceLocationFormat等等接口,依赖注入等。完全没有co的影子。

这也说明,在co里面,我们不是不能采用oo,就象在oo里面,我们也可以围绕某个接口按照co来提供一整套的实现一样,就象在oo里面,我们也可以在函数内部用po的方法来实现某个具体功能一样。

下面开始对factory做一些co的勾当: 
先是最简单的:

Java代码  
  1. class ReturnFactory implements Factory{
  2. private final String s;
  3. public String create();{return s;}
  4. }

然后是两个factory的串联,

Java代码  
  1. class ConcatFactory implements Factory{
  2. private final Factory[] fs;
  3. public String create();{
  4. StringBuffer buf = new StringBuffer();;
  5. foreach(f: fs);{
  6. buf.append(f.create(););;
  7. }
  8. return buf.toString();;
  9. }
  10. }

最后,我们把这几个零件组合在一起:

Java代码  
  1. Logger myprefix(Logger l);{
  2. Factory timestamp = new TimestampFactory(some_date_format);;
  3. Factory traceback = new TraceBackFactory(some_location_format);;
  4. Factory both = new ConcatFactory(
  5. timestamp,
  6. new ReturnFactory(" - ");,
  7. traceback,
  8. new ReturnFactory(" : ");
  9. );;
  10. return new PrefixLogger(both, l);;
  11. }

如此,基本上,在行首添加东西的需求就差不多了,我们甚至也可以在行尾添加东西,还可以重用这些factory的组合子。

另一点我想说明的是:这种重构是相当局部的,仅仅影响几个组合子,而并不影响整个组合子框架。

真正影响组合子框架的,是Logger接口本身的变化。假设,readonly提出了一个非常好的意见:printException应该也接受level,因为我们应该也可以选择一个exception的重要程度。

那么,如果需要做这个变化,很不幸的是,所有的实现这个接口的类都要改变。

这是不是co的一个缺陷呢?

我说不是。 
即使是oo,如果你需要改动接口,所有的实现类也都要改动。co对这种情况,其实还是做了很大的贡献来避免的: 
只有原子组合子需要实现这个接口,而派生的组合子和客户代码,根本就不会被波及到。 
而co相比于oo,同样面对相同复杂的需求,往往原子组合子的数目远远小于实际上要实现的语义数,大量的需求要求的语义,被通过组合基本粒子来实现。也因此会减少直接实现这个接口的类的数目,降低了接口变化的波及范围。

那么,这个Logger接口是怎么来的呢?

它的形成来自两方面:

1。需求。通过oo的手段分配责任,最后分析出来的一个接口。这个接口不一定是最简化的,因为它完全是外部需求驱动的。

2。组合子自身接口简单性和完备性的需要。有些时候,我们发现,一个组合子里面如果没有某个方法,或者某个方法如果没有某个参数,一些组合就无法成立。这很可能说明我们的接口不是完备的。(比如那个print函数)。 
此时,就需要改动接口,并且修改原子组合子的实现。 
因为这个变化完全是基于组合需求的完备性的,所以是co方法本身带来的问题,而不能推诿于oo设计出来的接口。 
也因为如此,基本组合子个数的尽量精简就是一个目标。能够通过基本组合子组合而成的,就可以考虑不要直接实现这个接口。 
当然,这里面仍然有个权衡: 
通过组合出来的不如直接实现的直接,可理解性,甚至可调试性,性能都会有所下降。 
而如果选择直接实现接口,那么就要做好接口一旦变化,就多出一个类要改动这个类的心理准备。

如何抉择,没有一定之规。

而因为1和2的目标并不完全一致,很多时候,我们还需要在1和2之间架一个adapter以避免两个目标的冲突。

比如说,实际使用中,我可能希望Logger接口提供不要求level的println函数,让它的缺省值取INFO就好了。

但是,这对组合子的实现来说却是不利的。这时,我们也许就要把这个实现要求的Logger接口和组合子的Logger接口分离开来。(比如把组合子单独挪到一个package中)。

Logger这个例子是非常简单的,它虽然来自于实际项目,但是项目对logging的需求并不是太多,所以一些朋友提出了一些基于实际使用的一些问题,我只能给一个怎么做的大致轮廓,手边却没有可以运行的程序。

那么,下面一个例子,我们来看看一个我经过了很多思考比较完善了的ioc容器的设计。这个设计来源于yan container。

先说一下ioc容器的背景知识。

所谓ioc容器,是一种用来组装用ioc模式(或者叫依赖注射)设计出来的类的工具。 
一个用ioc设计出来的类,本身对ioc容器是一无所知的。使用它的时候,可以根据实际情况选择直接new,直接调用setter等等比较直接的方法,但是,当这样的组件非常非常多的时候,用一个ioc容器来统一管理这些对象的组装就可以被考虑。

拿pico作为例子,对应这样一个类:

Java代码  
  1. class Boy{
  2. private final Girl girl;
  3. public Boy(Girl g);{
  4. this.girl = g;
  5. }
  6. ...
  7. }

我们自然可以new Boy(new Girl());

没什么不好的。

但是,如果这种需要组装的类太多,那么这个组装就变成一件累人的活了。

于是,pico container提供了一个统一管理组建的方法:

Java代码  
  1. picocontainer container = new DefaultContainer();;
  2. container.registerComponentImplementation(Boy.class);;
  3. container.registerComponentImplementation(Girl.class);;

这个代码,很可能不是直接写在程序里面,而是先读取配置文件或者什么东西,然后动态地调用这段代码。

最后,使用下面的方法来取得对象:

Java代码  
  1. Object obj = container.getComponentInstance(Boy.class);;

注意,这个container.getXXX,本身是违反ioc的设计模式的,它 主动 地去寻找某个组件了。所以,组件本身是忌讳调用这种api的。如果你在组件级别的代码直接依赖ioc容器的api,那么,恭喜你,你终于成功地化神奇为腐朽了。

这段代码,实际上应该出现在系统的最外围的组装程序中。

当然,这是题外话。

那么,我们来评估一下pico先,

1。让容器自动寻找符合某个类型的组件,叫做auto-wiring。这个功能方便,但是不能scale up。一旦系统复杂起来,就会造成一团乱麻,尤其是有两个组件都符合这个要求的时候,就会出现二义性。所以,必须提供让配置者或者程序员显示指定使用哪个组件的能力。所谓manual-wire。 
当然,pico实际上是提供了这个能力的,它允许你使用组件key或者组件类型来显示地给某个组件的某个参数或者某个property指定它的那个girl。

但是,pico的灵活性就到这里了,它要求你的这个girl必须被直接登记在这个容器中,占用一个宝贵的全局key,即使这个girl只是专门为这个body临时制造的夏娃。

在java中,遇到这种情况:

Java代码  
  1. void A createA();{
  2. B b = new B();;
  3. return new A(b,b);;
  4. }

我们只需要把b作为一个局部变量,构造完A,b就扔掉了。然而,pico里面这不成,b必须被登记在这个容器中。这就相当于你必须要把b定义成一个全局变量一样。 
pico的对应代码:

Java代码  
  1. container.registerComponent("b" new CachingComponentAdapter(new ConstructorInjectionComponentAdapter(B.class);););;
  2. container.registerComponent("a", new ConstructorInjectionComponentAdapter(A.class););;

这里,为了对应上面java代码中的两个参数公用一个b的实例的要求,必须把a登记成一个singleton。CachingComponentAdapter负责singleton化某个组件,而ConstructorInjectionComponentAdapter就是一个调用构造函数的组建匹配器。

当然,这样做其实还是有麻烦的,当container不把a登记成singleton的时候(pico缺省都登记成singleton,但是你可以换缺省不用singleton的container。),麻烦就来了。

大家可以看到,上面的createA()函数如果调用两次,会创建两个A对象,两个B对象,而用这段pico代码,调用两次getComponentInstance("a"),会生成两个A对象,但是却只有一个B对象!因为b被被迫登记为singleton了。

2。pico除了支持constructor injection,也支持setter injection甚至factory method injection。(对最后一点我有点含糊,不过就假设它支持)。所以,跟spring对比,除了没有一个配置文件,life-cycle不太优雅之外,什么都有了。

但是,这就够了吗?如果我们把上面的那个createA函数稍微变一下:

Java代码  
  1. A createA();{
  2. B b = new B();;
  3. return new A(b, b.createC(x_component););;
  4. }

现在,我们要在b组件上面调用createC()来生成一个C对象。完了,我们要的既不是构造函数,也不是工厂方法,而是在某个临时组件的基础上调用一个函数。

缺省提供的几个ComponentAdapter这时就不够用了,我们被告知要自己实现ComponentAdapter。

实际上,pico对很多灵活性的要求的回答都是:自己实现ComponentAdapter。

这是可行的。没什么是ComponentAdapter干不了的,如果不计工作量的话。

一个麻烦是:我们要直接调用pico的api来自己解析依赖了。我们要自己知道是调用container.getComponentInstance("x_component")还是container.getComponentInstance(X.class)。 
第二个麻烦是:降低了代码重用。自己实现ComponentAdapter就得自己老老实实地写,如果自己的component adapter也要动态设置java bean setter的话,甭想直接用SetterInjectionComponentAdapter,好好看java bean的api吧。

其实,我们可以看出,pico的各种ComponentAdapter正是正宗的decorator pattern。什么CachingComponentAdapter,什么SynchronizedComponentAdapter,都是decorator。

但是,这也就是decorator而已了。因为没有围绕组合子的思路开展设计,这些decorator显得非常随意,没有什么章法,没办法支撑起整个的ComponentAdapter的架构。

下一章,我们会介绍yan container对上面提出的问题以及很多其他问题的解决方法。

yan container的口号是:只要你直接组装能够做到的,容器就能做到。 
不管你是不是用构造函数,静态方法,java bean,构造函数然后再调用某个方法,等等等等。 
而且yan container的目标是,你几乎不用自己实现component adapter,所有的需求,都通过组合各种已经存在的组合子来完成。

对我们前面那个很不厚道地用来刁难pico的例子,yan的解决方法是:

Java代码  
  1. b_component = Components.ctor(B.class);.singleton();;
  2. a_component = Components.ctor(A.class);
  3. .withArgument(0, b_component);
  4. .withArgument(1, b_component.method("createC"););;

b_component不需要登记在容器中,它作为局部component存在。 
是不是非常declarative呢?

下一节,你会发现,用面向组合子的方法,ioc容器这种东西真的不难。我们不需要仔细分析各种需求,精心分配责任。让我们再次体验一下吊儿郎当不知不觉间就天下大治的感觉吧。

待续。

from:http://ajoo.iteye.com/blog/23314

论面向组合子程序设计方法 之 重构相关推荐

  1. 论面向组合子程序设计方法 之 重构2

    已经有点感觉用ioc container来说明co不见得是个好主意了.  这个container的例子举出来,明显提出意见的人比那个简单的logging例子少了很多.  毕竟连pico是怎么回事,怎么 ...

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

    发现老庄的连载方法很好.又能吸引眼球又能好整以暇.于是从善如流. 这几天在完善我的neptune系统和jaskell语言.顺手发现了一个logging的需求.如获至宝阿. 为什么呢?不是因为这个需求多 ...

  3. java用排序子程序,论面向组合子程序设计方法 之一 创世纪

    新开张.先炒些冷饭过来.狡兔三窟,万一哪天javaeye倒台了(robbin气的脸都青了),我辛辛苦苦码的字就丢了,就象当年在allaboutprogram的遭遇一样,哭都找不到庙门. 发现老庄的连载 ...

  4. 论面向组合子程序设计方法 之 oracle

    不少朋友说我的阐述很苍白无力.这让我很苦恼.我确实是拚了命地想把问题说清楚,我也有实际non-trivial的项目经验,怎么就说不明白呢?哎! 所以,还是不能不多罗嗦一下,希望能够再阐述得明白一点. ...

  5. 面向组合子程序设计方法 之 新约

    每个小孩刚开始走路的时候都是跌跌撞撞的.  我们不自量力,妄图踩着上帝的步伐前进.结果就是这么几个简单的象白开水似的类.失望吗?是不是造物试图模仿造物主本身就是一种可笑的狂妄呢? 别急,让我们失声痛哭 ...

  6. 论面向组合子程序设计方法 之 微步毂纹生

    最近.age0提出了一个OO设计的问题.因为这个例子更加贴近生活,是我们老百姓所喜闻乐见的商场折扣问题,所以我准备改铉更张用这个例子了.具体的例子请看:  http://forum.iteye.com ...

  7. 论面向组合子程序设计方法 之 燃烧的荆棘

    唧唧歪歪一大堆.肯定早有人不耐烦了.  "你丫还有没有点实在的东西呀?"  要是我,可能也早就忍不住了. 好,好.我其实并没有忘记前面说的那个logging的例子.卖了这么长时间的 ...

  8. 论面向组合子程序设计方法 之 南无阿弥陀佛

    其实,前面我还忘了提一个非常重要的基本组合子:singleton.  这里补充提一下: Java代码   class SingletonComponent implements Component{ ...

  9. 论面向组合子程序设计方法 之 失乐园 之补充

    失乐园发了之后.有的朋友对"OO是一种自顶向下的方法论"的论述有些疑问. 这里补充一下. 如果认可OO是一个责任分配体系,那么自顶向下就是一个自然的逻辑结论. 没有分析出来&quo ...

最新文章

  1. 阿里云服务器如何安装memcached
  2. 注册自定义HTTP Handlers
  3. lean软件Android有吗,leanchat-android
  4. C#从入门到精通视频教程(2009年最新)- 视频列表
  5. 模拟电子技术基础:基本放大电路
  6. MTCNN人脸检测与人脸对齐
  7. linux怎么用jconsole_怎么在linux jconsole
  8. Python数据结构与算法(17)---归并排序
  9. SPSS 效度分析【SPSS 036期】
  10. fastboot 操作
  11. 分享嵌入式开发使用过程中遇到的几个问题(MQX4.2,IAR,Kinetis K66)
  12. PY32 单片机离线烧录器使用说明
  13. HTML 用过渡跟动画制作一个简易的旋转魔方
  14. 高考导数大题中的双变量不等式问题的求解思路
  15. win8系统如何修改锁屏界面壁纸
  16. OPC UA Sever+Client下载教程
  17. 开放与共赢:从NetSuite中国峰会看甲骨文的SDN生态圈
  18. 斯蒂文斯理工学院计算机专业排名,斯蒂文斯理工学院:院系设置及大学排名
  19. C++数组之动态数组
  20. mysql workbench下载安装_MySQL Workbench

热门文章

  1. quartz源码解析--转
  2. 金融风控实战——信贷业务架构与业务分析
  3. 【风控体系】互联网反欺诈体系漫谈
  4. java中的abstract和interface差异
  5. java word转html 烟火,Java多线程制作烟花效果.doc
  6. Visual Studio 2013开发 mini-filter driver step by step (5) - 读写文件
  7. SpringBoot - 实践阿里巴巴【Manager 层_通用业务处理层】
  8. Shell - 监控某个进程的内存占用情况、主机CPU、磁盘空间等信息以及守护进程
  9. Spring Boot2.x-11 使用@ControllerAdvice和@ExceptionHandler实现自定义全局异常
  10. php war,PHP提示Warning:phpinfo() has been disabled函数禁用的解决方法