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

迄今,发现典型的几种疑问是: 
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.   
  9.   private void prefix(int lvl);{  
  10.     if(freshline);{  
  11.       Object r = factory.create();;  
  12.       if(r!=null);  
  13.         logger.print(lvl, r);;  
  14.       freshline = false;  
  15.     }  
  16.   }  
  17.   public void print(int lvl, String s);{  
  18.     prefix(lvl);;  
  19.     logger.print(lvl, s);;  
  20.   }  
  21.   public void println(int lvl, String s);{  
  22.     prefix(lvl);;  
  23.     logger.println(lvl, s);;  
  24.     freshline = true;  
  25.   }  
  26.   public void printException(int lvl, Throwable e);{  
  27.     prefix(lvl);;  
  28.     logger.printException(lvl, e);;  
  29.     freshline = true;  
  30.   }  
  31. }  

这里,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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值