呵呵.发现激烈的争论对发现双方的问题还是有所帮助的.大家心浮气躁之下,很有可能露出破绽.
不过,要真正解决问题,还是要心平气和下来.否则一直吵闹下去除了闹一肚子气不会有什么结果的.
通过和readonly, charon的讨论.我发觉确实有必要把构造函数,静态工厂,抽象工厂,容器配置这几个有联系也有区别的东西辨析一下.
好吧,开吹.
一.问题的提出.
假设我的系统有这样一个接口:
- interface Hello{
- void sayHello(String message);
- }
一个使用ioc方式的客户如下:
- class HelloUser{
- private final Hello hello;
- public void test(){
- hello.sayHello("yes");
- }
- }
有若干个Hello的不同实现,其中一个如下:
- class TerseHelloImpl1 implements Hello{
- private final Map history = new HashMap();
- public void sayHello(String msg){
- if(!history.containsKey(msg)){
- System.out.println(msg);
- history.put(msg, msg);
- }
- }
- public TerseHelloImpl1(){}
- }
也就是说,它记录每次谈话的记录,如果一句话曾经说过,它就不再说第二遍.
这是个生造出来的例子.但是能力有限,希望能够帮助理解问题.
好了,那么如何使用这个类呢?
假设我们通过调用构造函数吧.
那么,一个直接调用构造函数的java代码(即使你使用容器,也可能会出现在你的mock test代码里的)
- Hello hello = new TerseHelloImpl1();
- new HelloUser(hello).test();
而如果这个类最终是采用容器组装进系统(我后面会说明这不是一个永真命题)
那么,配置文件大致象这样:
- <...... class="TerseHelloImpl1"/>
好了. so far so good.
下面我们开始重构.
假设,我们公司老板脑袋出水了,花了一百万买了一个GenericHello的模块,它具有这样一个接口:
- class GenericHello implements Hello{
- public GenericHello(Comparator com);
- };
也就是说,它实现了Hello的接口,但是作为参数,它接受一个Comparator.这个Comparator对象用来判断两句话是否重复.
然后,我发现我写的TerseHelloImpl1功能上和GenericHello重复了.为了避免重复,我决定重构我的TerseHelloImpl1,让它直接调用GenericHello.
我希望这样写:
- class TerseHelloImpl1 implements Comparator{
- public int compare(Object s1, Object s2){
- //compare case-sensitively.
- }
- public static Hello instance(){
- return new GenericHello(singleton);
- }
- private static final Comparator singleton = new TerseHelloImpl1();
- }
也就是说,我希望可以直接把GenericHello的对象给用户用. 另外,因为我自己实现的这个大小写相关的comparator不需要状态,所以做成singleton来节省对象创建的开销.
我这里对GenericHello直接使用构造函数, 是为了避免枝节的争论.我这里其实可以也用一个静态工厂的.
但是,用户原来用的是我的TerseHelloImpl1 .
怎么解决这个矛盾呢?
下面就出现分歧了.
我来复述一下readonly的意思:
1.我们把这种变化的封装交给容器.通过改变配置文件来应对变化.
这样,我们不需要改动TerseHelloImpl1 的代码,只需要改变配置文件.
我不清楚配置文件的具体表达能力和语法,不过我们假设配置文件有能力做这件事,比如:
- <.... construction="new GenericHelloImpl(new CaseSensitiveStringComparator())"/>
具体语法如何无关紧要.
好,可以应对这个变化了.
2. 对于不使用配置文件的mock test.直接修改代码如下:
- Hello hello = new GenericHelloImpl(new CaseSensitiveStringComparator());
看,通过把这部分变化的逻辑移动到配置文件里,我们达到了只改变配置文件,而不改动java代码的目的.
下面,我来挑一挑毛病:
1. 假设任何一个对象的创建都通过配置文件和容器是没有根据的.
一个现成的例子就是你的mock test code,你是不是一般不会用容器了? 如果那样,你是不是还是要改动你的test code呀?
配置文件更多是为了对系统进行配置而做的,而不是用来给系统里面上百个小模块封装变化用的.很多对最终用户和部署员不可见的类是不必要放在配置文件中的.
比如本问题中的从TerseHelloImpl1 到GenericHello的这个变化,完全是系统内部重构,而不是对最终系统功能有影响的所谓configure.
而把这种局部的实现细节的变化层层上报到系统最顶层的配置文件之中不是一个好主意. 如果大家都这样作, 系统频繁重构的结果就是频繁改动全局的配置文件. 也会导致配置文件中有太多莫名其妙无关紧要的信息. 本来是局部的变化非要升级到全局, 岂不是开历史的倒车?
这是典型对配置文件的误用.也是一种变相的对容器的依赖.是一个anti-pattern.
另外, 如果你做的是一个库而不是个最终产品, 难道也要给用户发一个config.xml过去? 这大大增加了用户的工作量和维护难度.
2. 问题的本身是重构,而不是扩展.这两者的目的是不同的.重构的目的是在一定范围内改进代码结构,消除耦合和冗余等, 而同时不改变程序外在行为.
而read-only的方案实际上是一个扩展方案,原来的代码不动,而使用新的代码.
那么老的有重复的代码就扔在那里臭掉?
3. 公开构造函数而不让人使用.这只是一个约定,没有强制力. 你怎么保证大家都是这么守纪律? 如果有人直接调用了构造函数你怎么办? 这就象放着private不用,而非要在文档上写:"不许直接引用我"一样, 吃力不讨好.
4. ioc是个好东西. 但是走了极端,要求任何一个地方都不能创建任何对象,都要ioc,就是过度设计了. 有些地方当我们不需要ioc的灵活性的时候(比如,你做一个cache,往往就是直接new一个HashMap吧?你会声明一个Map或者MapFactory等外界传进来这么夸张吗?), 直接new或者调用静态工厂还是有需要的.
下面介绍一下charon的抽象工厂方案.
我其实始终没有明白charon要说的是什么.
姑且按照我理解解释一下:
1. 每个类都有且只有一个对应的抽象工厂接口.
2. ioc的时候,大家声明一个这个抽象工厂的变量,而不是直接声明这个产品接口变量.
解释一下就是, 我刚才例子中的HelloUser变成:
- class HelloUser{
- private final HelloFactory factory;
- public void test(){
- factory.create().sayHello("yes");
- }
- }
3. 配置文件中,不配置具体产品类,而是配置工厂类.
应用在上面的例子中,就是:
- interface HelloFactory{
- Hello create();
- }
第一版的工厂实现:
- version1:
- class TerseHelloFactory1 implements HelloFactory{
- Hello create(){
- return new TerseHelloImpl1();
- }
- }
mock test代码:
- HelloFactory factory = new TerseHelloFactory1();
- new HelloUser(factory).test();
配置文件:
- <...... factory="TerseHelloFactory1"/>
下面,当我们买了GenericHello之后, 需要增机一个工厂类,改动(或增加)mock test代码和配置文件
新的工厂代码:
- class TerseHelloFactory3 implements HelloFactory{
- Hello create(){
- return new GenericHello(new CaseSensitiveStringComparator()):
- }
- }
mock test代码:
- HelloFactory factory = new TerseHelloFactory3();
- new HelloUser(factory).test();
配置文件:
- <...... factory="TerseHelloFactory3"/>
好吧.下面我来挑毛病:
1.2.3.4实际上都和readonly的方案一样.
你不应该假设我们只能通过配置文件,通过一个抽象工厂来构造产品. OO的世界是个多样化的世界, 现实世界的需求也是复杂不可预期的.你凭空给加上这么大一个紧箍咒,孙悟空就没法七十二变了.
这个问题是重构的问题.不是问你怎么样能够在不碰原来的垃圾代码的情况下扩展.
你的构造函数既然是公有的,就有一个可能被人不小心使用的问题.
你的mock test代码无论如何需要改动.
除此之外,还有
5. ioc不能直接用产品接口而非要用工厂接口是典型的过度设计.你搞了一个一点用也没有的抽象.复杂化了问题却没有相应的收益.
6. 抽象工厂创建对象和从构造函数直接接受对象语义上是有不同的.你的这个设计有点给蛇穿鞋子的味道.
好吧. 让我最后祭起我的杀手锏: 命名构造函数,或者说静态工厂.
第一版的时候:
- class TerseHelloImpl1 implements Hello{
- ...
- public static Hello instance(){return new TerseHelloImpl1();}
- }
ioc用户:
- class HelloUser{
- private final Hello hello;
- public void test(){
- hello.sayHello("yes");
- }
- }
mock test代码:
- Hello hello = TerseHelloImpl1.instance();
- new HelloUser(hello).test();
配置文件:
- <...... factory="TerseHelloImpl1.instance()"/>
好,第二版来的时候, 我们只需要重构TerseHelloImpl1如下:
- class TerseHelloImpl1 implements Comparator{
- ...
- public static Hello instance(){
- return new GenericHello(singleton);
- }
- private static final Comparator singleton = new TerseHelloImpl1 ();
- }
好,所有的变化被instance()函数完全封装, mock test code不用动, 配置文件不用动.
事实上,我们并不要求关于TerseHelloImpl1的构造一定必须在配置文件中. 是否用配置文件来管理TerseHelloImpl1的构造这个决定完全由系统的整体设计决定,我们只关心我们自己的模块,不该我们管的,该闭嘴就闭嘴.
那么,比之于readonly或者charon的方案我们失去了什么灵活性么?
没有,什么也没有失去. 我们所做的仅仅是隐藏了自己的构造函数. 使用TerseHelloImpl1类的代码有它自己的自由来决定如何构造TerseHelloImpl1 实例.
它可以直接调用TerseHelloImpl1.instance(), 可以通过抽象工厂.
如:
- class HelloFactoryImpl implements HelloFactory{
- Hello create(){return TerseHelloImpl1.instance();}
- }
可以用配置文件, 可以用reflection.
唯一的代价: 静态工厂比之构造函数有那么一点额外的复杂度. 所以是选择构造函数还是静态工厂是需要程序员根据具体上下文来权衡的.
做一下总结:
charon和readonly的方案有一个共同点: 就是他们都不约而同地做了一个假设, 这个类TerseHelloImpl1 的构造必然经由容器和配置文件. 不可能有其它的构造方式. 我想,这大概是因为用配置文件确实是一个很灵活的方案. 而这两位都就此以为找到了银弹,所有的对象构造问题都可以也必须用一个配置文件一揽子解决了.而不用容器或者配置文件的设计就必然不是好设计,是应该先天就没有出生的权利的.
除此之外, charon的方案还引入了更多的不必要的间接层.
而静态工厂方案的特点:它只面对一个局部的小模块,不对使用它的外部环境做任何假设. 外面如何使用完全是外面的事. 它只做好自己的实现细节隐藏. 一切的实现细节变化,该它负责的,它都自己封装起来. 而不该它管的,不管用配置文件是否是个天才的想法,它都不做评价,而把决定权留给系统设计.
而我们知道, 一个方案,当它做的假设越少,它就越灵活.