最后介绍的设计模式,是另一个常被误解的模式,它就是建造者模式(Builder Pattern)。传统上对它的介绍是,它用于将对象的构造与其表示分离开来。
问题提出
打开MACOS的keynote,创建一个演示文稿,然后将其导出为某种格式。支持PDF、PowerPoint、图像等格式。
用代码实现。
创建类Slide代表幻灯片,属性text模拟其上的文字。
创建类Presentation代表演示文稿,它由一些幻灯片组成。属性slides代表幻灯片列表,方法addSlide用于添加幻灯片。
现在来实现导出export方法。先添加枚举presentationFormat来表示演示文稿的导出格式。
创建PdfDocument类代表导出的PDF文档。
创建Movie类代表导出的影片。
现在来看一下导出方法(export),根据传递的参数(format),导出相应的格式。如果要导出PDF格式,就创建PdfDocument文档并将演示文档的幻灯片添加到PDF文档。如果要导出Movie,就创建Movie对象并将各幻灯片添加到Movie对象的帧里。
这种实现有很多问题。
首先,违反了开放闭合原则。明天如果要支持导出新的演示文稿格式,我们就得回来修改export方法。
其次,Presentation类与PdfDocument、Movie类耦合太紧密了。支持的格式越多,就越增加这种耦合。
其三,Presentation类需要了解其他类的结构和用法,比如PdfDocument的addPage方法,Movie的addFrame方法。格式越多,需要了解的知识就越多,而实际上这些知识不应由Presentation类来了解。
第四,export方法的代码有重复。对于PDF格式,我们遍历所有幻灯片Slide,然后添加进PDF文档;对于Movie格式,我们同样遍历所有幻灯片,然后添加到影片的帧里。如果我们要添加版权信息,那么所有格式的代码都要修改,比如PDF格式的代码要添加pdf.addPage(“Copyright”),而Movie格式的代码也得加一句movie.addFrame(“Copyright”,3)。
解决方案
前面说过,建造者模式将对象的构造与其表示分离开来。
在这个例子,构造是演示文稿的导出逻辑,而表示则是目标的格式(比如PDF、影片),所以在这里我们要使用建造者模式将导出逻辑与表示的格式进行分离。同样的导出逻辑应用到不同的表示格式。
定义接口PresentationBuilder,它有方法addSlide。我们可以创建该接口各种不同的实现,比如PdfDocumentBuilder、MovieBuilder。PdfDocumentBuilder只知道如何表示PDF格式,而Movie只知道表示影片格式,它们实现自己的addSlide方法。
现在让演示文稿Presentation与PresentationBuilder通话。
如图,演示文稿Presentation将不再与具体的PdfDocumentBuilder、MovieBuilder等耦合,而只与接口PresentationBuilder通话。
在Presentation的export方法里,对于每一张幻灯片slide,调用实际的builder进行构造具体的表示对象。
export方法里构造演示文稿的逻辑属于构造逻辑,而在具体的PdfDocumentBuilder里的逻辑则属于表示逻辑,用于将演示文稿表示为PDF格式。MovieBuilder的逻辑则用于将演示文稿表示为影片格式。当然还可以有其他Builder。
我们再添加一些东西。在PdfDocumentBuilder中,当创建了一个Pdf文档时,需要立即能访问它,我们添加一个方法getPdfDocument。同样的,在MovieBuilder中我们添加另一个方法getMovie。
这就是建造者模式(Builder Pattern)。我们再强调一次,这种模式的目的是将对象的构造与其表示分开。很多人不明白这种模式的真正目的,会出现一些误解。
经典的结构如下。
显然,Director类的construct方法负责构造逻辑,而具体的Builder(ConcreteBuilder)实现buildPart方法负责实际的表示。
代码实现
创建接口PresentationBuilder,定义addSlide方法。创建类PdfDocumentBuilder和MovieBuilder,实现addSlide方法。PdfDocumentBuilder添加私有属性document表示PDF文档,addSlide方法里将幻灯片添加到PDF文档。Movie Builder添加私有属性movie表示影片对象,addSlide方法里将幻灯片添加到影片帧。
现在到了有趣的部分,修改export方法。参数改成PresentationBuilder对象builder,去除丑陋的判断和重复代码。遍历幻灯片,简单调用builder.addSlide即可。
Main函数测试。新建presentation对象,添加测试幻灯片Slide1 和 Slide 2,然后进行导出,指定导出参数为PdfDocumentBuilder。
有一些小问题,目前Presentation的export方法返回值是void,实际场景是需要能立即访问导出对象的,但又不能将返回值改成PdfDocument或Movie,这样就是具体耦合了。
我们给PdfDocumentBuilder添加getPdfDocument方法,给MovieBuilder添加getMovie方法,用于获取相应的导出对象。
Main可以做以下修改,用于访问导出的对象。
如果想导出Movie格式,只需将MovieBuilde传给export方法即可。
回顾下,如果我们要修改导出的逻辑,只需在export方法这一个地方修改即可,如果要支持新的导出格式,我们不必修改export的代码,只需添加一个PresentationBuilder即可。
小结
建造者模式用于将对象的构造与其表示分离开来。
结束语
终于,我们来到了本系列的结尾,非常感谢您的关注。
通过我们在本系列中讨论的概念和模式,希望能够帮助您从不同的角度思考软件的构建。任何人都可以编写代码,但编写易于维护和扩展的优秀代码是优秀软件工程师与普通程序员的区别。
很多人不喜欢面向对象编程,这些人可能从一开始就没有正确学习面向对象,这就是为什么他们喜欢函数式编程,因为它至少对初学者来说更容易掌握。
不要误会意思,函数式编程很棒并且有很多用户,但不能仅仅因为尝试过面向对象编程并失败了,就说它不好或没用。