3.1 探索DSL集成
DSL是一种优美的抽象,它也毫不例外地需要与应用程序架构中的其他组件集成。在一般的使用场景中,DSL所建模的制品属于应用程序中易变的部分,比如业务规则和配置参数。所以DSL和应用程序主体要能够以不同的步调各自独立地演变,同时它们又能够与工作流无缝地结合,这两点都是非常重要的设计要求。
这一节,我们将探索无缝集成DSL的各种途径。一种DSL本身只针对一个专门的领域,却有可能在多个更大的领域中使用。至于实际上是否被广泛使用,还要看它所针对的领域的通用程度。例如,一种处理日期时间的DSL在任何需要计算日期的程序中都用得上,而一种针对企业税收规定的DSL用途就非常有限了。另外,因为日期DSL要考虑集成到多个应用程序上下文的情况,所以它要具有更强的适应性。
我们稍后便深入介绍DSL集成问题,在此之前,请看图3-2,一个DSL驱动的应用程序架构大体上就是这个样子。
图3-2 宏观视角下基于DSL的应用程序架构,注意DSL与核心应用程序的解耦情形。不同部分的演变时间表各不相同
对于典型的多层架构,DSL可以用在任何一层,只要它准备好该层要求的集成上下文信息就行。集成内部DSL相对容易,因为内部DSL一般与应用程序使用同种语言,且被设计为库的形式。集成外部DSL较为麻烦,需要建立某种插入机制,对外公开专门的端口给应用程序对接。我们不急着列举内内部和外部DSL集成到应用程序架构的具体用例,先来谈谈为什么需要小心处理DSL的集成问题。
为什么关心DSL集成
核心应用程序对内有各种组件要连接,对外有DSL脚本要插入,两者都需要你小心处理。DSL的演变步调独立于核心应用程序,所以两者之间的耦合程度要恰如其分。
如果选用Groovy、Ruby、Scala等表现力强的语言作为核心应用程序的主要语言,基本上不存在什么集成问题;根本就没必要插入其他语言的DSL脚本。所以,接下来的内容主要与在Java应用程序中集成DSL脚本的情况有关。
开发者很容易鲁莽地决定在一个应用程序内使用多种语言的DSL,却忘了想想到时候要怎么集成。如果选错了DSL的实现语言,图3-2中好端端的架构可能会变成开发者的噩梦。没有人希望落到好像图3-3所示那般境地。
图3-3 架构师如坐针毡,苦思冥想怎样把不同语言写成的DSL跟核心应用程序集成。你能帮他解除这个定时炸弹吗
如果想做到DSL与应用程序无缝集成,不想陷入图3-3中那位仁兄的境况,那么你就必须好好考虑表3-1列举的几个问题。
表3-1 将DSL集成到核心应用程序
待解问题 | 架构师应做事项 |
---|---|
关注点分离 一方面是DSL要解决的核心问题,一方面是DSL要应付的应用程序上下文,怎样保证两者之间泾渭分明? | 正确定义DSL的适用上下文,并考虑DSL在当前应用之外可能的应用场景 参考附录A中作为一个核心特质探讨的抽象设计原则:精炼 |
DSL API的演化 DSL API的演变要独立于应用程序上下文 | 确保API在演变过程中维持向后兼容 如果使用第三方提供的DSL,一旦发现它的API引入了不兼容的改动,就要立即警觉起来,否则说不定什么时候会被“反咬上一口” |
避免语言摩擦 各种DSL所用的语言太多,会给整个架构的开发和维护造成混乱 这不仅是技术问题,还是人员问题。当程序员被迫维护太多种语言写成的代码,他们会失去合作的意愿 | 确保DSL的实现语言可以跟应用程序的宿主语言无缝地互操作 宁可部分牺牲DSL语法的灵活性,也不应选择无法很好地集成到核心应用程序的语言。不能因为不同语言运行在相同虚拟机(VM)上就认为其间能无缝地互操作,请记住这个提醒 如果DSL的实现语言可以通过多种途径与应用程序的宿主语言互操作,请优先选择具有最自然集成方式的一那种;基于沙盒的脚本环境虽然是一种通用的集成途径,只宜作为后备选项。我们将在3.2节以Groovy和Java互操作为例来说明各种集成方式 |
你已经知道为DSL和核心应用程序制定集成策略的必要性,接下来该了解一下各种策略的使用模式了。首先说明的是内部DSL的集成模式,内部DSL主要做成库的形式,以库中API的形式进行集成。