《领域特定语言》一第3章 实现DSL 3.1DSL处理之架构

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 本节书摘来自华章出版社《领域特定语言》一书中的第3章,第3.1节,作者 (英)Martin Fowler,更多章节内容可以访问云栖社区“华章计算机”公众号查看

第3章 实现DSL

至此,对于什么是DSL,以及为何要用DSL,我们已经透彻理解。如果要开始构建DSL,那么现在该深入研究所用的技术了。虽然构建内部DSL和外部DSL所用的技术有所不同,但它们还是有一些共通之处的。本章主要关注内部DSL和外部DSL的一些共通问题,而下一章再讨论各自具体的问题。本章先不谈语言工作台,留待后续探讨。

3.1DSL处理之架构

关于DSL实现的大体结构(见图3-1),也就是所谓的DSL系统架构─可能是我们要谈论的最重要的内容之一。

image

迄今为止,你应该已经厌倦了听我说了无数次的“DSL是模型上面薄薄的一层结构”。这里所说的“模型”,称为“语义模型”(第11章)模式。这个模式背后的概念是:所有重要的语义行为都可以在模型中捕获,而DSL的任务就是通过解析来填充模型。所以,根据我的理解,语义模型在DSL中扮演着核心角色─事实上,全书都会首先假设,我们在使用语 义模型。(当然,在本节的最后,在我们有足够的上下文去讨论它们的章节,我会谈谈语义模型的替代方案。)
因为我是一个面向对象偏执狂,所以我理解的语义模型首先是一个对象模型。我喜欢既有数据又有行为的多功能对象模型 (Rich Object Model),但语义模型不必拘泥于此,它也可以仅仅是一种数据结构。虽然我坚持我们应该使用一个合适的对象,但有数据模型来表示语义模型总好过没有。所以,在本书的讨论中,尽管我会将其假定为有合适行为的对象,但事实上,数据结构也是可以用来描述语义模型的选择之一。
很多系统都使用Domain Model [Fowler PoEAA]捕获系统的核心行为,而且通常DSL就是负责组装Domain Model的重要部分,但我依然坚持把Domain Model和语义模型区分开。DSL的语义模型通常是一个系统的Domain Model的子集,因为并不是Domain Model的所有部分都适合用DSL处理。另外,DSL的任务不仅仅是填充Domain Model,它还用于 其他任务。
语义模型完全就是一个普通的对象模型,可以像操作其他所有对象模型一样操作它。在前面关于状态(state)的例子中,用状态模型的命令–查询API组装一个状态机,然后运行它,获取状态对象的行为。从某种意义上说,它与DSL是相互独立的,但在现实中,它们又是焦不离孟,孟不离焦。
(如果有编译器背景知识,你可能会将Domain Model等同于抽象语法树。简而言之,二者不同。我们会在3.2节中再进行分析。)
分离语义模型和DSL有几个好处。首先,我们可以暂时不纠结于DSL的语法和解析器,而专注于当前领域的语义。如果用上DSL,这就说明我们所表达的东西已经非常复杂,复杂到了要拥有自己的模型来表示。
特别是,这样一来,就可以直接创建语义模型中的对象,操作它们进行测试。比如,可以创建一堆状态和迁移(transition),测试事件(event)和命令(command)是否运行良好,而无须处理解析。如果状态机的执行存在问题,问题必然在模型中,无须理解解析如何运作。
对于显式的语义模型而言,可以用多种DSL组装它。比如,从简单的内部DSL开始,稍后再给出一个更加易读的外部DSL版本。考虑到既有的脚本和用户,也许我们希望保留既有的内部DSL,同时支持内部和外部DSL。因为二者都可以解析成相同的语义模型,所以这么做并不困难。它还可以帮助我们避免语言之间的重复。
更为重要的是,拥有一个独立的语义模型,模型和语言就可以独立演化。如果要改变模型,无须修改DSL就可以探索做法,模型能够工作之后,给DSL添加必要的语言构造即可。同样,如果需要尝试不同的DSL语法,只要验证它们是否可以创建相同的模型对象即可。比较它们组装语义模型方式的不同,就可以知道两种语法之间的区别。
从许多方面来看,语义模型和DSL语法的分离实际上反映了领域对象及其表现的分离,这种做法在企业级软件设计中是很常见的。有时,我甚至把DSL视为另一种类型的用户界面。
对比DSL和表现,也有一些局限性。DSL和语义模型始终还是关联的。如果要给DSL增加新构造,就需要保证语义模型中有支持,这也就意味着二者需要同时修改才可以。但是另一方面,二者的分离可以将语义问题和解析问题分开思考,从而简化了很多事情。
内部DSL和外部DSL的不同就在于解析这一步,既包括解析的目标,也包括解析的方式。两种风格的DSL都会产生同样的语义模型,正如前文暗示的一样,没有理由不使用单独的语义模型。事实上,这也正是我在编写状态机的例子中所使用的策略:多种DSL,一个语义模型。
当使用外部DSL时,DSL脚本、解析器和语义模型之间有条清晰的界限。DSL脚本由一种独立的语言编写,解析器读取这些脚本,然后组装语义模型。而使用内部DSL时,它们之间更容易混杂在一起。我提倡使用一个显式的对象层次(“表达式生成器”(第32章)),后者提供必要的连贯接口作为一种语言,然后,运行DSL脚本,调用表达式生成器的方法组装语义模型。所以对内部DSL而言,DSL脚本的处理是由宿主语言解析器和表达式生成器共同完成的。
这让我们想到另一个有趣的问题:在内部DSL中使用“解析”这个词可能令人感觉古怪,我承认,我对此也并不感到十分舒服。然而,我发现,对内部DSL和外部DSL的处理进行类比思考其实是非常有用的。传统的解析方式是,获得文本流,将文本组织为解析树,处理解析树产生有用的输出。而解析内部DSL,输入就是一系列方法调用,将它们组织到一个层次结构中(通常是隐式地在栈中组织),以便后续产生有用的输出。
这里使用“解析”这个词还涉及另一个因素,在很多场景下它并不直接处理文本。在内部DSL中,宿主语言解析器处理文本,而DSL处理器处理的更多是语言构造。但是XML DSL也有同样的情况:XML解析器把文本翻译成XML元素,而DSL处理器基于这个基础进行工作。
是时候重温一下内部DSL和外部DSL的区别了。之前采用的区分标准─是否以应用的基础语言编写─通常而言是对的,但不绝对。一个极端的例子是,应用以Java编写,而DSL以JRuby编写。在这种情况下,我依然会将其归为内部DSL,而我们用到的技术都是来自本书关于内部DSL的章节。
二者之间的真正区别在于,内部DSL使用一种可执行的语言编写,然后,通过在那种语言中执行DSL进行解析。无论 是JRuby还是XML,DSL都是嵌入载体语法中,但不同之处在于,JRuby代码是要执行的,而XML数据结构只是读取而已。当然,大多数时候,内部DSL都是以应用的主要语言所实现的,因此,通常来说,这个定义还是有用的。
一旦有了语义模型,就需要让模型按照期望工作。在状态机的例子中,这个任务就是控制安全系统。有两种方法可以达到这个目的。最简单的,通常也是最好的办法就是: 执行语义模型,因为语义模型本身就是代码,可以根据需要执行它。
另一个方法是代码生成。代码生成是指,生成单独编译和运行的代码。在一些圈子中,代码生成被视为DSL的根本。我看到一些讨论,认为任何和DSL相关的工作都需要通过生成代码来实现,代码生成不可或缺。在个别情况下,我甚至发现一些人在谈论或编写“解析器生成器”(第23章)时,总是不可避免地要谈论代码生成。DSL同代码生成并没有本质关联,大多数情况下,执行语义模型是最好的选择。
代码生成在一种情况下最为有用,就是运行模型同解析DSL不在同一个地方的时候。一个很恰当的例子就是,代码执行在一个语言选择有限的环境中,比如在硬件有限制,或者在关系数据库中。你肯定不希望在一个烤箱或者在SQL中运行解析器,因此用一种更加合适的语言生成解析器和语义模型,然后再用它生成C或者SQL。另一个类似的场景是,解析器依赖于生产环境用不到的程序库。这种情况很普遍,比如,为了DSL使用一个复杂工具,这也就是语言工作台倾向于使用代码生成的原因。
对于这些情况,在解析环境下,有一个“无须生成代码也可以运行”的语义模型还是很有用的。运行语义模型,就可以在不了解代码生成如何运作的情况下,也能够体验DSL的执行。如果不生成代码,就能够测试解析和语义,就可以更快测试以及隔离问题。基于语义模型进行验证,可以在生成代码之前就捕获一些错误。
即便在一个能够很好地解释语义模型的环境里,关于代码生成,也存在一个争论,许多开发人员发现,富语义模型中的某些逻辑非常难以理解。从语义模型生成的代码会让一切变得更加明显,不再如魔法般隐晦。在一个缺乏高水平开发人员的团队中,这点至关重要。
就代码生成而言,务必记住一点,它只是DSL蓝图中的一个可选项。用到时,它极为关键,但大多数情况用不到。在我看来,DSL如同雪地靴,在雪地中远足,当然要有一双保暖防水的靴子,而在炎炎夏日,却完全用不着。
使用语义模型生成代码还有一个好处,它解耦了代码生成器和解析器。纵然对解析过程一无所知,我也能够写出一个代码生成器,并对其进行独立测试。单凭这一点,语义模型就已值回票价了。另外,它也更容易支持生成多种目标代码,如果需要的话。

相关文章
|
1月前
|
JavaScript Java Go
探索Go语言在微服务架构中的优势
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出。本文将深入探讨Go语言在构建微服务时的性能优势,包括其在内存管理、网络编程、并发模型以及工具链支持方面的特点。通过对比其他流行语言,我们将揭示Go语言如何成为微服务架构中的一股清流。
128 53
|
1月前
|
存储 负载均衡 监控
如何利用Go语言的高效性、并发支持、简洁性和跨平台性等优势,通过合理设计架构、实现负载均衡、构建容错机制、建立监控体系、优化数据存储及实施服务治理等步骤,打造稳定可靠的服务架构。
在数字化时代,构建高可靠性服务架构至关重要。本文探讨了如何利用Go语言的高效性、并发支持、简洁性和跨平台性等优势,通过合理设计架构、实现负载均衡、构建容错机制、建立监控体系、优化数据存储及实施服务治理等步骤,打造稳定可靠的服务架构。
34 1
|
1月前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
1月前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
2月前
|
Cloud Native Go API
Go语言在微服务架构中的创新应用与实践
本文深入探讨了Go语言在构建高效、可扩展的微服务架构中的应用。Go语言以其轻量级协程(goroutine)和强大的并发处理能力,成为微服务开发的首选语言之一。通过实际案例分析,本文展示了如何利用Go语言的特性优化微服务的设计与实现,提高系统的响应速度和稳定性。文章还讨论了Go语言在微服务生态中的角色,以及面临的挑战和未来发展趋势。
|
2月前
|
运维 Go 开发者
Go语言在微服务架构中的应用与优势
本文深入探讨了Go语言在构建微服务架构中的独特优势和实际应用。通过分析Go语言的核心特性,如简洁的语法、高效的并发处理能力以及强大的标准库支持,我们揭示了为何Go成为开发高性能微服务的首选语言。文章还详细介绍了Go语言在微服务架构中的几个关键应用场景,包括服务间通信、容器化部署和自动化运维等,旨在为读者提供实用的技术指导和启发。
|
2月前
|
负载均衡 Go API
探索Go语言在微服务架构中的应用与优势
在这篇技术性文章中,我们将深入探讨Go语言(又称为Golang)在构建微服务架构时的独特优势。文章将通过对比分析Go语言与其他主流编程语言,展示Go在并发处理、性能优化、以及开发效率上的优势。同时,我们将通过一个实际的微服务案例,详细说明如何利用Go语言构建高效、可扩展的微服务系统。
|
2月前
|
安全 Go 云计算
探索Go语言在微服务架构中的应用与优势
在本文中,我们将深入探讨Go语言(又称为Golang)在构建微服务架构中的独特优势。文章将分析Go语言的并发模型、简洁的语法以及高效的编译速度,以及这些特性如何使其成为微服务架构的理想选择。我们将通过一个简单的微服务示例,展示Go语言在实际开发中的表现,并讨论其在性能和可维护性方面的优势。
|
2月前
|
前端开发 中间件 Go
实践Golang语言N层应用架构
【10月更文挑战第2天】本文介绍了如何在Go语言中使用Gin框架实现N层体系结构,借鉴了J2EE平台的多层分布式应用程序模型。文章首先概述了N层体系结构的基本概念,接着详细列出了Go语言中对应的构件名称,包括前端框架(如Vue.js、React)、Gin的处理函数和中间件、依赖注入和配置管理、会话管理和ORM库(如gorm或ent)。最后,提供了具体的代码示例,展示了如何实现HTTP请求处理、会话管理和数据库操作。
38 0
|
2月前
|
消息中间件 监控 Go
Go语言在微服务架构中的优势与实践
【10月更文挑战第10天】Go语言在微服务架构中的优势与实践