如何为一个现有的目标和平台体系结构构建一个合适的域体系结构。本章将着眼于构建DSL、构建变换体系结构的通用的最佳实践和几个侧重技术的详细内容。
一、DSL构造
1、选择一种合适的DSL
在尝试为某个域寻找DSL时,总是应该考虑需要表达的可变性,因此在从事一种健全的类似图形的文本或图形化DSL之前,应问一问自己更简单的DSL形式是否充分。
如果只需要完成常规的配置,那么简单的属性文件或向导可能就足够表示描述一个产品所需要的可变性了。如果需要更多的自由--必须从中进行选择的可变空间变得更大--平滑的或基于树的配置会更加适合,同时特征模型成为遵循一种从根本上面向配置的方法的功能最强大的DSL。
构建DSL的各种方法
2、配置和构造--变型形式
现在可以看看两种DSL:支持创造性构造的DSL和提供配置手段的DSL,将这两者结合在一起时很有趣的。
使用元模型可以定义所有种类的数据结构。
现在假设想描述模型的变型形式。例如,在人和地址的背景下,下面的变型形式可能是有意义的:
- 一次聚会可以有一个或多个地址;
- 一次聚会可能会,也可能不会存储电话号码;
- 建设是电话号码,希望存储国家代码;
- 地址可能有一个state字段,以在美国为代表。
为了表达这些变型形式,可以把一个特征模型---一种常规的配置DSL--覆盖在用一种创造性构造DSL构建的模型上。在特征模型上选择或者取消某种特征会导致包含或移除某个模型元素。必须将结构模型中特定的模型元素和配置模型中的特征联系起来。
3、建模行为
DSL描述了一个软件系统的结构化方面,如组件结构、持久化映射或输入格式。不过,大多数系统还有需要被建模的结构化方面。
最简单的对行为进行建模的方式是如果必要,将行为简化为简单的描述性标签。
另一种相对简单的描述行为的方法是使用一种著名的形式主义指定特定种类的行为。那种方法的经典例子是状态图,其他的例子
4、具体语法问题
从模型变换或代码生成的角度出发,只要有办法把它变换为与生成器一起工作的抽象表示法,而且只要元模型恰当地表述了应该用DSL建模的概念,DSL的具体语法就不是真正的问题。不过DSL是元模型的“用户接口”-即,应用程序的开发者必须能够正确地阅读、编写并理解模型。这是一个不能被忽视的非常重要的问题。毕竟,MDSD的思想就是提供更多表达域概念的有效手段:这种效率在很大程度上取决于具体语法。
在考虑具体语法的时候,记住一般不是把模型“画”在一个活动挂图上,而是把它画在一个交互式的编辑器中。这意味着可能具有一些特征,如放大、平移、情景菜单和按钮或者随意折叠。
5、元模型的连续式验证
简单的组件元模型
阐述了域的正式元模型的发展和连续式验证的重要性。必须在域专家们的帮助下完成对元模型进行的检查。开发团队必须能够平稳地使用它而且不能产生误解。
一旦存在不能用元模型的词汇轻松表达的事物,这就意味着这种表达并不符合元模型,元模型有错误,或者它是不准确的。
这种技术在域建模中非常流行。这种技术成为无处不在的语言。
二、一般变换的体系结构
1.应该生成目标体系结构的哪个部分
在域体系结构背景下,总是会生成那些工件,一方面它们不被包含在平台内,另一方面不能用DSL对它们进行很好的和简介的描述。
典型的机遇编程语言的构件的引言表明“生成”已经研究得很深入,如DSL内的环。DSL应该主要是便于陈述的而且不会变异为一种经典的编程语言--如果出现了这种情况,那么实际上应该使用一种编程语言并在生成的代码中集中手写的代码。
2.相信再生
应该用一种包括所有生成的/变换的工件的再生的全自动构建过程创建最后实现的应用程序。。一旦需要一个单独的手工步骤作为业务流程的一部分,或者在重新生成之后必须手工修改一行单独的源代码,在MDSD放弃支持传统的非生成开发之前,这只是一个时间的问题,因为手工修改生成的工件时单调和易错的。
这并不意味着应该--或必须--100%地生成一个应用程序。只要3GL适用于这个目的,完全可以用3GL继续实现它的一部分。这个最佳实践只说明了生成的那些部分必须时被完整地生成,以便可以在一个循环中重新创建完整的系统。随后对生成的工件的修改是不受限制的。
3、开发模型
为了避免重复并最小化手工劳动的数量,应该尽可能多地发掘一个模型中包含的信息。这意味着通常要生成的不止是源代码:构件和配置脚本、用于组件测试的骨架/固定设备,甚至可能还有测试实现,测试数据和模拟对象、数据库定义脚本、数据移植和群脚本,用于控制和对数据进行维护或部分文档进行测试的简单GUI。
1)组成配置的生成
DSL可以被用作一种架构的“配置语言”,所以在一定范围内架构和DSL是彼此适合的。对于复杂的平台来说,这些配置通常不是源代码,而是目前经常用XML表示的广义的配置文件。在大多数情况下,可以由模型轻易地生成这些配置,因为常常已经包含了必要的信息。
2)支持系统的体系结构
只关注软件的体系结构是不能构建任何系统的。总是需要一个系统的系统结构对现有的机器、过程以及给它们分配的软件进行说明。
4、生成漂亮的代码--只要可能
假设开发者永远不会看到生成的代码是不现实的。即使开发者不需要改变生成的代码,例如通过插入手写的部分,如果它们用传统的工具调试生成的应用程序,或者如果它们必须检查生成器的配置,它们还是会面对它。
使用这种最佳实践对于帮助开发者接受代码生成是很重要的。
支持这种最佳实践的基本声明不但适用于生成的代码,而且也适用于手写的代码:源代码不再主要是为机器缩写,而是为了供其他源代码的使用者--人--阅读。
5、模型驱动集成
在很多情况下,必须将自己用MDSD开发的系统集成到现有的系统和基础设施中。实际上被隔离的软件项目是罕见的。在大多数情况下,软件时在现有的系统环境中被开发的,即在前面还有剩余的生存期。此外,随着时间的过去,开发者常常希望用更新、更好的合适的功能逐渐取代传统系统,该功能应该用现代的技术实现。
集成一般包括各种API的系统性映射,以及需要的数据和这些API之间的协议变换。根据集成策略,必须在要开发的应用程序中,或者在要集成的现有的传统应用程序中插入集成代码。需要的工件常常包括一次性使用的恰当的数据变换脚本。
6、分离生成和非生成的代码
在只生成应用程序的一部分的时候--通常是现在经常会出现的情况--那些缺口必须用手写的代码填平。但是,修改生成的代码隐藏了很多有关一致性、构建管理版本控制和模型与生成的源代码之间的一致性的问题,这在很大程度上是由于重复的再生,即使最后的一个方面是由--从一种专有的技术角度出发--现代的生成器所控制的。
如果永远不修改带有生成的代码的文件,那么如果必要,可以轻易地删除生成的代码,而且可以进行一次完整的再生。如果生成的代码必须被手工修改,那么这也许只能出现在那些被特殊指定的地方,这些地方在再生期间是不会被重写的。
7、模块变换
为了使变换的重用成为可能,建议对变换进行模块化。根据使用的变换语言,这可以用结构化的或面向对象的编程概念完成,就像在经典的编程中一样。其中有子程序/过程、类或当时负责生成目标体系结构的不同层或不同方面的耦合松散的组件。也可以个别地对它们进行搅浑。这些结构化的方式通常是特定工具的,不过是非常重要的。
8、层叠的模型驱动开发
模型到模型的变换对一个变换过程进行模块化是有用的。不过,一个潜在复杂的变换过程应该被分解为哪些步骤这个问题仍然存在。MDA使用象征符号PIM和PSM来为这些决定提供指导:应该在PIM中对自己的业务逻辑进行建模,然后把它转换为PSM,最后由它生成代码。
首先从以体系结构为中心的MDSD开始,这意味着为自己的软件体系结构提供MDSD支持。元模型包含体系结构的基本构件和从一种技术的角度描述系统的模型。一旦已经构建了这种基础设施,就可以在它上面叠加MDSD-基础设施的其他层。
三、建立变换的技术方面
1.生成的代码和手工实现部分的显示集成
显式集成意味着在开始时,生成的代码时完全独立于手写代码的。开发者负责集成这些工件。这两种代码实际上完全独立--非生成的代码常常依赖于生成的代码,因为在一个系统中它们通常是一起被使用的--这种情况是很少、很罕见的。
最简单的集成方法是在生成的代码中创建保护区域,开发者可以在其中插入手写的代码。这些保护区是用它们能够被生成器读取的方式指定的,所以在随后的生成器运行期间,手写的代码将不会被重写。
UML工具一般用这种方式工作。这里,从模型数据中生成类占位程序,随后开发者将行为集成到类占位程序中。
2.虚拟代码
为了获得一个一致的系统,开发者经常会强制做某些事情。在3层实现的背景下,例如需要开发者实现一个满足下面需求的它们自己类:
- 必须遵循一种特殊的命名传统;
- 必须继承某个类,而且如果必须要的话,重写特定的操作;
- 必须实现一个特定的接口,否则就要提供特定的预定义操作;
因为这里正在手工开发的类,所以不能通过代码生成模版对它们进行简单的增强。然后,可以完成的事生成代码,该代码可以在编译器的帮助下检查需要的特征。
3.技术子域
大型系统一般包含各种各样的方面。在一个单独的模型中描述所有的这些方面是复杂的,也是不实际的。模型面临不同方面的细节负载过重以及太过的危险。此外,在绝大多数情况下某种DSL是适合描述一个特定方面的,不过不适合系统中的其他方面。
DSL标出持久化的模型元素,以便可以生成需要的代码和RDBMS模式。在一个单一的模型中容纳所有的这些信息是很困难的。一个基于UML的语言并不能总是覆盖所有的这些方面。
4、代理元素
在原则上,用网管元类集成不同的子域的效果不错,不过不论模型是不同子域的模型还是例如在各个部分中作为借口的共享元素的模型,它都将导致在学多模型中出现特定的模型元素。为了避免信息的肤质,通常建议使用代理或饮用。
5、外部模型标记
为了能够将一个源模型变换为一个目标模型,有时候必须在生成时提供适合特定目标模型的其他信息。把该信息加入源模型会用目标模型的概念对其造成不必要的影响。OMG建议使用模型标记,不过只有很少的工具充分支持这种概念。
6、方面定位和MDSD
面向方面编程AOP。根据AOP,方面是横切的关注点,它们水平地穿越了代码。代表性的例子通常具有技术的本质,如事务、持久化或记录。方面定位的目标是在一个软件系统族的背景下,在一个单独的模块内局部化这些横切关注点,并使得它更易变化或配置。
用一个生成器可以轻松地实现几个具体的方面:
- 线程同步
- 资源分配
- 安全
7、描述性元对象
在将一个丰富的特定域平台用于MDSD时,为了能够动态控制平台,应用程序在运行时经常需要关于模型元素的信息。
8、生成的反射层
元对象协议是一种对编程语言对象进行检查、修改或“重新解释”的一种方法。着通常是动态发生的。在MDSD背景下,至少能够提供一种只读的元对象协议,这样可以对这些类进行内部检查或者可以动态调用操作。这种工作与基本的编程语言是否支持反射或其他相似的机制无关:也可以用C/C++实现这种方法!一个通用的借口允许客户程序访问各种生成的类。
四、解释程序的使用
绝大部分都是在思想上讨论带有代码生成的MDSD。可以论证的是这是一种最广泛的作MDSD的方法,不过解释程序是一种不同的方法,它共享了相同的基本原理而且符合“将正式的模型自动变换为可执行代码”的定义,尽管是用一种不同的方式。
解释程序和生成器在功能上是等价的。每个模型都可以用作其他任何模型的输入,至少在原理上是可以的。不过更常见的是使用将生成器用于一个系统的结构的方面而将解释程序用于行为。支持这样做的基本原理是结构的方面是良好的、有组织的。
1.解释程序
一个解释程序是一段软件,它在运行时读取一个模型并对它进行评价,完成模型中指定的任何操作。就像一个生成器一样,一个解释程序用精确定义的语义作用于正式模型,而且使用这两种技术可以执行模型中指定的操作。
2.再次访问MDSD术语
1)域
我们把一个域定义为一个受限的兴趣或知识领域,有意使该术语是广泛适用的。很明显这个定义没有做出任何有关将一个解释程序或一个生成器用于某个给定域的假设。
2)元模型
任何一个元模型都可以用作生成器或解释程序的基础。
3)元元模型
解释程序和生成器的分析器是相同的,因此它们的元元模型是相同的--至少在原理上。
4)正式的模型
任何带有具体语法的模型既可以用作生成器的输入,也可以用作解释程序的输入。不过行为丰富的文本模型是一个域,其中解释程序特别能够显示自己的威力,因为它们使得带有从那些基本的编程语言中得到的略微不同的语义的简单基本构件的使用变得轻松。
5)平台
生成器和解释程序都是基于平台代码的--已经存在在目标平台上的代码--而且使用尽可能多的平台代码通常是一个好主意。不过解释程序与平台间的相互作用看起来与生成器与平台间的相互作用不同。
3.解释程序的非功能性属性
那么什么时候应该使用一种生成方法,以及什么时候一个解释程序更加合适呢?
1)性能
2)代码规模
3)绑定时间
4.在一个系统中集成解释程序
为了有多帮助,必须将解释程序集成到系统中的余下部分中。一个解释程序通过3个点接触其余的世界,也就是通过它可以调用解释程序的接口、系统能够使得特定的功能可以被一个模型调用的扩展点以及可以向解释程序提供模型的机制。
1)调用解释程序
解释程序是用平台的编程语言写成的代码片,因此它可以像其他的代码片那样被调用。一个解释程序一般提供一个基本上只带一个方法的简单接口,把对象看作是传递给解释模型的参数--并返回模型的执行结果。
2)扩展点
定义一个处理一个域的所有方面的容易理解的元模型常常是不可行的。那样做经常会引入很大的附加复杂度,毕竟简单行是从事MDSD的原因之一。因此最佳实践是在元模型中定义扩展点,允许一个模型引用那些用基本编程语言所写的代码。
3)提供模型
一个解释程序需要一个在运行时执行的模型,因此应用程序必须提供它。解释程序的模型通常是文本文档,这为它们的存储和供应提供了更多的可能性。
最简单的方法是把它们配置为系统的一部分。
另一个可供选择的办法是把模型作为外部资源存储起来并令系统在运行时重新获得它们。
5.解释程序和测试
就像其他的代码片一样,也需要对解释程序进行测试,而且和任何性质的MDSD一样,测试有两种风格。
1).测试解释程序
解释程序倾向于比生成器更容易测试,因为它们不需要生成代码。实际上,可以以一种直接的方式用最基本的单元测试来对解释程序进行测试,特别是如果它们处理的是文本模型。
最普遍的方法是使用黑盒测试,它将解释程序全部提供给一个模型,然后检查结果。
2).测试模型
可以用标准单元测试对模型进行测试,这种测试方法通过自己的正式接口调用解释程序而且对结果或产生的影响进行检查。
不过解释程序的一个附加的好处是可以不重新启动系统就改变模型,这使得系统立刻显示出修改的行为。这使得将调试部分域需求工程师结合起来成为可能,其中双方交替修改模型直到系统按照要求运转。