|
该帖已经被评为精华帖
|
|
|---|---|
| 作者 | 正文 |
|
最后更新时间:2004-08-18
呵呵.发现激烈的争论对发现双方的问题还是有所帮助的.大家心浮气躁之下,很有可能露出破绽.
不过,要真正解决问题,还是要心平气和下来.否则一直吵闹下去除了闹一肚子气不会有什么结果的. 通过和readonly, charon的讨论.我发觉确实有必要把构造函数,静态工厂,抽象工厂,容器配置这几个有联系也有区别的东西辨析一下. 好吧,开吹. 一.问题的提出. 假设我的系统有这样一个接口: [code:1]interface Hello{ void sayHello(String message); }[/code:1] 一个使用ioc方式的客户如下: [code:1]class HelloUser{ private final Hello hello; public void test(){ hello.sayHello("yes"); } }[/code:1] 有若干个Hello的不同实现,其中一个如下: [code:1]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(){} }[/code:1] 也就是说,它记录每次谈话的记录,如果一句话曾经说过,它就不再说第二遍. 这是个生造出来的例子.但是能力有限,希望能够帮助理解问题. 好了,那么如何使用这个类呢? 假设我们通过调用构造函数吧. 那么,一个直接调用构造函数的java代码(即使你使用容器,也可能会出现在你的mock test代码里的) [code:1] Hello hello = new TerseHelloImpl1(); new HelloUser(hello).test();[/code:1] 而如果这个类最终是采用容器组装进系统(我后面会说明这不是一个永真命题) 那么,配置文件大致象这样: [code:1]<...... class="TerseHelloImpl1"/>[/code:1] 好了. so far so good. 下面我们开始重构. 假设,我们公司老板脑袋出水了,花了一百万买了一个GenericHello的模块,它具有这样一个接口: [code:1]class GenericHello implements Hello{ public GenericHello(Comparator com); };[/code:1] 也就是说,它实现了Hello的接口,但是作为参数,它接受一个Comparator.这个Comparator对象用来判断两句话是否重复. 然后,我发现我写的TerseHelloImpl1功能上和GenericHello重复了.为了避免重复,我决定重构我的TerseHelloImpl1,让它直接调用GenericHello. 我希望这样写: [code:1]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(); }[/code:1] 也就是说,我希望可以直接把GenericHello的对象给用户用. 另外,因为我自己实现的这个大小写相关的comparator不需要状态,所以做成singleton来节省对象创建的开销. 我这里对GenericHello直接使用构造函数, 是为了避免枝节的争论.我这里其实可以也用一个静态工厂的. 但是,用户原来用的是我的TerseHelloImpl1 . 怎么解决这个矛盾呢? 下面就出现分歧了. 我来复述一下readonly的意思: 1.我们把这种变化的封装交给容器.通过改变配置文件来应对变化. 这样,我们不需要改动TerseHelloImpl1 的代码,只需要改变配置文件. 我不清楚配置文件的具体表达能力和语法,不过我们假设配置文件有能力做这件事,比如: [code:1]<.... construction="new GenericHelloImpl(new CaseSensitiveStringComparator())"/>[/code:1] 具体语法如何无关紧要. 好,可以应对这个变化了. 2. 对于不使用配置文件的mock test.直接修改代码如下: [code:1]Hello hello = new GenericHelloImpl(new CaseSensitiveStringComparator());[/code:1] 看,通过把这部分变化的逻辑移动到配置文件里,我们达到了只改变配置文件,而不改动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变成: [code:1]class HelloUser{ private final HelloFactory factory; public void test(){ factory.create().sayHello("yes"); } }[/code:1] 3. 配置文件中,不配置具体产品类,而是配置工厂类. 应用在上面的例子中,就是: [code:1]interface HelloFactory{ Hello create(); }[/code:1] 第一版的工厂实现: [code:1]version1: class TerseHelloFactory1 implements HelloFactory{ Hello create(){ return new TerseHelloImpl1(); } }[/code:1] mock test代码: [code:1] HelloFactory factory = new TerseHelloFactory1(); new HelloUser(factory).test();[/code:1] 配置文件: [code:1]<...... factory="TerseHelloFactory1"/>[/code:1] 下面,当我们买了GenericHello之后, 需要增机一个工厂类,改动(或增加)mock test代码和配置文件 新的工厂代码: [code:1]class TerseHelloFactory3 implements HelloFactory{ Hello create(){ return new GenericHello(new CaseSensitiveStringComparator()): } }[/code:1] mock test代码: [code:1] HelloFactory factory = new TerseHelloFactory3(); new HelloUser(factory).test();[/code:1] 配置文件: [code:1]<...... factory="TerseHelloFactory3"/>[/code:1] 好吧.下面我来挑毛病: 1.2.3.4实际上都和readonly的方案一样. 你不应该假设我们只能通过配置文件,通过一个抽象工厂来构造产品. OO的世界是个多样化的世界, 现实世界的需求也是复杂不可预期的.你凭空给加上这么大一个紧箍咒,孙悟空就没法七十二变了. 这个问题是重构的问题.不是问你怎么样能够在不碰原来的垃圾代码的情况下扩展. 你的构造函数既然是公有的,就有一个可能被人不小心使用的问题. 你的mock test代码无论如何需要改动. 除此之外,还有 5. ioc不能直接用产品接口而非要用工厂接口是典型的过度设计.你搞了一个一点用也没有的抽象.复杂化了问题却没有相应的收益. 6. 抽象工厂创建对象和从构造函数直接接受对象语义上是有不同的.你的这个设计有点给蛇穿鞋子的味道. 好吧. 让我最后祭起我的杀手锏: 命名构造函数,或者说静态工厂. 第一版的时候: [code:1]class TerseHelloImpl1 implements Hello{ ... public static Hello instance(){return new TerseHelloImpl1();} }[/code:1] ioc用户: [code:1]class HelloUser{ private final Hello hello; public void test(){ hello.sayHello("yes"); } }[/code:1] mock test代码: [code:1] Hello hello = TerseHelloImpl1.instance(); new HelloUser(hello).test();[/code:1] 配置文件: [code:1]<...... factory="TerseHelloImpl1.instance()"/>[/code:1] 好,第二版来的时候, 我们只需要重构TerseHelloImpl1如下: [code:1]class TerseHelloImpl1 implements Comparator{ ... public static Hello instance(){ return new GenericHello(singleton); } private static final Comparator singleton = new TerseHelloImpl1 (); }[/code:1] 好,所有的变化被instance()函数完全封装, mock test code不用动, 配置文件不用动. 事实上,我们并不要求关于TerseHelloImpl1的构造一定必须在配置文件中. 是否用配置文件来管理TerseHelloImpl1的构造这个决定完全由系统的整体设计决定,我们只关心我们自己的模块,不该我们管的,该闭嘴就闭嘴. 那么,比之于readonly或者charon的方案我们失去了什么灵活性么? 没有,什么也没有失去. 我们所做的仅仅是隐藏了自己的构造函数. 使用TerseHelloImpl1类的代码有它自己的自由来决定如何构造TerseHelloImpl1 实例. 它可以直接调用TerseHelloImpl1.instance(), 可以通过抽象工厂. 如: [code:1]class HelloFactoryImpl implements HelloFactory{ Hello create(){return TerseHelloImpl1.instance();} }[/code:1] 可以用配置文件, 可以用reflection. 唯一的代价: 静态工厂比之构造函数有那么一点额外的复杂度. 所以是选择构造函数还是静态工厂是需要程序员根据具体上下文来权衡的. 做一下总结: charon和readonly的方案有一个共同点: 就是他们都不约而同地做了一个假设, 这个类TerseHelloImpl1 的构造必然经由容器和配置文件. 不可能有其它的构造方式. 我想,这大概是因为用配置文件确实是一个很灵活的方案. 而这两位都就此以为找到了银弹,所有的对象构造问题都可以也必须用一个配置文件一揽子解决了.而不用容器或者配置文件的设计就必然不是好设计,是应该先天就没有出生的权利的. 除此之外, charon的方案还引入了更多的不必要的间接层. 而静态工厂方案的特点:它只面对一个局部的小模块,不对使用它的外部环境做任何假设. 外面如何使用完全是外面的事. 它只做好自己的实现细节隐藏. 一切的实现细节变化,该它负责的,它都自己封装起来. 而不该它管的,不管用配置文件是否是个天才的想法,它都不做评价,而把决定权留给系统设计. 而我们知道, 一个方案,当它做的假设越少,它就越灵活. 声明:JavaEye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
|
|
| 返回顶楼 | |
|
最后更新时间:2004-08-18
有一定的道理,静态工厂是比较好用的,抽象工厂要看了,不一定都要用抽象工厂构造对象的,而且多加一层不是很有必要
|
|
| 返回顶楼 | |
|
最后更新时间:2004-08-18
ajoo 写道 下面,我来挑一挑毛病: 1. 假设任何一个对象的创建都通过配置文件和容器是没有根据的. 同意此观点,但是既然使用配置了,在test里面当然不会new TerseHelloImpl1();可以从容器中取啊。至少spring是可以做到的。 ajoo 写道 3. 公开构造函数而不让人使用.这只是一个约定,没有强制力. 你怎么保证大家都是这么守纪律? 如果有人直接调用了构造函数你怎么办? 这就象放着private不用,而非要在文档上写:"不许直接引用我"一样, 吃力不讨好. 这是一个缺陷,缺少一个强制的机制。但近似的问题很多,比如要求必须针对接口编程等等? ajoo 写道 4. ioc是个好东西. 但是走了极端,要求任何一个地方都不能创建任何对象,都要ioc,就是过度设计了. 有些地方当我们不需要ioc的灵活性的时候(比如,你做一个cache,往往就是直接new一个HashMap吧?你会声明一个Map或者MapFactory等外界传进来这么夸张吗?), 直接new或者调用静态工厂还是有需要的. 严重同意,可我没有见哪个资料上说所有类都要从ioc走。 实际上你说的问题应该转化为:什么类用ioc,什么时候不用,而这是设计问题,每个项目的取舍都会不一样。 至于你举的例子客户端出现: Hello hello = TerseHelloImpl1.instance(); 感到吃惊,这不是针对接口编程啊。TerseHelloImpl1已经仅仅绑在了客户端身上,这个类想drop都drop不掉了。 而TerseHelloImpl1 hello = TerseHelloImpl1.instance();这样的代码我倒能接受,因为这里的含义就是要和TerseHelloImpl1绑在一起而不管它是不是Hello的实现。 |
|
| 返回顶楼 | |
|
最后更新时间:2004-08-18
ajoo 不同意你的看法
你重构以后,你原来的代码已经没有了,万一某个客户发现你们老板买来的lib和他们公司的policy有冲突,不能使用你refactor的版本,怎么办 而且第一第二种方法中,我们也可以使用你重构代码的方式来达到你代码同样的效果 我觉得静态工厂的问题在于,你把用户绑定到静态工厂上了 你的例子有一个TerseHelloImpl1.instance() 那么,如果有很多不同的对象要创建呢? 那么,客户代码中就绑定了无数的诸如 TerseHelloImpl2.instance() 。。。。。。。。。。 TerseHelloImpln.instance() 这样的话,我情愿把所有类似的绑定集中到IOC容器的配置文件上去 |
|
| 返回顶楼 | |
|
最后更新时间:2004-08-18
各位大哥好, 2天没有来了, 今天看到讨论还在如火如荼地进行, 真是感动地鼻涕眼泪一把呀......
偶来指出ajoo的错误: ajoo 写道 我来复述一下readonly的意思: 1.我们把这种变化的封装交给容器.通过改变配置文件来应对变化. No, 偶很懒惰, 偶一直推荐的是通过改变组装代码来应对变化, 配置文件那些追求完美的人才喜欢用的. ajoo 写道 那么,一个直接调用构造函数的java代码(即使你使用容器,也可能会出现在你的mock test代码里的) 2. 对于不使用配置文件的mock test.直接修改代码如下: 错, 不存在mock test的代码和集成测试代码的区别, 测试代码只有一份, 测试代码只知道它能够从IoC容器获得一个interface, 至于这个interface的实现是mock还是真实的, 那是组装代码的事情. ajoo 写道 配置文件更多是为了对系统进行配置而做的,而不是用来给系统里面上百个小模块封装变化用的.很多对最终用户和部署员不可见的类是不必要放在配置文件中的. 所以说你不了解pico, 组装代码难道不能隔离么? 配置文件难道不能隔离么? 组装代码和配置文件难道不能被打包么? 一个pico容器里不能包含子容器么? 正如你所说, 局部的实现细节的变化只会被包含在局部的组装代码或者配置文件里. 部署员所要做的只是重新deploy一个jar而已. ajoo 写道 2. 问题的本身是重构,而不是扩展.这两者的目的是不同的.重构的目的是在一定范围内改进代码结构,消除耦合和冗余等, 而同时不改变程序外在行为. 而read-only的方案实际上是一个扩展方案,原来的代码不动,而使用新的代码. 那么老的有重复的代码就扔在那里臭掉? 既然脑袋短路的老板花了100万买了一个实现, 那么偶修改一下组装代码就好了, 老代码你看着不爽就删除掉呗. 反正客户的代码只知道从容器中获得的是一个interface而已, 更本不用做啥改动, 哪里来的重复代码? ajoo 写道 3. 公开构造函数而不让人使用.这只是一个约定,没有强制力. 你怎么保证大家都是这么守纪律? 如果有人直接调用了构造函数你怎么办? 这就象放着private不用,而非要在文档上写:"不许直接引用我"一样, 吃力不讨好. 这个没有办法, 偶宁可强调这个小小的纪律, 也不愿意把构造函数变成private (否则, 你还想不想让其他人扩展你的类呀......) 在实际开发中, 偶还没有发现有谁那么勤劳: 放着现成的测试用例(从container获取interface)不抄, 反而非要找到这个interface的某个实现, 然后再new出来玩....... youcai和xangd同学已经批评过你的静态工厂方法的代码依赖性强的缺点了, 偶就不唐僧了...... |
|
| 返回顶楼 | |
|
最后更新时间:2004-08-18
别的大家都说了。
单就我的角度来看,这里还有几个问题。 1 在我的概念里面,重构是有一个底线的,这个底线就是保持所实现的接口类型不变。否则,还是另外起一个名字。 所以这个例子本身不太可靠。 2 假设这个例子合理,那么这里正好是抽象工厂的用武之地。或者说,本身的设计就应当使client依赖于抽象工厂的接口。 那样,在出现变化之后,有两个选择。一个是重构TerseHelloFactory1,因为重构前后仍然实现相同的接口,我没有心理负担。 另一个选择,就是定义一个TerseHelloFactory2。这个做法的好处是,如果老板突然开窍,要变回去,那么只需要更改配置文件。 3 抽象工厂一般而言不会仅仅针对一个类。而是针对一组类。我相信在一个应用中,一个类必然不会独立存在。此时,一个抽象工厂,可能抵得上几个静态工厂方法。 4 我对一般对象出现在配置文件中有心理障碍,但是对工厂对象放在配置文件中,可以接受。 5 是不是用配置文件,对抽象工厂和静态工厂而言都是一样的。只是在名字上是不是保持同一性。抽象工厂的好处是保持了类在接口语义上的一致性。 还有一点,我觉得需要着重强调一下,有必要区分组件代码和装配组件的胶水代码。装配代码独立出来放在代码或者是配置文件中,应该都能满足隔离变化的要求。 |
|
| 返回顶楼 | |
|
最后更新时间:2004-08-18
除了静态方法对具体子类的直接依赖问题之外,对象的产生封装在对象内部也是一个很奇怪的想法,如果在不同情况下,例如需要同步或者需要lazy等等的话,你必须针对修改你的代码,实际上我们最关心的是能够在不同场合使用同一个对象的业务逻辑,而你这样做的话会紧紧因为我们需要不同的对象创建方法而修改对象的代码,这是非常不智的,这也是singleton被视作evil的重要原因之一。(例如singleton,有时候我们希望使用超类的实例,有时候希望使用子类的实例,有的时候我们希望产生单个,有的时候需要产生多个[取singleton单控制点的含义],有的时候需要同步,有的时候不需要同步,在集群的情况我们甚至可能需要数据库来实现唯一化控制,有时候希望缓存,有的时候不需要缓存,有的时候希望增强,有的时候希望采用动态代理产生。所以一般来说,我们希望在一个系统内部最多只有一个入口的singleton,而我们也往往也不打算在其它不同的场合重用这个singleton)。
代码的重用是对他业务逻辑的重用,这正是对象的核心价值。所以如同charon所说的,相对而言,我们往往不在乎组装代码的重用性,而是追求业务代码本身可以被按照不同的组装方式使用,例如既可以在EJB下,也可以在普通的Java应用程序中,既可以作为远程传输的对象,因此,我们往往不会去强制对象构造和产生的方式(例如由EJB容器产生,由IOC容器产生、有抽象工厂产生,或者直接由new产生),相反,是把对象的产生交给外部负责,这样才能达到在不同场合下对象最大的可重用性。说个简单的,如果对象的构造方法是私有的,那么现在的很多框架(例如hibernate,JavaBean,EJB等等你就根本不能使用了).这也是现在认为POJO比有特殊要求的对象更好的原因,因为任何一种框架和技术都可以自由选择自己的方式来创建对象。 所有创建型设计模式和IoC容器的使用正是基于这样的假设的,只有对象把构造的责任交给外部来实现,那么我们才能有效地隐藏对象创建的时机、方式、方法,给不修改对象本身的代码而能够使用不同的对象创建方式(包括使用子类,替代类)提供了前提,从而提高对象实现业务逻辑的在不同场合的可重用性。所以不是说一定要用IOC容器或者用抽象工厂,而是可以用这些方法,也可以不用这种方法。而就抽象工厂和IoC容器本身而言,通过它们各自的封装,可以进一步实现对不同具体实现子类的解绑,就更加好了。如果有一天你不想用Pico或者不想用抽象工厂,你可以选择其他更加合适的方法来实现解绑。而这个责任不应该交给对象自己来实现,因为我们根本无法预料这个对象将会以什么样的方式被构造出来,在什么时候构造出来,需要依赖什么其它外部机制构造出来(例如可能依赖数据库,或者依赖串行化),这最好是有外部使用这个对象的环境来决定 最终,我们希望能够让对象的创建、对象的销毁完全脱离对象的使用,垃圾收集器已经为我们提供VM级别的支持,而抽象工厂和工厂方法以及其它构造型设计模式,IoC从模式和框架的角度给我们另一半。 |
|
| 返回顶楼 | |
|
最后更新时间:2004-08-19
xangd 写道 ajoo 不同意你的看法
你重构以后,你原来的代码已经没有了,万一某个客户发现你们老板买来的lib和他们公司的policy有冲突,不能使用你refactor的版本,怎么办 而且第一第二种方法中,我们也可以使用你重构代码的方式来达到你代码同样的效果 我觉得静态工厂的问题在于,你把用户绑定到静态工厂上了 你的例子有一个TerseHelloImpl1.instance() 那么,如果有很多不同的对象要创建呢? 那么,客户代码中就绑定了无数的诸如 TerseHelloImpl2.instance() 。。。。。。。。。。 TerseHelloImpln.instance() 这样的话,我情愿把所有类似的绑定集中到IOC容器的配置文件上去 1. 我就知道我这个漏洞会被人抓出来. 不错,这个例子不太好.生造出来的. 但是我想要表达的意思大家能明白.就是:当需要进行重构的时候.这也许不是一个非常完美的重构的例子. 我道歉. 2.这个所谓绑定也是我要说的. 我和charon已经说过无数次了. 我们比较的是静态工厂和构造函数.两者都要对具体类产生依赖. X.instance()和new X()都依赖于X. 而如果你说可以用配置文件来调用构造函数, 这样这个依赖就在java代码之外了的话(我想readonly他们的意思就在这里). 那么,谁说你就不能用配置文件来配置工厂呢? 用配置文件来和工厂相比是不公平的. 就象我说你游泳没有phelps快,你跳上一辆赛艇,然后和phelps比一样. 静态工厂的好处在于: 它不要求也不排斥你用配置文件.所有的设计决策还是你自己拿主意. |
|
| 返回顶楼 | |
|
最后更新时间:2004-08-19
ajoo 写道 xangd 写道 ajoo 不同意你的看法
你重构以后,你原来的代码已经没有了,万一某个客户发现你们老板买来的lib和他们公司的policy有冲突,不能使用你refactor的版本,怎么办 而且第一第二种方法中,我们也可以使用你重构代码的方式来达到你代码同样的效果 我觉得静态工厂的问题在于,你把用户绑定到静态工厂上了 你的例子有一个TerseHelloImpl1.instance() 那么,如果有很多不同的对象要创建呢? 那么,客户代码中就绑定了无数的诸如 TerseHelloImpl2.instance() 。。。。。。。。。。 TerseHelloImpln.instance() 这样的话,我情愿把所有类似的绑定集中到IOC容器的配置文件上去 1. 我就知道我这个漏洞会被人抓出来. 不错,这个例子不太好.生造出来的. 但是我想要表达的意思大家能明白.就是:当需要进行重构的时候.这也许不是一个非常完美的重构的例子. 我道歉. 2.这个所谓绑定也是我要说的. 我和charon已经说过无数次了. 我们比较的是静态工厂和构造函数.两者都要对具体类产生依赖. X.instance()和new X()都依赖于X. 而如果你说可以用配置文件来调用构造函数, 这样这个依赖就在java代码之外了的话(我想readonly他们的意思就在这里). 那么,谁说你就不能用配置文件来配置工厂呢? 用配置文件来和工厂相比是不公平的. 就象我说你游泳没有phelps快,你跳上一辆赛艇,然后和phelps比一样. 静态工厂的好处在于: 它不要求也不排斥你用配置文件.所有的设计决策还是你自己拿主意. 你没得选择的,举个例子,你有一个A,现在假设你第一个程序使用它的时候希望是永远是同一个对象,但是你在第二个程序里面希望是每次都产生新的一个对象,现在你怎样在两个程序之间重用这个对象? 静态工厂的坏处就在于不管你使用何种方式,你都唯一决定了你的设计和实现,而外部创建的好处就在于: 引用 它不要求也不排斥你用配置文件.所有的设计决策还是你自己拿主意. 你可以在不同的场合对同一个类采用不同的构造方法。 |
|
| 返回顶楼 | |
|
最后更新时间:2004-08-19
引用 No, 偶很懒惰, 偶一直推荐的是通过改变组装代码来应对变化, 配置文件那些追求完美的人才喜欢用的. 好吧.我错误理解了你. 那么既然组装代码是手写的,是java代码,那么你仍然是在java代码里依赖了具体类.你强调构造函数只不过是因为暂时某些容器还不支持工厂函数而已. 而我,把这归结为那个容器的不成熟. (当然,理想是理想,如果现实中必须使用那个容器, 那么因为容器的限制不能使用静态工厂也是一个理由) 引用 错, 不存在mock test的代码和集成测试代码的区别, 测试代码只有一份, 测试代码只知道它能够从IoC容器获得一个interface, 至于这个interface的实现是mock还是真实的, 那是组装代码的事情.
就是说, 你的测试代码也依赖容器? 呵呵.不知道是不是所有人都喜欢这样作. 至少,我的观点是:你不能在写一个小模块的时候对整个系统如何设计,如何测试的作出这么多要求. 按照职责划分,你该作甚么就做什么,不应该把自己的失误赖到设计者身上. 比如, 你自己写了三个小类A,B,C, 各自给了一个构造函数的接口. 然后回头你发现你其实可以写一个更通用的类,然后让这三个小类都委托这个更通用的类. 你怎么做? 是不是象你说的似的直接找到组装的人:"老兄,我发现我那三个类这么调用更好.你改一下吧." 而如果组装的人跟你抱怨: 你怎么改动这么多,这么频繁,一会儿一个主意. 你就理直气壮地说: "你觉得改起来麻烦是你的总体设计不好.要是我,只要改两行配置文件就好了."? 呵呵.我不认为这是一个懂得设计的人应该这么做的.局部的变化就应该封装在局部. 引用 所以说你不了解pico, 组装代码难道不能隔离么? 配置文件难道不能隔离么? 组装代码和配置文件难道不能被打包么? 一个pico容器里不能包含子容器么? 正如你所说, 局部的实现细节的变化只会被包含在局部的组装代码或者配置文件里. 部署员所要做的只是重新deploy一个jar而已.
设计的粒度可大可小. 除非你跟我说在任何一个粒度上你都可以用一个"局部配置文件"来封装变化 (比如说,每负责一个子模块的人都奢侈地拥有一个自己的配置文件), 否则我不认为这是一个足够的理由. 另外,即使这样可以,我也反对在系统里面引入这么多层的子配置文件. 这是一个系统的噩梦. 如此发展下去,你会把整个java语言给你的oo弹性全抛弃而应用配置文件来封装变化, 这明显是个错误的方向. 引用 既然脑袋短路的老板花了100万买了一个实现, 那么偶修改一下组装代码就好了, 老代码你看着不爽就删除掉呗. 反正客户的代码只知道从容器中获得的是一个interface而已, 更本不用做啥改动, 哪里来的重复代码?
既然如此,那么以你的方案可以不改动原来的代码作为资本就没有意义了.我的前提是"重构", 重构就是要修改不合理的代码. 所以所谓"原来的代码不用改"不能算作什么优点. 引用 这个没有办法, 偶宁可强调这个小小的纪律, 也不愿意把构造函数变成private (否则, 你还想不想让其他人扩展你的类呀......)
在实际开发中, 偶还没有发现有谁那么勤劳: 放着现成的测试用例(从container获取interface)不抄, 反而非要找到这个interface的某个实现, 然后再new出来玩....... 呵呵. 我曾经说过,我很少希望别人继承我的类, 我的类基本都是final的.当然,你可以不同意. 这是另外一个话题了. 但是, 这仍然是个别程序员的选择. 除非你能从整个软件工程的角度证明不允许继承永远是错误的,你这个批评就没有打中要害. 关于实际开发中,你没有发现...云云, 呵呵, 你的经验是普遍真理吗? 如果不是, 我宁可不去假设大家都永远不会直接new一个东西出来. 我的方案的好处在于它没有任何假设.你可以找到这个interface的某个实现,然后instance()出来,也可以从container获取, 你的自由. 我作为一个子模块的设计者不会去强迫你. 引用 youcai和xangd同学已经批评过你的静态工厂方法的代码依赖性强的缺点了, 偶就不唐僧了...... 依赖性强? 谁有本事跳出来给我证明静态工厂比构造函数依赖性强? 你用构造函数的可以用配置文件, 静态工厂的就不可能? 也许某个容器现在不支持, 这应该算是静态工厂的一个现实中的弱点,但是也象我试图表达的, 也是这个容器的一个缺点. |
|
| 返回顶楼 | |












