软体艺术系列--抽象工厂 (原文最终修订于2006年10月18日 凌晨04:25:06)

2002年三月四日,星期一早上11点钟...

我可不想还像上周五那样迟到,于是就和大家一同走入会议室,而且看见Adelaide正要准备开始演说。我们的小小团队成长的迅速。有几位同事我还都没见过面。数了数,房间里座着25位成员,Jean也在其中。

为了引起注意,Adelaide敲了敲桌子,大家也都坐定。她很显然,有些紧张,不过也信誓旦旦。于是先镇定了一下,然后就开始了演说。

“大家好,Jasmine让我谈谈Abstract Factory模式,我和她在SMCRemote的项目中还刚刚使用过此模式,所以我想还是能就此谈些看法。我的理解是这样的:”Adelaide开始在黑板上划下了以下内容:

“除了后来又增加的同时支持Java和C++代码的功能之外,以上就是SMC编译器的原始设计。稍候我要展示的是我们正在设计之中的plug-in架构,此架构通过创建CodeGenerator类的派生类就可以增加新语言的支持到这个编译器中。不过,大家也可以在当前的这个设计中看出,要让编译器能够相创建匹配的实例,它就必须要直接的依赖于这些派生类。”

“于是,我们打算用Abstract Factory模式来打破这个依赖,并将Compiler类和CodeGenerator的派生类隔离开来。”

很明显,Adelaide是演练过这段内容的,以至于她过得非常流畅,甚至有些快了。Jerry一定是已经注意到了这点,因为就在Adelaide转身擦黑板的时候,他就打断了她,并且问了个显而易见的问题。

“Adelaide,为什么编译器知道JavaGenerator和C++Generator会是个问题呢?我倒觉得这些依赖非常必要啊。毕竟,Compiler类仅仅是去new这些类,之后就都通过CodeGenerator接口来使用它们了,不是么?”

Adelaide不安地看了一眼Jasmine,深吸了口气,然后就直接回答Jerry的问题。

“我们不希望在Compiler和CodeGenerator的派生类中存在任何的依赖那是因为不想一旦派生类发生变动的时候我们都要去重新编译Compiler。而CodeGenerator就是用来使Compiler类和这些改变相隔离的。因为关键字new就失去了隔离的功效,太不应该了吧。”

我对刚刚的讨论感到费解,问到:

“为什么派生类的改变就会强制Compiler类重新编译呢?”

Adelaide这时显得有些慌乱了,她看了看Jasmine。于是Jasmine起身支援她,并以坚定的目光看着我。

“好,真是热门话题,我们上周在VISITOR模式讨论刚刚讨论过这个。所以我想大家都知道答案了。不过因为房间里的其他人,我还是解释一下。我们的编译系统会根据文件的修改日期来决定是否重新编译该模块。因为Compiler依赖于JavaGenerator,如果JavaGenerator.java比Compiler还新的话,编译系统就会自动的重编译Compiler。Alphonse对么?”

“嗯,没错,谢谢。”

我的一个老同窗Alex站了起来,然后说道:“好,是的,但你可以通过class.forName的方式来解决啊。”

“是的,是可以”,Jasmine回答说,“但是我们不希望Compiler中存有那种细节的内容,我们要努力分离重点。”Jasmine以挖苦的口吻拖长了最后两个字,这迫使Alex座了下来。

Joseph站起来说道:“Jasmine,Alex问得有道理,你不必用这种方式让他坐下。”

Jasmine轻叹了一声,她的肩膀也放了下来。然后她说:“好,Joseph你是对的。Alex,我为刚刚的突兀感到抱歉。我们当下的目标是要确保Compiler不关心这些派生类是如何创建的,或者说,实际上就是有关派生类的任何细节。这样想是因为我们认为派生类的构建机制会随时间而改变,而我们不希望这些改变对Compiler有影响。我们希望Compiler就是个编译器,而不是别的什么。”

Jasmine做了下来,我看到这回她的视线转向了Jerry。

Adelaide转过身,把黑板上残留的图案擦掉,然后划出了如下的图:

 

然后,她又训练有素的继续演说着。“这是我们的计划方案。它是典型的Abstract Factory模式。注意GeneratorFactory接口。Compiler类调用此接口某个makeXXXGenerator方法,作为回应,RealGeneratorFactory会构建出相应的实例并返回给Compiler。接下来看看红线圈住的图案...”

“不好意思,亲爱的”,Jean打断说,“不过这幅UML真是让我感到看得头晕啊,我看我还是太古董了,想看看代码究竟什么样子。你是这么可爱的女孩儿,用一些代码来展示一下好吗?”

看了看Adelaide的脸,很显然,这应该在她演讲内容的预期之外。不过,看来她还是抱有信心,颇为振作,也或是与Jasmine又通过眼神。稍微停顿了一下,然后她就开始在黑板上写:

public interface GenratorFactory {

    public CodeGenerator makeCppGenerator();

    public CodeGenerator makeJavaGenerator();

}

public class RealGeneratorFactory implements GeneratorFactory {

    public CodeGenerator makeCppGenerator() {return new CppGenerator(); }

    public CodeGenerator makeJavaGenerator() {return new JavaGenerator(); }

}

“清楚么?”她颇为礼貌的问道。

Jean几乎没有太看代码,然后满意的说,“是的,亲爱的,很好。”

Avery在我耳边细语:“Jean只是想确信Adelaide知不知道该怎么写这些代码。”

我表示同意,但没吭声。

“好,现在看看红线所圈住的部分吧。”Adelaide继续说,“红圈之内的就是我们的plug-in架构,之外的都是plug-in。任何在红圈之内的属于核心引擎部分。注意,所有的依赖是怎样指向红圈之内的!我的意思是,红圈之外元素的改变不会影响到红圈之内的。比如,如果一个CodeGenerator的派生类发生了改变,圈内元素是无需重新编译的。”

“鸡毛蒜皮的倒是做了不少!”Avery已经站起身来慷慨激昂的说着,“这个结构和Visitor所遇到的问题没什么差别。在GeneratorFactory和CodeGenerator的派生类间有循环依赖。每次你增加一个新的CodeGenerator派生类,你都要为GeneratorFactory接口增加一个新的make方法。也就是说,你们的红圈中的元素还是需要重新编译。所以,整个模式毫无价值!”

Avery骄傲的站在那里,与此同时,Adelaide看起来都快哭了。Jasmine掸了掸鞋上的土,“Avery,你有点儿...”

但就在Jasmine开口的时候,Jean站了起来说:“Avery,你跟我来好吗。”Jasmine挣大了眼睛,她的眉毛也竖了起来,然后她再次坐下了。Avery的脸色苍白,Jean招呼着他向大门走去,两位走后会场陷入了不自然的沉默之中。

这不自然的静默持续了许久,然后Jasper开口了:“噢,上周讨论这些的时候还好好的呢。”有人开始笑了起来,顿时缓解了紧张气氛。

Jerry起身,“Avery本可以说得更好的。不过他也提出了观点,不是么?一旦增加了新的CodeGenerator,你们确实必须要修改并重新编译核心引擎。”

Adelaide盯着黑板,擦拭着眼泪,看来是无法回答问题了。Jasmine最终站了起来说:“是的,那是设计的意图所在。我们料想不会太过频繁的新语言增加以至于影响我们。我们努力分离的是核心引擎和现有CodeGenerator派生类的维护性改变。”

Jared站了起来说:“好,那是足够了。但如果你确实要对语言的增加做出保护措施呢?那时你会怎么做呢?每当增加一个新的CodeGenerator派生类的时候,你该如何防止重新编译核心引擎呢?”

屋子里沉寂了几秒钟,之后我站了起来。

“我在过去的几天里都在考虑这个问题,因为我们在Dtrack项目中也遇到过类似问题。我们不希望每增加一种太空服的时候就重新编译一遍核心引擎。”我擦掉了黑板上的内容,然后画下了一下部分:

“避免万一Jean再要求,我会直接配上了代码的。”

public interface GeneratorFactory {

    public CodeGenerator makeGenerator(String type);

}

public class RealGeneratorFactory implements GeneratorFactory {

    public CodeGenerator makeGenerator(String type) {

        if(type.equals("C++")) return new CppGenerator();

        if(type.equals("Java")) return new JavaGenerator();

        return null;

    }

}

“现在设想字符串“C++”和“Java”作为Compiler类的命令行参数。你们可以看出,我们可以毫无顾虑的增加新的CodeGenerator派生类而无需重新编译或是影响到核心引擎。”
Jasmine说:“但还是换汤不换药啊,这样类型就不安全了!如果拼错了字符串呢?”

“难道你的单元测试不会发现那些错误拼写么?”

“噢...”

Jerry补充说:“我想Jasmine的意思是,我们更希望编译器能帮我们发现问题。”

“为什么呢?”我回答说,“我的问题还是,为什么你不用单元测试来确保你到底有没有拼写错误呢?”

“好吧,但这是一个命令行参数!”

“没错,但如果你输错了一个命令行参数,难道就不会得到一个错误信息的提示吗?在这个例子里,Compiler引擎会告诉你:'Ruby是不支持的语言'。”

“好吧,很好,但如果你从脚本或是其它环境中调用Compiler,并且这时候出现了拼写错误,那编译器是不会警告你的。”

“没错,但还是那句话,你的单元测试会查出来。”

“哦...”

 很有意思,好像Jerry、Jasmine从未想到过这点。确实,屋子里的资深人士都露出了这种表情。

“为什么这会是个问题?”我问道。

Jasper说:“哦,我不知道其他人如何,但我在想类型安全到底是不是值得我们去考虑。我们在过去两年来所做的这些单元测试也许是让类型安全的考虑显得多余了。”

我看到几位点头,还有几位摇头。但在我离开房间的时候,我还在回味着这件事情...(本文待续)

 

 

 (原文链接网址:http://butunclebob.com/ArticleS.UncleBob.CraftsMan49; Robert C. Martin的英文blog网址: http://www.butunclebob.com/ArticleS.UncleBob 

作者简介:Robert C. MartinObject Mentor公司总裁,面向对象设计、模式、UML、敏捷方法学和极限编程领域内的资深顾问。他不仅是Jolt获奖图书《敏捷软件开发:原则、模式与实践》(中文版)(《敏捷软件开发》(英文影印版))的作者,还是畅销书Designing Object-Oriented C++ Applications Using the Booch Method的作者。MartinPattern Languages of Program Design 3More C++ Gems的主编,并与James Newkirk合著了XP in Practice。他是国际程序员大会上著名的发言人,并在C++ Report杂志担任过4年的编辑。

 

你可能感兴趣的:(C++,单元测试,interface,compiler,引擎,编译器)