5.5 告诫
一直以来,本章向你展示的都是正面的例子。我们用了三种最流行的动态JVM语言来讨论DSL的实现,不但让你见识了不同语言的各种惯用法和实现技巧,还让你亲手实现了若干证券交易应用领域的实用DSL脚本。然而,不管目前进展得多么顺利,你终究会遇到一些陷阱,而有些潜在的危险我必须给你指出来。
我特意为本章分属三种语言的例子选取了关系密切的三个用例。这么做的目的,是强调即使问题相同,你也应该根据不同语言的特点,选择不同的解决手段。在Ruby实现中适合用动态元编程来解决的问题,使用Clojure时,就不一定能照搬Ruby的套路。你必须学会选择正确的工具去做正确的事。而恰恰在你选择的过程当中,很容易冷不防被常见的陷阱绊倒。我们打算从DSL开发的角度讨论其中的一些陷阱。
5.5.1 遵从最低复杂度原则
实现内部DSL的时候,应该选择宿主语言中最简单、同时最适用于解答域模型的惯用法。我们经常可以看到开发者在不必要的情况下选用元编程手段,例如Ruby的猴子补丁就是一个被滥用的典型。(还记得猴子补丁吗?猴子补丁可以打开一个类,修改其中的方法和属性。由于修改结果会作用于全局,Ruby的猴子补丁特别危险。)很多时候Ruby模块足以代替猴子补丁,与其打开一个类并插入新方法,不如把方法放入一个Module
中,然后有针对性地将Module
包含进有需要的类中。
5.5.2 追求适度的表现力
过度追求DSL的表现力,有可能给实现带来无谓的复杂性。语言的表现力能满足用户要求就够了。下面的Ruby DSL片段来自第5.2.2小节,对于程序员来说,这样的语言已经足够让人理解其领域语义。
TradeDSL.new.new_trade 'T-12435',
'acc-123’, :buy, 100.shares.of('IBM'),
'unitprice' => 200, 'principal' => 120000, 'tax' => 5000
表现力已经够充分了!那么我们为什么要继续开发解释器版DSL呢?有两个理由。首先,我希望表现力提高之后,DSL能得到团队中领域专家的认可。领域专家老鲍是第一位抱怨原版DSL所含非本质复杂性的用户,解释器版更符合他平常在交易台前的用语。其次,我希望充分展示Ruby的动态性的潜力。当你在现实中真正设计DSL时,一定要记住表现力水平应该配合用户的身份。
5.5.3 坚持优秀抽象设计的各项原则
经常会遇到一些现实情况,让你忍不住想要增加DSL的枝节去提高用户的认同感,结果往往就违反了第1章讨论过的优秀抽象的设计原则。语言里的赘词和虚饰多了,封装就容易被破坏,实现的内部细节也更容易暴露。为提高DSL的表现力而削弱抽象的不可变性,并不一定是划算的。这方面的取舍可以看代码清单5-5的例子。我们把Instrument
抽象做成可变的,得以将票据创建逻辑表达为流畅的DSL语句。然后在代码清单5-7中,我们进一步利用抽象的可变性提高DSL的表现力。
这里的告诫并不是让你放弃对表现力的追求。请你记住,语言设计是一种充满了取舍和妥协的工作。任何决策、对抽象设计原则的任何让步,都应该经过审慎评估。对设计原则的调整,也应该以DSL目标用户的身份背景为标杆。
5.5.4 避免语言间的摩擦
按照一般的认识,不同的DSL不能组合使用。一种DSL总是针对一个专门的领域。你在设计交易系统的DSL时,总是以建模领域为参照去调整其表达方式。至于如何与帐务明细DSL、投资组合管理DSL之类的第三方DSL集成,你根本想不了那么多。
虽然你无法预料一切情况,但设计中还是应该尽量提高抽象的组合能力。函数天生比对象容易组合。如果你的实现语言支持Ruby、Groovy、Clojure中的高阶函数,那么应该把设计重点放在组合子的串联上,通过组合子之间的联系,自然凝聚为语言的脉络。可组合的抽象具有诸多优点,有利于并发,具体的讨论请查阅附录A。
如果你的抽象不能组合,DSL就成了一盘散沙。语言成分一个个孤立着零落不成句,领域用户又怎么可能用得自然。
以上就是DSL设计中最容易踩中的几个陷阱,务必加以注意。从语言中挑选哪一部分子集用于DSL实现,是极端重要的设计决策。你要时时记住DSL的集成需求,始终尊重优秀抽象的设计原则。