领域语言
语言的界限就是一个人的世界的界限。
——维特根斯坦
计算机语言会影响你思考问题的方式,以及你看待交流的方式。每种语言都含有一系列特性——比如静态类型与动态类型、早期绑定与迟后绑定、继承模型(单、多或无)这样的时髦话语——所有这些特性都在提示或遮蔽特定的解决方案。头脑里想着Lisp设计的解决方案将会产生与基于C风格的思考方式而设计的解决方案不同的结果,反之亦然。与此相反——我们认为这更重要——问题领域的语言也可能会提示出编程方案。
我们总是设法使用应用领域的语汇来编写代码(参见210页的需求之坑,我们在那里提出要使用项目词汇表)。在某些情况下,我们可以更进一层,采用领域的语汇、语法、语义——语言——实际进行编程。
当你听取某个提议中的系统的用户说明情况时,他们也许能确切地告诉你,系统应怎样工作:
在一组X.25线路上侦听由ABC规程12.3定义的交易,把它们转译成XYZ公司的43B格式,在卫星上行链路上重新传输,并存储起来,供将来分析使用。
如果用户有一些这样的做了良好限定的陈述,你可以发明一种为应用领域进行了适当剪裁的小型语言,确切地表达他们的需要:
From X25LINE1 (Format=ABC123) {
Put TELSTAR1 (Format=XYZ43B);
Store DB;
}
该语言无须是可执行的。一开始,它可以只是用于捕捉用户需求的一种方式——一种规范。但是,你可能想要更进一步,实际实现该语言。你的规范变成了可执行代码。
在你编写完应用之后,用户给了你一项新需求:不应存储余额为负的交易,而应以原来的格式在X.25线路上发送回去:
From X25LINE1 (Format=ABC123) {
if (ABC123.balance < 0) {
Put X25LINE1 (Format=ABC123);
}
else {
Put TELSTAR1 (Format=XYZ43B);
Store DB;
}
}
很容易,不是吗?有了适当的支持,你可以用大大接近应用领域的方式进行编程。我们并不是在建议让你的最终用户用这些语言实际编程。相反,你给了自己一个工具,能够让你更靠近他们的领域工作。
|
提示17 |
|
|
Program Close to the Problem domain |
|
无论是用于配置和控制应用程序的简单语言,还是用于指定规则或过程的更为复杂的语言,我们认为,你都应该考虑让你的项目更靠近问题领域。通过在更高的抽象层面上编码,你获得了专心解决领域问题的自由,并且可以忽略琐碎的实现细节。
记住,应用有许多用户。有最终用户,他们了解商业规则和所需输出;也有次级用户:操作人员、配置与测试管理人员、支持与维护程序员,还有将来的开发者。他们都有各自的问题领域,而你可以为他们所有人生成小型环境和语言。
|
具体领域的错误 如果你是在问题领域中编写程序,你也可以通过用户可以理解的术语进行具体领域的验证,或是报告问题。以上一页我们的交换应用为例,假定用户拼错了格式名: From X25LINE1 (Format=AB123) 如果这发生在某种标准的、通用的编程语言中,你可能会收到一条标准的、通用的错误消息: Syntax error: undeclared identifier 但使用小型语言,你却能够使用该领域的语汇发出错误消息: "AB123" is not a format. known formats are ABC123, XYZ43B, PDQB, and 42. |
实现小型语言
在最简单的情况下,小型语言可以采用面向行的、易于解析的格式。在实践中,与其他任何格式相比,我们很可能会更多地使用这样的格式。只要使用switch语句、或是使用像Perl这样的脚本语言中的正则表达式,就能够对其进行解析。281页上练习5的解答给出了一种用C编写的简单实现。
你还可以用更为正式的语法,实现更为复杂的语言。这里的诀窍是首先使用像BNF这样的表示法定义语法。一旦规定了文法,要将其转换为解析器生成器(parser generator)的输入语法通常就非常简单了。C和C++程序员多年来一直在使用yacc(或其可自由获取的实现,bison[URL 27])。在Lex and Yacc[LMB92]一书中详细地讲述了这些程序。Java程序员可以选用javaCC,可在[URL 26]处获取该程序。282页上练习7的解答给出了一个用bison编写的解析器。如其所示,一旦你了解了语法,编写简单的小型语言实在没有多少工作要做。
要实现小型语言还有另一种途径:扩展已有的语言。例如,你可以把应用级功能与Python[URL 9]集成在一起,编写像这样的代码:
record = X25LINE1.get(format=ABC123)
if (record.balance < 0):
X25LINE1.put(record, format=ABC123)
else:
TELSTAR1.put(record, format=XYZ43B)
DB.store(record)
数据语言与命令语言
可以通过两种不同的方式使用你实现的语言。
数据语言产生某种形式的数据结构给应用使用。这些语言常用于表示配置信息。
例如,sendmail程序在世界各地被用于在Internet上转发电子邮件。它具有许多杰出的特性和优点,由一个上千行的配置文件控制,用sendmail自己的配置语言编写:
Mlocal, P=/usr/bin/procmail,
F=lsDFMAw5 :/|@qSPfhn9,
S=10/30, R=20/40,
T=DNS/RFC822/X-Unix,
A=procmail -Y -a $h -d $u
显然,可读性不是sendmail的强项。
多年以来,Microsoft一直在使用一种可以描述菜单、widget(窗口小部件)、对话框及其他Windows资源的数据语言。下一页上的图2.2摘录了一段典型的资源文件。这比sendmail的配置文件要易读得多,但其使用方式却完全一样——我们编译它,以生成数据结构。
命令语言更进了一步。在这种情况下,语言被实际执行,所以可以包含语句、控制结构、以及类似的东西(比如58页上的脚本)。
|
图2.2 Windows .rc文件 |
你也可以使用自己的命令语言来使程序易于维护。例如,也许用户要求你把来自某个遗留应用的信息集成进你的新GUI开发中。要完成这一任务,常用的方法是“刮屏”(screen scraping):你的应用连接到主机应用,就好像它是正常的使用人员;发出键击,并“阅读”取回的响应。你可以使用一种小型语言来把这样的交互编写成脚本:
locate prompt "SSN:"
type "%s" social_security_number
type enter
waitfor keyboardunlock
if text_at(10,14) is "INVALID SSN" return bad_ssn
if text_at(10,14) is "DUPLICATE SSN" return dup_ssn
# etc...
当应用确定是时候输入社会保障号时,它调用解释器执行这个脚本,后者随即对事务进行控制。如果解释器是嵌入在应用中的,两者甚至可以直接共享数据(例如,通过回调机制)。
这里你是在维护程序员(maintenace programmer)的领域中编程。当主机应用发生变化、字段移往别处时,程序员只需更新你的高级描述,而不用钻入C代码的各种细节中。
独立语言与嵌入式语言
要发挥作用,小型语言无须由应用直接使用。许多时候,我们可以使用规范语言创建各种由程序自身编译、读入或用于其他用途的制品(包括元数据。参见元程序设计,144页)。
例如,在100页我们将描述一个系统,在其中我们使用Perl、根据原始的schema规范生成大量衍生物。我们发明了一种用于表示数据库schema的通用语言,然后生成我们所需的所有形式——SQL、C、网页、XML,等等。应用不直接使用规范,但它依赖于根据规范产生的输出。
把高级命令语言直接嵌入你的应用是一种常见做法,这样,它们就会在你的代码运行时执行。这显然是一种强大的能力;通过改变应用读取的脚本,你可以改变应用的行为,却完全不用编译。这可以显著地简化动态的应用领域中的维护工作。
易于开发还是易于维护
我们已经看到若干不同的文法,范围从简单的面向行的格式到更为复杂的、看起来像真正的语言的文法。既然实现更为复杂的文法需要额外的努力,你又为何要这样做呢?
权衡要素是可扩展性与维护。尽管解析“真正的”语言所需的代码可能更难编写,但它却容易被人理解得多,并且将来用新特性和新功能进行扩展也要容易得多。太简单的语言也许容易解析,但却可能晦涩难懂——很像是60页上的sendmail例子。
考虑到大多数应用都会超过预期的使用期限,你可能最好咬紧牙关,先就采用更复杂、可读性更好的语言。最初的努力将在降低支持与维护费用方面得到许多倍的回报。
相关内容:
l 元程序设计,144页
挑战
l 你目前的项目的某些需求是否能以具体领域的语言表示?是否有可能编写编译器或转译器,生成大多数所需代码?
l 如果你决定采用小型语言作为更接近问题领域的编程方式,你就是接受了,实现它们需要一些努力。你能否找到一些途径,通过它们把你为某个项目开发的框架复用于其他项目?
练习
5. 我们想实现一种小型语言,用于控制一种简单的绘图包(或许是一种“海龟图形”(turtle-graphics)系统)。这种语言由单字母命令组成。有些命令后跟单个数字。例如,下面的输入将会绘制出一个矩形:
P 2 # select pen 2
D # pen down
W 2 # draw west 2cm
N 1 # then north 1
E 2 # then east 2
S 1 # then back south
U # pen up
请实现解析这种语言的代码。它应该被设计成能简单地增加新命令。(解答在281页)
6. 设计一种解析时间规范的BNF文法。应能接受下面的所有例子:(解答在282页)
4pm, 7:38pm, 23:42, 3:16, 3:16am
7. 用yacc、bison或类似的解析器生成器为练习6中的BNF文法实现解析器。(解答在282页)
8. 用Perl实现时间解析器(提示:正则表达式可带来好的解析器)。(解答在283页)








