中国开发网: 论坛: 程序员情感CBD: 贴子 675684
sealw: 设计决定、反悔、霰弹式修改和架构污染
设计决定、反悔、霰弹式修改和架构污染
——浅谈Google Guice的用法
王海鹏(sealwang@gmail.com)

设计决定
“好吧,在这一个项目中我们就使用Google Guice!”我们坚毅、果敢、英明神武的首席架构师宣布了这个决定。
为什么不呢?依赖注入是这么成功的一个概念,我们使用Spring作为依赖注入框架已经好多年了。而Google Guice又给我们带来了许多的改进。
首先,Guice抛弃了XML配置文件,这使它具备了“重构友好”的特征。在Guice中,我们通过Java代码来说明接口(服务)和实现类的绑定,例如下面这一段代码:

public class MyModule extends AbstractModule {
protected void configure() {
bind(Service.class)
.to(ServiceImpl.class)
.in(Scopes.SINGLETON);
}
}

这样,当我们需要重构(修改)接口或实现类的名称时,就可以利用集成开发环境提供的重构功能来完成,不需要再手工修改XML中的类名。在重构深入人心的今天,我们不希望改一个类名还需要额外的工作。
第二,在抛弃了XML配置文件之后,应用程序就不再需要一个XML解析器了。这样,Guice就比Spring“瘦身”了不少。高手们总是对减少资源的占用怀有特殊的爱好,不是吗?Guice 1.0的Jar包只有544K(高手可能会认为还是大了一点),不依赖于其他的Jar包。另外,由于不需要读取并解析XML配置文件,执行的速度也提高了不少。
第三,Guice使用了JDK 5的新功能,其中一项就是泛型。我们不再像以前使用Spring那样进行强制类型转换了,我们有了编译时刻的类型安全检查,而且代码也简化了。我们像下面这段代码那样取得组件实例:

public class MyApplication {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new MyModule());
Client client = injector.getInstance(Client.class);
client.go();
}
}

第四,Guice使用了JDK 5的另一项新功能,annotation!这个词太可爱了,听起来和innovation有点相似,以至于我们一时间不知道如何将它翻译成中文。Guice提供了近乎神奇的@Inject annotation,满足您各式各样的注入需要!(我相信这些注入方式都是在Martin Fowler的那篇著名文章中提到的。)
我们可以实现构造方法中的注入,像下面这样:

public class Client {
private final Service service;

@Inject
public Client(Service service) {
this.service = service;
}

public void go() {
service.go();
}
}

也可以对属性直接注入,甚至不需要设值方法,像下面这样:

public class ServiceImpl implements Service {
@Inject
Emailer emailer;

public void go() {
// Some expensive stuff.
...
// Send confirmation.
emailer.send(...);
}
}

第五,Guice有在真实应用中成功使用的历史。据说,它曾在Google的广告应用程序中使用,而Google的广告应用程序可能是目前世界上规模最大的应用程序!
最后,更重要的是,品牌就是品质。Google是如此受人尊敬的公司,吸引了许多重量级的人物加盟,已经成为许多IT人士向往的地方。使用Google的荣誉产品,就像使用JDK自带的包那样可靠,我们还有什么可担心的?据说Spring的新版本也向Guice学习,使用了annotation。模仿就是一种肯定,不是吗?

反悔
“新郎,你愿意娶新娘为妻吗?”
“是的,我愿意。”
“无论她将来是富有还是贫穷、或无论她将来身体健康或不适,你都愿意和她永远在一起吗?”
“是的,我愿意。”
“将来看到更漂亮、更好、更合适的,你也不会改变今天这个决定吗?”
如果理性地回答,答案就太不浪漫了。因为未来不可预见的事情实在太多。好在热恋中的人们倾向于不那么理性,所以答案是“永远。如果非要加上一个期限,我希望是一万年”。
我们的架构师正在与Guice开始一段新的感情。这种感觉太好了,上次有这种感觉好像还是在刚使用Spring的时候,那已经是几年前的事情了。现在他压根没想到以后可能会像抛弃Spring一样抛弃Guice。
直到有一天,一个刚刚参加工作的小兄弟问了他一个很傻的编译错误。他告诉这个小兄弟,要使用@Inject,必须这样:

import com.google.inject.Inject;

小兄弟满意地继续他的开发,但我们的架构师却觉得有一丝不祥的感觉掠过,空气中似乎飘荡着一种坏味道。
他检查了一下项目源代码。在第一次迭代中,我们写了50多个类,其中大约有一半都包含了@Inject!这种情况不妙,简直太不妙了。这违反了他多年的经验所建立起来的原则信仰。
这违反了Larry Constantine的“高内聚、低耦合”的原则。这条伟大的原则如此简单而深奥,以至于不断受到人们的践踏。
虽然“疯狂的Bob Lee”为Guice设计出了极小的API接口,但在使用@Inject这一点上,却让人不太赞同。他似乎鼓励我们在自己代码的各处插入@Inject。
当我们把一个设计决定分散到代码的不同地方,它就会成为难以改变的既成事实。即使是对于JDK自带的类,我们也不能这么做。我们不能在所有的业务类中都引用JDBC的包,使用Connection、Statement和RowSet。相反,我们应该把它们放在持久层中。也许,我们应该使用Hibernate这样的框架,将它们封装起来。也许我们还需要再封装一层,设计一个PersistentLayer接口和一个PersistentLayerHibernateImpl实现。这样,如果我们以后有新想法,只要再写个PersistentLayerJdbcImpl或PersistentLayerDb4oImpl或PersistentLayerIbatisImpl或PersistentLayerHibernateAndIbatisMixImpl。
如果我们把使用Guice这个设计决定作为一种默认的假定,让它散布在代码的各个角落,那么将来我们要反悔的代价就非常大。实际上,我们就不能反悔了。
我们会反悔吗?我们需要反悔吗?不能反悔的决定是我们可以承受的吗?
我们的架构师做出了一个痛苦的决定:我们需要能反悔。毕竟,作为一名架构师,他的全部声誉就在于设计出有弹性的架构,可以容纳将来可能的变化。

霰弹式修改
我们的架构师是经验丰富的。办法总比问题多,不是吗?关键是要发现问题。根据“所有的问题都可以通过添加一个间接的中间层来解决”这一原理,他设计了一个Factory,这是一个全局的Singleton:

public class Factory {

private static Factory factory;

synchronized public static Factory getInstance() {
if (factory == null) {
factory = new Factory();
}
return factory;
}

private Injector injector;

private Factory() {
injector = Guice.createInjector(new MyModule());
}

public <T> T getInstance(Class<T> type) {
return injector.getInstance(type);
}
}

然后,在每个用到服务依赖注入的类中,在构造方法里加上赋值语句:

public class Client {
private Service service;

public Client() {
this.service = Factory.getInstance().getInstance(Service.class);
}

public void go() {
service.go();
}
}

注意,我们没有为service属性加上final关键词,这样您仍可以有setService()方法,可以利用Mock对象对Client类进行单元测试。
剩下的工作就是从项目已有的代码中去掉所有的@Inject。这是一种“霰弹式修改”,不幸中的万幸,我们只要修改20多个类,而不是200多个、2000多个。
由于有自动化的测试类作为保障,这种改动很快就完成了。如果以后我们想换一个依赖注入框架,就不需要“霰弹式修改”了。我们甚至可以很容易改回用Spring,当然,目前还没有这个必要。
再见了,@Inject!

架构污染
不久后的一天,我们的架构师在浏览代码时,惊奇地又看到了@Inject!
原因很快查到了,一个刚刚结束了婚假回到开发团队的开发人员,因为错过了关于这次修改的讨论,重新在代码中引入了@Inject。
我们的架构师设计出的精美架构受到了污染。这种事情以前也发生过,似乎总是有各种各样的原因,导致架构在实现者手里变形。我们的架构设计师能设计故事的开始,却猜不中故事的结局。这种情况是他所痛恨的。
“我们需要有一种手段,能对架构的实现进行监控。”架构师在开发例会上说,“这不是不相信我们的程序员,人都可能犯错的。”
“信任,但要核查。”里根在签署核裁军条约时曾这么说。
一个对敏捷软件开发颇有研究的高级程序员说,“我最近看了一本书,书名是《持续集成》,书中提到了一个工具,好像叫JDepend,可以显示项目中的包依赖关系。正适合解决这个问题。”
后来的故事简单了,通过重构,我们将Factory放在了单独的一个包里,只有这个包才对Guice产生依赖。我们的架构师经常地查看JDepend的输出,看着他设计架构一天天地实现。

尾声
我们的架构师盯着他的二十二寸宽屏液晶显示器,回味着简洁优美的架构所带来的快感。一个念头在他脑海中闪现:“如果Guice不提供@Inject相关的功能,那Jar包就会更小了。”
Guice是开放源代码的,如果有时间,我们的架构师将替他的“新娘”瘦身,去掉@Inject相关的所有类,让这种架构污染永远也不可能再发生。但是现在,还有更多的项目工作等着他。也许等他忙完这段时间,会再来折腾一下这个问题。
生命在于折腾,不是吗?

相关信息:


欢迎光临本社区,您还没有登录,不能发贴子。请在 这里登录