前言
本专题主要介绍在Visual Studio 2012中使用Visualization & Modeling SDK进行领域特定语言(DSL)的开发,包括两个部分的内容。在第一部分中,将对领域特定语言进行简单介绍,并讲解如何使用Visual Studio 2012创建一个领域特定语言的开发解决方案,以及Visual Studio 2012集成开发环境对DSL开发的支持;在第二部分中,将以实际应用为例,介绍开发DSL的主要步骤,包括设计、定制、调试、发布以及使用等。本文为本专题的第一部分。
领域特定语言概述
在软件开发过程中,领域特定语言(Domain Specific Language,DSL)可以是一种面向特定应用领域的编程语言,可以是针对某一类问题的表述形式或解决方案,也可以是一种规约(Specification)集合。给DSL下一个明确的定义并不是件容易的事情,比如XML,在大多数情况下,它当然不是DSL,因为能够使用XML进行描述的信息种类数不胜数,但如果我们把XML用于.NET应用程序的配置中,让配置文件以XML的形式进行组织,那么这种具有特定结构的XML就可以看成是一种DSL:这种DSL面向.NET应用程序的配置领域,为这一领域中的问题提供解决方案。在这里,我们暂且不讨论用XML来实现某种DSL是否合适。
对领域驱动设计(Domain Driven Design,DDD)有一定了解的读者,一定对“通用语言(Ubiquitous Language)”这一词语并不陌生。不错,通用语言是DDD的讨论核心,换句话说,所有与DDD相关的理念和实践,都是围绕通用语言而存在和进行的。通用语言解决了软件系统(或者说应用程序)开发过程中的一个重要问题:沟通。目前的现状是:领域专家(业务人员)对软件开发技术和过程并不了解,甚至是一无所知;而软件开发人员又对其所涉及的领域难以理解。所以,DDD认为,应该基于软件系统所面临的领域,设计一套能够同时被领域专家和软件开发人员所理解的语言,以便尽量减少由沟通引起的误解和分歧。这就是通用语言的由来,这里我们还需要区分一下“通用语言”和“普通编程语言(General-Purpose Programming Language)”的差别,虽然两者都有“通用”的含义,但前者是指“在领域专家和软件开发人员之间的通用性”。
由此引出两个问题:首先,通用语言的定义和设计是需要一定成本的,这对于经费本不充裕的项目来说,无疑是难以实践的;其次,通用语言的学习和普及也是需要一定成本的,相对于普通编程语言而言,通用语言的学习成本更大:整个团队需要了解并逐步接受一个新的事物,无论是时间上,还是经费上,都有着一定的开销。结合DDD来看,DDD已经在通用语言的基础上抽象了一些语言元素,比如:实体、值对象、聚合与聚合根、领域服务、领域事件等,然而让一个普通的团队来理解并在自己所面对的领域中准确把握这些元素,也不是一件容易的事情。我想,这也算是侧面解答了“为什么要领域驱动”的疑问:如果你的项目规模不是很大,并希望以最快速度看到开发结果的话,请不要选择DDD。相反,当项目达到一定的规模时,现实复杂度使我们不得不对其进行长远规划,通用语言的定义和学习成本也就变得不那么突出,更重要的是,通用语言的引入和DDD的应用将帮助项目向着良性的方向发展,使项目的各个方面处于一种可控状态。右图来自Martin Fowler《企业应用架构模式》一书,该图对Transaction Script、Table Module(Active Record)和Domain Model三种模式的应用进行了比较,可以看到,对于逻辑相对简单的情况而言,应用Domain Model模式的成本会比较高,然而随着领域逻辑复杂度的增加,相对于其它两种模式而言,使用Domain Model模式的开发和维护成本会更小。虽然DDD并非Domain Model模式,但我们同样可以用这幅图进行类比,以解答“为什么要领域驱动”的问题。
“通用语言”就是面向特定领域的DSL,根据现行的DSL分类方式,“通用语言”基本上都是以“外部DSL”的方式实现,因为嵌入式(或者说“内部”)DSL都需要一种“普通编程语言”作为宿主,但领域专家不一定熟悉普通编程语言。相比之下,UML就不是一种DSL,因为UML并非专注于一个特定的领域。
DSL一般分为两种:嵌入式(内部)DSL(Embedded/Internal DSL)和外部DSL(External DSL)。上面也简单提到过,内部DSL通常需要一种普通编程语言作为宿主,就编译器角度而言,内部DSL和普通编程语言之间没有差别,但对于开发人员来说,内部DSL提供了一种可读性更强的、更能表达特定领域语义的编程方式。较为流行的内部DSL实现方式就是我们熟知的“流畅接口”(Fluent Interface):通过一系列属性和方法的连贯调用,让人觉得好像是在使用一种自然语言来表达和解决特定领域的具体问题。在.NET中,LINQ就是这样一种内部DSL,它以另一种形式集成于宿主编程语言(C#或VB.NET)中,开发人员可以通过这种形式的代码来很顺利地表达查询语义;从编译器的角度看,LINQ形式的代码其实就是流畅接口的调用,如果您对这个问题感兴趣的话,您可以使用C#语言写两个方法,在A方法中使用LINQ方式对一个列表结构进行筛选,比如:var query = from l in list where l == 3 select l;;而在B方法中则使用System.Linq命名空间中的扩展方法,以流畅接口的方式对列表结构进行同样的筛选操作,比如:var query = list.Where(l => l == 3);,当你使用ildasm.exe工具查看这两个方法所产生的IL代码时,你会发现两者完全相同。至于“流畅接口”的实现方式,我已经在《在C#中使用装饰器模式和扩展方法实现Fluent Interface》一文中给出了解决方案。
另一种是外部DSL,外部DSL的实现通常需要借助于新的编译器/解释器,这也就意味着团队需要设计并开发一套能够将外部DSL翻译成普通编程语言,或者能够将外部DSL转换为能在程序环境中运行的组件的编译器/解释器。外部DSL也有两种不同的形式:文本式的和图形化的。文本式的优点在于维护简单,冗余信息比较少(基于XML的外部DSL除外),而图形化的优点则在于直观,但它会连带很多冗余信息,在团队环境中使用图形化DSL会产生一些版本控制上的问题,因为这种图形化的DSL会夹带很多与显示相关的特性,比如图形位置、尺寸、颜色等等,而这些信息本不属于DSL的内容,但在版本控制系统中,又无法忽略对这些信息的更改操作,所以很容易增加解决版本冲突和代码合并的工作量。外部DSL的例子很多,例如:NHibernate的映射文件、web/app.config,虽然这两种是基于XML的;还有Entity Framework的图形化设计器,它是一种面向ORM(Object Relational Mapping,对象关系映射)领域的图形化DSL,它能通过T4将edmx转换成C#源代码,然后通过编译器联编并产生可执行程序。
接下来,我们将讨论如何在Visual Studio 2012中使用Visualization & Modeling SDK(VMSDK)开发DSL。通过VMSDK开发的DSL属于外部DSL。
使用Visual Studio 2012开发DSL的先决条件
要使用Visual Studio 2012开发DSL,就需要安装Visual Studio Visualization & Modeling SDK(VMSDK),而安装VMSDK之前,需要确保Visual Studio SDK安装正确。VMSDK的前身是DSLTools,DSLTools是面向Visual Studio 2005/2008的DSL开发包。以下给出Visual Studio 2012 SDK和Visualization & Modeling SDK的下载地址:
- Visual Studio 2012 SDK:http://www.microsoft.com/en-us/download/details.aspx?id=30668
- Visualization & Modeling SDK for Visual Studio 2012:http://www.microsoft.com/en-us/download/details.aspx?id=30680
在后面的案例中可以了解到,我们所开发的DSL会被打包成一个VSIX的Visual Studio扩展包,于是DSL的发布也变得异常简单:只需要在客户机上安装这个VSIX扩展包即可完成发布。这也是为什么VMSDK需要基于Visual Studio SDK的原因之一。
DSL解决方案的创建
现在,我们使用Visual Studio来创建一个新的DSL解决方案,以便了解DSL的开发过程。在Visual Studio的New Project对话框中,在Other Project Types | Extensibility分类下,可以看到一个名为Domain-Specific Language Designer的工程模板,选择这个模板,并选择解决方案的保存路径,然后为解决方案设置一个名称,点击OK按钮:
现在可以看到Domain-Specific Language Designer Wizard对话框,这个向导对话框能帮助开发人员一步步地完成DSL解决方案的设置,它包含了以下这些内容:
- Solution Settings页面:该页面提供了DSL模板的选择列表,开发人员需要根据自己的实际应用来选择一个合适的模板,例如,如果希望开发一个类似于UML中类图结构的DSL,那么就选择Class Diagrams模板,我们熟知的Entity Framework Model Designer所设计的edmx就属于这种结构的DSL。在这个页面中,还需要为DSL指定一个名称
- File Extension页面:在这个页面中,为DSL指定一个文件扩展名,比如Entity Framework Model DSL的文件扩展名是edmx。当输入文件扩展名后,向导会自动搜索系统注册表以确保所输入的扩展名没有被占用
- Product Settings页面:这里主要是对DSL作为一种产品的一些设置,包括三个方面的内容:DSL所属的产品名称、开发这套产品的公司名称,以及DSL所使用的命名空间。例如,Contoso公司正在开发一套软件产品,产品名为ContosoSoftware,并在这套产品中使用了一种面向某个业务领域的DSL,于是,在开发DSL的时候,就可以将这些信息填写在这个页面中
- Signing页面:Visual Studio会在DSL解决方案中创建一个产生Visual Studio扩展包(VSIX)的工程,通过编译和使用这个扩展包,就能很方便地将DSL部署到Visual Studio开发环境,这部分内容后面还会涉及到。根据Visual Studio扩展包的开发规范,工程需要签名,因此开发人员需要在这个页面中设置一个强名密钥文件。与程序集签名类似,可以创建一个新的强名密钥文件,或者使用一个已有的强名密钥文件
- Summary页面:在此列出了之前各个页面中的设置选项,以便开发人员能够在解决方案创建之前,对这些设置选项进行确认。当确认无误之后,单击Finish按钮以在Visual Studio中创建DSL解决方案
在Solution Explorer中,我们可以看到,Visual Studio为DSL解决方案创建了两个工程:Dsl和DslPackage。Dsl是DSL开发的主要工程,今后我们对DSL的开发和测试都会在这个工程中进行;DslPackage则是一个Visual Studio Extension工程,其主要作用就是产生一个.vsix的Visual Studio扩展包,以方便DSL的部署。DslPackage相关的内容不是本专题的重点,请感兴趣的读者参阅MSDN中有关Visual Studio SDK和扩展包开发的相关专题内容。
Dsl工程中比较重要的内容有:GeneratedCode文件夹、Resources文件夹以及DSL的定义文件:DslDefinition.dsl。GeneratedCode文件夹下包含了很多T4模板,这些T4模板会根据DslDefinition.dsl中DSL的定义产生相应的C#代码;Resources文件夹下则包含了DSL所使用的资源文件,比如用于在Visual Studio工具栏中表示某个工具的图标图片、在设计器中所使用的鼠标光标文件等等;我们开发DSL的大部分工作都是在DslDefinition.dsl文件上进行的,它指定了DSL包含了哪些领域类型(或者说领域概念)、这些领域类型之间的关系是什么,以及领域类型及关系在设计器上的展现形式。其它的两个文件不是我们讨论的重点,在常规的场景中无需深究这两个文件。
接下来,让我们了解一下使用Visual Studio进行DSL开发的集成开发环境。
DSL集成开发环境简介
Visual Studio 2012 DSL集成开发环境主要由以下窗体构成:
- 工具栏(Toolbox):就像Windows Forms设计器工具栏一样,它提供了DSL的设计工具,包括领域类型(Domain Class)、关系(Relationship)等工具,我们会在后面对这些工具进行简要介绍
- DslDefinition.dsl设计器:DSL设计器界面,其中定义了DSL所涉及的所有领域类型及其之间的关系。它同时还定义了这些类型和关系在实际应用中的表现形式,比如某个领域类型的实例是以圆形表示,还是以圆角矩形表示,包括这些图形的颜色、字体、线条粗细等定义
- DSL细节(DSL Details):开发人员可以在这个窗口中对所选的DSL对象的某些属性细节进行设置,比如通过Mapping Details可以设置领域类型与所使用的图形之间的映射关系
- DSL浏览器(DSL Explorer):事实上这里才是真正设计DSL的地方,DslDefinition.dsl设计器无非也就是对DSL浏览器中特定内容的图形化表示。在DSL Explorer中,我们可以定义领域类型(Domain Class)、关系(Relationship)、图形(Shapes)、连接器(Connectors),以及DSL本身所提供的工具(Tools)和验证机制(Validation)。在本专题的第二部分对案例进行介绍的时候,我们会使用到DSL Explorer,但并不一定会涵盖DSL Explorer的各个方面
在日常开发过程中,主要用到的也就是上面所介绍的四个窗体,另外我们还有可能需要根据实际情况来编写一些定制化的C#代码,所以C#程序编辑器和解决方案资源管理器(Solution Explorer)也会被用到。在下一个部分的介绍中,我们会经常涉及到Visual Studio 2012 DSL集成开发环境的这些内容。
DSL工具栏(Toolbox)简介
DSL工具栏中常用的工具主要分为三类:领域类型(Domain Class)类工具、关系(Relationship)类工具以及图形(Shape)类工具,接下来让我们简单地了解一下这些常用的工具。
领域类型类工具
领域类型类工具包括:Domain Class和Named Domain Class。Domain Class就是DSL的重要组成元素,它对DSL中某个特定的概念进行了准确的定义;Named Domain Class其实就是Domain Class,只不过它默认包含了一个字符串类型的领域属性:Name属性。在实际应用中使用所设计的DSL时,当我们创建一个Named Domain Class的实例时,就可以通过Name属性给这个实例设置一个名称。需要注意的是,当某个Domain Class继承于Named Domain Class时,无需再对Domain Class指定Name属性,此时Name属性将会被继承下来。
关系类工具
关系类工具主要是用于指定领域类型之间的关系,这类工具包括:Embedding Relationship、Reference Relationship以及Inheritance。
- Embedding Relationship:用于指定两个领域类型之间的包含关系。在定义Embedding Relationship的时候,可以在关系的两端指定对方类型的重复数(Multiplicity)。最常见的应用就是用于定义DSL模型与领域类型之间的关系:一个DSL模型可以包含某种领域类型,可以用Has一词来表述这种关系。下图中的ExampleModel和ExampleElement就是用这种关系关联起来的,它表述了这样的概念:ExampleModel可以包含0至多个ExampleElement,而ExampleElement则必须属于某个ExampleModel:
- Reference Relationship:用于指定两个领域类型之间的引用关系。比如在设计类图(Class Diagram)的DSL时,我们可以设计面向对象编程语言中的“类”可以引用关联到多个“接口”,当然在面向对象技术中,这种引用关联有个专业术语,称为“实现(Realization)”。试想一下,如果将“接口”方面的重复数设置为1,那么我们所设计的这门面向对象语言就仅支持“单接口实现”,也就是一个“类”只能实现一个“接口”。以下是引用关系的一个样例。在这个样例中,我们可以了解到,在我们所设计的DSL中,ExampleElement可以引用0至多个ExampleElement,而它也能被0至多个其它的ExampleElement所引用
- Inheritance:用于指定两个领域类型之间的继承关系。在DSL的定义中使用继承关系,可以将具有相同行为和属性的领域类型抽象为一个基类型,并在基类型中对这些行为和属性进行定义。比如:某个状态流的DSL定义中包含三种类型的状态:起始状态、中间状态和结束状态,这些都是“状态”概念的具体表述形式,因此,可以设定一个“状态”的抽象领域类型,并使得“起始状态”、“中间状态”和“结束状态”都继承于“状态”抽象领域类型。以下是DSL设计器中对继承关系的表现方式:
图形类工具
在实际应用中,我们可以使用某种图形来表示DSL中的概念。这些图形的形式是多样化的,比如在状态流DSL中,我们可以使用空心圆来表示起始状态,使用圆角矩形来表示中间状态,以及使用实心圆来表示结束状态。Visual Studio 2012 DSL集成开发环境为DSL开发人员提供了丰富的图形类工具,包括:Geometry Shape、Compartment Shape、Image Shape、Connector、Port Shape以及Swimline。
- Geometry Shape:表示一种几何图形,包括矩形、圆角矩形、椭圆形和圆形四种形式。在开发DSL时,可以在设计器上对几何图形的线条粗细、线条颜色、填充色等属性进行设置
- Compartment Shape:这是一种组合型的图形,这种图形由多个部分组成,每个部分被称为一个“间隔”(Compartment)。例如:类图(Class Diagram)中就是使用Compartment Shape对类和接口进行展示的,类或接口的图形会被分为几个部分:首先是类或接口的名称,然后是“字段”部分,再是“属性”部分,之后就是“方法”部分。Compartment Shape对于创建类似于类图这样的DSL是非常适用的,以下就是一个类图的例子,可以帮助我们直观地了解Compartment Shape:
- Image Shape:使用一幅图片来表示某个DSL中的概念。比如可以在所设计的DSL中,使用一个“小人”的图片来表示“角色”的概念
- Connector:这种图形主要用在领域关系上。例如领域类型A通过引用关联与领域类型B建立了关系,那么在DSL的实际应用中,我们就可以使用一根线条来连接A和B的实例,以表示两者之间的引用关系。这根线条就是一个Connector图形,在DSL的开发过程中,我们也能够通过DSL设计器对Connector图形的线型、线条粗细、颜色、箭头等属性进行设置
- Port Shape:用以表示端点的图形。例如有些类图中,会以一个端点的形式来表示其所实现的接口,还有一些组件视图也会用这种图形来表示组件向外界提供的接口等
- Swimline:用以表示泳道图形。泳道通常只有在一些非常特殊的情形下才会被用到,例如在设计分层架构的DSL中,可以把架构中不同的层看成是独立的泳道,在每个泳道中,只能包含一些特定的领域类型,比如“客户端展现机制”只能放在“展现层”泳道中等
其它工具
最后一个需要介绍的工具就是Diagram Element Map,它是一个连接工具,用于将DSL设计器中左边的领域类型/关系跟右边的图表元素(Diagram Element)连接起来,以表示领域类型/关系应该使用哪个图形进行展现。在开发DSL的时候,选择这个工具,然后用鼠标从领域类型/关系拖拽到相应的图表元素上,即可完成两者映射的建立。
使用Visual Studio 2012进行DSL开发调试
在解决方案资源管理器中,Dsl工程上单击鼠标右键,选择Set as StartUp Project选项将其设置为启动工程,然后按F5键即可启动Visual Studio 2012 Experimental Instance,并对DSL进行调试。
在调试环境中,可以在“解决方案资源管理器”中看到两个DSL文件:Sample.mydsl1和Test.mydsl1,双击Sample.mydsl1即可在设计器中看到DSL的范例,而Test.mydsl1则是供开发人员测试用,开发人员可以在Test.mydsl1的设计器中,使用工具栏中的工具添加一些领域类型/关系的实例,以便对自己所开发的DSL进行测试。在下一个部分中,我们还会使用这个Experimental Instance来调试我们的客户化代码,并创建一些T4模板以实现自动化代码生成。
小结
本文是《在Visual Studio 2012中使用VMSDK开发领域特定语言》专题文章的第一部分。本文首先对领域特定语言(DSL)进行了简单的介绍,包括DSL的概念、分类和例子;然后还讲解了Visual Studio 2012中开发DSL的先决条件、环境配置以及开发环境的窗口布局和工具栏等;最后简要地介绍了Visual Studio 2012下DSL的调试功能。在接下来的第二部分中,我们将从一个实际案例入手,介绍DSL开发的基本过程、客户化定制、T4自动化代码产生、DSL的部署,以及在生产环境中使用DSL等内容。