4.2 内嵌式DSL:元编程模式

元编程就是“编写写程序的程序”。书本上像这样的元编程定义,很容易使人误以为元编程不过是给代码生成换了个花哨的名字。从实践角度来说,元编程,是在语言环境的编译时或运行时设施上,用设施所提供的元对象来撰写程序。看过2.5节的深入讨论,相信你能理解这个定义,我们就不再赘述了。

本节我们将观察金融中介系统领域的几个例子,它们都可以用元编程手段来建模。对于一部分例子,我会首先用一种不具备元编程能力的语言来实现;然后,当我们换一种语言,发挥其元编程能力在更高层次的抽象上编程时,你会看到前后两种实现简洁度的变化。

代码提示 后面的内容中含有大量代码片段,我会插入补充内容说明一些预备知识,解释必要的语言特性,以便读者理解实现中的细微之处。这些补充内容只是简单说明一下稍后代码清单中就要用到的语言特性,欲了解特定语言的更多相关信息,请参阅本书附录中相应语言的速查表。

本节接下来给出了3种模式风格,这些风格落实之后,就成为图4-2列出的那些元编程模式的具体实现。我们将从一个用例场景开始,从用户的角度去观察其中的DSL,并且解析DSL来了解其实现结构。一个用例不见得只应用了一种模式,实际上以下每种模式风格之下,都为了履行解答域的职责,准备了好几种具体的模式实现。

4.2.1 隐式上下文和灵巧API

我们先对前面的章节来个简短回顾。客户在证券交易商那里开户,证券交易商代客户交易他们持有的股票并保证安全。关于客户账户的更多信息,请翻阅3.2.2节的补充内容“金融中介系统:客户账户”。

下面我们准备设计一段交易商开设客户账户的DSL。你可以自行评判元编程技巧对API表现力的改善效果,即使这些元编程技巧作用于用户看不到的内部实现。

1. 评判DSL的表现力

来看下面这段DSL脚本。它的功能是创建新的客户账户,然后向证券中介企业注册该账户。

4.2 内嵌式DSL:元编程模式 - 图1Ruby知识点

  • Ruby怎样定义类和对象。Ruby是一种面向对象(OO)语言,它定义类的方式和其他OO语言差不多。不过,Ruby有其独特的对象模式,允许用户在运行时通过元编程手段修改、检查、扩展对象。

  • Ruby用“块”(block)来实现闭包。闭包指的是一个函数和它的执行环境。Ruby通过它的“块”语法实现闭包。

  • Ruby元编程基础知识。Ruby的对象模型包含许多可以用于反射式和生成式元编程的元件材料,例如类、对象、实例、方法、类方法、单例(singleton)方法等。Ruby元编程机制允许你在运行时探查其对象模型,也允许动态地改变对象行为或生成代码。

代码清单4-1 创建客户账户的DSL

  1. Account.create do 创建账户
  2. number "CL-BXT-23765"
  3. holders "John Doe", "Phil McCay"
  4. address "San Francisco"
  5. type "client"
  6. email "client@example.com"
  7. end.save.and_then do |a| 保存账户
  8. Registry.register(a)
  9. Mailer.new
  10. .to(a.email_address)
  11. .cc(a.email_address)
  12. .subject("创建新账户")
  13. .body("客户账户 #{a.no} 已创建")
  14. .send 开户后发送邮件
  15. end

上面的代码展示了用户使用DSL的情况。留意观察它是怎样隐藏实现细节,同时又将账户创建过程向DSL使用者表达清楚的。这段代码创建账户之余还做了其他一些事情。你能够在不知道内部实现的前提下,仅从代码清单4-1看出是哪些事情吗?如果你全都能看出来,那么这就是一段漂亮的DSL实现。

你很容易识别这段DSL代码的全部举动。它首先创建账户➊并保存(可能是保存到数据库)➋,然后是登记账户并发送邮件给账户所有人➌等动作。

从表现力上看,这段DSL给用户提供了一套符合直观感觉的API,效果很好。领域专家拿到这段DSL脚本肯定也能看出里面的动作序列,因为脚本中很好地运用了领域语汇。接下来,我们开始深入了解内部实现。

2. 定义隐式上下文

Account抽象内实现了创建实例时需要调用的众多方法。代码清单4-2中完全是一般Ruby语言定义类实例方法的常规写法。

代码清单4-2 Account抽象在其实现中运用了领域语汇

  1. class Account
  2. attr_reader :no, :names, :addr, :type, :email_address
  3. def number(number)
  4. @no = number
  5. end
  6. def holders(*names)
  7. @names = names
  8. end
  9. def address(addr)
  10. @addr = addr
  11. end
  12. def type(t)
  13. @type = t
  14. end
  15. def email(e)
  16. @email_address = e
  17. end
  18. def to_s()
  19. "No: " + @no.to_s +
  20. " / Names: (" + @names.join(',').to_s +
  21. ") / Address: " + @addr.to_s
  22. end
  23. end

这些显而易见的方法定义不是我们关心的内容,我们要关注这些常规语句表现出来的微妙方面,发现前面所说的那些实现模式。

拿到一段代码,你首先要确定它的执行上下文。这个上下文可以是刚才定义的一个对象,也可以是你特地配置或者隐式声明的一个执行环境。有的语言要求对于每一处方法调用都明确将方法和对应的上下文联系在一起。请看下面的Java代码:

  1. Account acc = new Account(number);
  2. acc.addHolder(hName1);
  3. acc.addHolder(hName2);
  4. acc.addAddress(addr);
  5. //...

所有针对Account对象的方法调用都必须在前面带上它的调用者,以此显式地传递上下文对象。显式表达上下文会产生繁冗的代码,不利于我们创作简洁易读的DSL。如果一种语言允许隐式声明上下文,肯定不是坏事。隐式上下文有利于语法简明,API紧凑。代码清单4-1中的numberholderstypeemail等方法调用都发生于一个隐式上下文中,也就是正在创建的Account实例之内。

那么怎样建立隐式上下文呢?请看下面从Account类中截取的相关Ruby代码片段,它使用一点巧妙的元编程手法达到了目的:

  1. class Account
  2. attr_reader :no, :names, :addr, :type, :email_address
  3. ## 省略部分同代码清单4-1
  4. def self.create(&block) create接收一个块
  5. account = Account.new
  6. account.instance_eval(&block) Account的上下文内执行传入的块
  7. account
  8. end
  9. end

请看位置➋,instance_eval是一种Ruby元编程语法结构,它会在其调用者的上下文内执行传递给它的Ruby块,因为我们是在Account对象上调用的,所以块就在Account对象的上下文内执行。结果对于那些通过块进行传递的方法➊来说,就好像每次调用的时候都隐式地接受了刚刚构造完毕的account对象。这是一个反射式元编程的例子。Ruby执行环境在运行时通过反射确定执行的上下文。

相同的手法也适用于Groovy,Groovy语言同样具备很强的元编程能力。

我们用Groovy语言改写上述Ruby代码,结果见代码清单4-3。

4.2 内嵌式DSL:元编程模式 - 图2 Groovy知识点

  • 如何在Groovy中创建闭包并为方法分发准备上下文。

代码清单4-3 为方法分发准备隐式上下文的Groovy代码

  1. class Account {
  2. //方法定义
  3. static create(closure) {
  4. def account = new Account()
  5. account.with closure
  6. account
  7. }
  8. }
  9. Account.create {
  10. number 'CL-BXT-23765'
  11. holders 'John Doe', 'Phil McCay'
  12. address 'San Francisco'
  13. type 'client'
  14. email 'client@example.com'
  15. }

前后两种实现不太一样,但得到的API表现力差不多。

3. 利用灵巧API改善表现力

易读是改善DSL表现力的必然结果。连贯接口是提高可读性、实现灵巧API的途径之一。通过方法串联,一个方法的输出很自然地成为另一个方法的输入。这种手法使连串的API调用表达起来更自然,也比较接近问题域内真实的动作序列。同时,因为调用的时候摆脱了一些死板代码,API显得“灵巧”。

如果回顾代码清单4-1中发送邮件之前的连串方法调用➌,你会看到那里的API调用跟你平常使用邮件客户端软件的动作序列是一样的。

你在设计DSL的时候应该注意语句是否流畅。代码清单4-4中的代码片段实现了代码清单4-1中使用的Mailer类。

代码清单4-4 实现了连贯接口的Mailer

  1. class Mailer
  2. attr_reader :mail_to, :mail_cc, :mail_subject, :mail_body
  3. def to(*to_recipients)
  4. @mail_to = to_recipients
  5. self ➊返回自身以利串联
  6. end
  7. def cc(*cc_recipients)
  8. @mail_cc = cc_recipients
  9. self
  10. end
  11. def subject(subj)
  12. @mail_subject = subj
  13. self
  14. end
  15. def body(b)
  16. @mail_body = b
  17. self
  18. end
  19. def send
  20. # 实际的发送操作
  21. puts "发送邮件到 (#{@mail_to.join(",")})"
  22. end
  23. end

Mailer实例被返回给调用者➊充当下一个调用的上下文。send方法是方法链条的最后一环,它结束整个动作序列,把邮件最终发送出去。

代码清单4-1向我们展示了创建账户的DSL,开户的3个步骤在代码中已经比较明显。不过,图4-3把步骤呈现得更清楚,它们运用各种模式塑造了DSL的最终形态。

enter image description here

图4-3 模式的应用步骤:➊在通过instance_eval设立的隐式上下文里创建账户。➋保存账户。➌通过连贯接口配置好Mailer,账户通过一个块来传递

模式的应用分为如下3个步骤:创建一个账户实例,将之保存到数据库,执行其余后续操作。在这里,第三步被设置成一个闭包(或Ruby块)。之前创建的account实例被作为输入传递给这个闭包,用于执行其他的操作。account实例在操作完成后仍然保持不变;注意,第三步操作属于有“副作用”(side-effecting)的操作。

管理程序的副作用是一个微妙的程序设计问题。我们需要将副作用隔离以保持抽象的纯粹性。无副作用的抽象可以为你减少许多烦恼。设计DSL的时候,你应该尽量明确地隔离有副作用的操作。代码清单4-1就把所有涉及副作用的代码解耦出来,放到了一个闭包中。隔离副作用的设计思路不仅适用于内部DSL设计,你设计任何抽象都应该牢记这个原则。

本节我们讨论了基于DSL的设计范式下两种被普遍使用的实现模式。本节的要点见下面的补充内容。

本节要点

通过方法串联手法实现带连贯接口的灵巧API(参见代码清单4-4中的Mailer类)。
隐式上下文可降低DSL的烦琐程度,形成比较紧凑的API,帮助提高表现力(参见Ruby代码片段中的create类方法,或者代码清单4-3中Groovy代码片段里的create静态方法)。

把副作用跟纯粹的抽象隔离开(参见代码清单4-1中用于开户和发送通知邮件的Ruby块)。

接下来,我们继续介绍别的实现结构,它们通过反射式元编程在DSL中实现动态行为。

4.2.2 利用动态装饰器的反射式元编程

在4.2.1节,我们了解到元编程技巧可以用来改善DSL的表现力和简洁度。本节将介绍运行时的另一种元编程手法:通过动态操控类对象,对其他对象进行装饰。

装饰器(Decorator)模式用于在运行时动态地增加对象的功能。(装饰器模式用在抽象之间,可以增加它们的组合能力,附录A对此有所讨论。)本节我们偏重于实现的角度,看看怎样发挥元编程的威力,做出更动态的装饰器。

1. Java中的装饰器

我们还是从Trade抽象入手,这个领域实体是对交易过程中一些最基本相关要素的建模。作为例子,我们打算在Trade对象的基础上设计一些影响其交易净值的配套装饰器。关于如何计算交易的现金价值,请看补充内容“金融中介系统:交易的现金价值”。

4.2 内嵌式DSL:元编程模式 - 图4金融中介系统:交易的现金价值

每一笔交易都有其现金价值,也就是接受证券的交易方需要向交出证券的交易方支付的金额。这个最终的支付金额称为NSV(Net Settlement Value,净结算价值)。NSV主要由两部分构成:证券总值和税费。证券总值取决于所交易证券的单价、种类,还有一些附加成分,如债券的孳息价格。税费额需要计入各种税、手续费、交易征费、佣金以及交易过程中产生的利息等。

证券总值的计算与证券的类型有关(比如有股权和固定收益之分),但大体上是所交易证券的单价和数量的一个函数。

另外的税费部分,根据交易发生的所在国、所在交易所、交易的证券,各有不同规定。例如,在中国香港,印花税被定为0.125%,买入和卖出股权证券要交0.007%的交易征费。

请看代码清单4-5中的Java代码。

代码清单4-5 Trade抽象和它的Java装饰器

  1. public class Trade { Trade抽象
  2. public float value() { 计算并返回交易价值
  3. //...
  4. }
  5. }
  6. public class TaxFeeDecorator extends Trade { 装饰器
  7. private Trade trade;
  8. public TaxFeeDecorator(Trade trade) {
  9. this.trade = trade;
  10. }
  11. @Override
  12. public float value() {
  13. return trade.value() + //...; ➍ 税费计算的具体细节
  14. }
  15. }
  16. public class CommissionDecorator extends Trade { 装饰器
  17. private Trade trade;
  18. public CommissionDecorator(Trade trade) {
  19. this.trade = trade;
  20. }
  21. @Override
  22. public float value() {
  23. return trade.value() + //...; ➎ 佣金计算的具体细节
  24. }
  25. }

代码清单4-5除了实现Trade抽象的契约➊,还有两个配套的装饰器➌,装饰器与Trade搭配使用可影响交易的成交净值➋➍➎。这些装饰器的用法如下:

  1. Trade t =
  2. new CommissionDecorator(
  3. new TaxFeeDecorator(new Trade()));
  4. System.out.println(t.value());

你完全可以在不触动基本的Trade抽象前提下,在其上继续增加其他装饰器。最后计算出来的交易净值等于施加于Trade对象的所有装饰器的合并作用结果。

代码清单4-5就是用Java实现的,是针对计算给定交易的交易净值这一任务而设计的DSL。显然这已经是以Java作为实现语言所能得到的最好结果了。站在程序员的角度,这段DSL很好理解,而且熟悉GoF设计模式(4.7节参考文献1)的程序员会很满意地看到抽象的模式原理被落实到一个具体的领域实现。不过,我们还能做得更好一些吗?

2. 改进Java实现

我们可以凭借Ruby或Groovy的反射式元编程能力提高DSL的表现力和动态性。不过,在查看具体的实现之前,我们先来确定一下哪些地方有改进的潜力。请看表4-1。

表4-1 先前用Java和装饰器模式实现的DSL可能具有的改进点

能否改进如何改进
表现力和领域友好度可以更简洁一些。毕竟对这方面的追求是无止境的
Trade抽象和装饰器被硬性捆绑在一起去除静态继承关系,这可提高装饰器的重用性
易读性。Java实现阅读顺序由外而内,要穿过一长串装饰器才能看到核心的Trade抽象,不符合一般直觉Trade放在前面,装饰器放到后面

Ruby、Groovy等动态类型语言比Java的语法简练不少。Ruby和Groovy都具备鸭子类型(duck typing)特性,通过牺牲对于Java、Scala等语言开箱即用的静态类型安全,可以换取更有利于重用的抽象。(其实Scala也可以实现鸭子类型,详见第6章。)表4-1所列的改进点要求你对Ruby的动态性有更深入的了解,所以我们接下来介绍这方面的一些知识。

3. Ruby中的动态装饰器

代码清单4-6中的Ruby代码是和先前的Java实现差不多的Trade抽象,其中充实了一些较为贴近现实的内容。

4.2 内嵌式DSL:元编程模式 - 图5Ruby知识点

  • Ruby的模块(module)特性有利于实现mixin,mixin可以附加在其他类或模块上。

  • 通过反射来生成运行时代码的相关Ruby元编程基础知识

代码清单4-6 Trade抽象的Ruby实现

  1. class Trade
  2. attr_accessor :ref_no, :account, :instrument, :principal
  3. def initialize(ref, acc, ins, prin)
  4. @ref_no = ref
  5. @account = acc
  6. @instrument = ins
  7. @principal = prin
  8. end
  9. def with(*args)
  10. args.inject(self) { |memo, val| memo.extend val } 动态模块扩展
  11. end
  12. def value
  13. @principal
  14. end
  15. end

与之前的Java实现相比,只有with方法➊这一部分比较值得讨论,其他部分差异不大。我们等下再回头讨论with方法,现在先看看装饰器的部分。我把装饰器设计成Ruby模块的形式,使用的时候可以将其作为mixin混入到实现主体之中(关于mixin,请参阅A.3节):

  1. module TaxFee
  2. def value
  3. super + principal * 0.2
  4. end
  5. end
  6. module Commission
  7. def value
  8. super - principal * 0.1
  9. end
  10. end

显而易见,这段代码中的装饰器并没有静态地耦合到作为抽象主干的Trade类。我们轻而易举地就达成了表4-1中的一项改进目标。

我们刚刚不是提过鸭子类型吗?上术代码片段中的这些模块可以混入到任意实现了value方法的Ruby类中;正好我们的Trade类就有这么一个value方法。那么上面模块定义中的super调用是怎么发挥作用的呢?在Ruby语言里,如果你指定了一个不带任何参数的super调用,Ruby会发送一条消息给当前对象的父对象,请求调用父对象的同名方法。这就是反射式元编程大显身手的时刻了。此处调用的是value方法。我们调用super类方法的时候并不需要静态地绑定任何具体的父类。图4-4展示了指向Trade类和各装饰器的super调用如何在运行时动态串联到一起,以达到我们所期望的效果。

enter image description here

图4-4 展示super调用如何将value()方法串接在一起。调用从Commission.value()开始,Commission是链条中的最后一个模块,调用向下逐级传播,直到Trade类。实线箭头连接起来就是调用的链条;求值计算沿着虚线箭头依次进行,最终求得结果220

在这个例子里,元编程的神奇作用体现在什么地方?它怎样改善DSL的表现力?若回答这两个问题,我们要回头说说代码清单4-6中的with方法。这个方法的作用是把作为参数传递给它的所有装饰器动态地连接到Trade对象,使Trade对象扩展成为新的抽象。图4-5说明了装饰器与主体类动态组合的原理。

enter image description here

图4-5 主体抽象(Trade类)通过with()方法动态地与各种装饰器(TaxFeeCommission)连接起来,形成扩展

我们完全可以通过对Ruby类的静态扩展取得与图中动态扩展相同的效果。但是在运行时进行扩展更有利于各部分抽象提高可重用性,降低耦合程度。代码清单4-6所实现的Trade对象与装饰器结合的例子如下:

  1. tr = Trade.new('r-123', 'a-123', 'i-123', 20000).with TaxFee, Commission
  2. puts tr.value

我们运用元编程技巧在运行时绑定对象,结果得到上面代码片段中的DSL。它的阅读次序由内而外,符合自然的领域语法。语法中的非本质复杂性低于先前的Java版本,对于领域用户而言表现力明显提高。(关于非本质复杂性的讨论,请参阅A.3.2节。)至此,表4-1所列的3项改进全部成功完成。这一路简直太顺利了。

本节要点
装饰器设计模式用于在对象上附加额外的职责。如果能像本节用Ruby模块做到的那样把装饰器动态化,你将可以大大提高DSL的易读性。

不过,我们所面对的并非坦途。任何依赖于动态元编程的模式都有你必须小心应对的陷阱。请特别注意那些可能给日后造成麻烦的问题,参见下面的小小提醒。

enter image description here在动态类型语言中使用运行时元编程将带给你更简洁的语法、更具表现力的领域语言,还有动态操控类结构的能力。而换取这些好处的代价是类型安全和运行速度两方面的牺牲。强调代价并不意味着劝阻你运用元编程技术设计DSL,只是提醒你不要忘记设计抽象的过程始终是一个权衡利弊的过程。在基于DSL的开发活动中,难免有时静态类型安全的重要性高于为DSL用户提供最佳表达手段的需要。本章后面还会讨论到,即使在满足静态类型检查的前提下,像Scala这样的语言仍然有办法让DSL的表现力不输于Ruby语言。所以千万别匆忙地下决定,先比较一下所有可能的方案吧。

本节介绍了如何利用元编程来实现动态的装饰器。其实现方式与Java、C#等静态类型语言差别很大,表现出了更高的灵活性。最重要的是,我们见识了动态装饰器用于实现DSL代码的真实案例。接下来,我们继续探索反射式元编程技术,把另一种常见的Java设计模式活用于DSL设计。

4.2.3 利用buider的反射式元编程

在第2章作为入门练习,我们实现过一个订单处理DSL,还记得吗?当时在Java版本的实现里面,我们为了提高DSL对于用户的表现力运用了Builder设计模式(4.7节参考文献[1])。下面是从2.1.2节照搬过来的一段代码,订单处理DSL的Java实现使用起来是这样子的:

  1. Order o =
  2. new Order.Builder()
  3. .buy(100, "IBM")
  4. .atLimitPrice(300)
  5. .allOrNone()
  6. .valueAs(new OrderValuerImpl())
  7. .build();

代码中运用了连贯接口这种常见手法去构建一个完整的Order对象。只是由于Java的缘故,整个构建过程是静态的;builder提供的所有构建方法全部只能静态地调用。如果借助Groovy语言的动态元编程模式,我们有办法提高builder的“极简”程度,同时又不削弱其表现力(4.7节文献[2];关于抽象设计中的极简特质,请参阅A.2节)。这样用户不用写那么多八股代码,得出来的DSL比较精干而且更易于掌握。在静态方式下需要堆砌大量死板代码来完成的事情,语言运行时利用反射就做到了(所以叫反射式元编程)。

4.2 内嵌式DSL:元编程模式 - 图9Groovy知识点

  • Groovy中如何定义类和对象。

  • Groovy builder允许你使用反射建立对象的层次结构,其语法简练,特别适合运用于DSL。

1. Groovy builder的魔力

代码清单4-7对运用Groovy对Trade对象进行了建模,从中可以看出它的基本组成要素。这里需要再次说明,我们举例所用的Trade抽象因应本章的议题作了大幅度的简化,不可以和真实交易系统中的情况相提并论。

代码清单4-7 Groovy实现的Trade抽象

  1. package domain.trade
  2. class Trade {
  3. String refNo
  4. Account account
  5. Instrument instrument
  6. List<Taxfee> taxfees = []
  7. }
  8. class Account {
  9. String no
  10. String name
  11. String type
  12. }
  13. class Instrument {
  14. String isin
  15. String type
  16. String name
  17. }
  18. class Taxfee {
  19. String taxId
  20. BigDecimal value
  21. }

这是一个平平无奇的Trade抽象,它由一个Account对象、一个Instrument对象和一个Taxfee对象列表组成。我现在要介绍一段builder脚本,它能神奇地探知抽象内部的类结构,用你提供的值正确构造出抽象内的各个对象。

代码清单4-8 作用于Trade对象的动态builder,用Groovy语言编写

  1. def builder =
  2. new ObjectGraphBuilder()
  3. builder.classNameResolver = "domain.trade"
  4. builder.classLoader = getClass().classLoader
  5. def trd = builder.trade( refNo: 'TRD-123') { 建立builder
  6. account(no: 'ACC-123', name: 'Joe Doe', type: 'TRADING')
  7. instrument(isin: 'INS-123', type: 'EQUITY', name: 'IBM Stock') 动态地产生方法
  8. 3.times {
  9. taxfee(taxId: 'Tax ${it}', value: BigDecimal.valueOf(100))
  10. }
  11. }
  12. assert trd != null
  13. assert trd.account.name == 'Joe Doe'
  14. assert trd.instrument.isin == 'INS-123'
  15. assert trd.taxfees.size == 3

对于一直使用Java语言的开发者来说,上面的代码确实有点神奇。DSL用户在脚本中写了一个trd➊方法,它构造了一个创建交易对象的builder。在trd方法里面,用户调用了accountinstrument➋等方法,可是这些方法明明Trade类里面从来没有定义过,就好像语言运行时把它们变出来的一样,代码居然能正确执行。这其实是Groovy元编程变的“小戏法”。

2. 揭开Groovy builder的秘密

除了元编程技术参与了“戏法”,命名参数和闭包也“出了很大力气”,才让代码清单4-8中的DSL脚本表现出那样奇妙的结果。前面各章的例子已经多次展示过闭包在Groovy语言中的用法。现在我们更深入一点,仔细看看语言运行时是怎样探知类名、正确创建实例,然后用builder获得的数据填充实例的属性的。要点参见表4-2。

表4-2 builder与元编程

运行时的作用工作原理
匹配方法名ObjectGraphBuilder上调用任何方法,Groovy都会把方法名通过ClassNameResolver策略进行匹配,匹配到的Class对象就是将要实例化的类
设置ClassNameResolver用户可以自定义ClassNameResolver策略,代之以自己的实现
创建实例Groovy得到Class对象之后,会应用策略NewInstanceResolver,调用目标类的无参数构造器创建该类的一个默认实例
处理类结构和层次关系如果目标类的内部引用了别的类,形成了父子关系(如代码清单4-8中的TradeAccount),builder会更复杂一些。遇到这样的情况,builder会应用RelationNameResolverChildPropertySetter等其他策略去确定属性所属的类,然后实例化它们

如果想进一步了解Groovy builder的工作细节,请参考4.7节参考文献[2]。

你已经看了不少元编程技术,对于怎样用它们设计具有表现力的DSL有了相当程度的了解。现在全面观察一下本节中我们所做过的事情,回顾图4-2中我们目前为止尝试过的所有模式。这些模式就是你今后闯荡DSL世界的工具了。

本节要点

builder可以用于在DSL里面分步构造对象。builder被动态化之后,大幅减少了不得不写的死板代码。Groovy或Ruby语言中的动态builder通过语言运行时的元对象协议动态地构造方法,大大缓解了DSL实现方面的负担。

4.2.4 经验总结:元编程模式

切不可将我们讨论过的众多模式视为一个个互不关联的实体。面对具体领域的时候,你会发现每一种模式在应用之后,都可能形成需要用另一种模式去化解作用力(4.7节参考文献[5])。图4-6简要回顾了本章到目前为止实现过的模式。图4-6将图4-2列举的DSL模式列于左侧,而本章举例讨论过的具体实现方案则列于右侧。

enter image description here

图4-6 目前为止介绍过的内部DSL模式。本章已经在Ruby和Groovy语言中实现了这些模式

我们讨论的几个模式都相当重要,实现内部DSL的时候你会常常用到。这些模式主要针对具备较强元编程能力的动态类型语言。

介绍完反射式元编程,我们即将探讨另一类模式,这类模式将用于在像Scala那样的静态类型语言里面实现内部DSL。当你用类型化的抽象来建模DSL元素时,语言类型系统中的一些规则可以自然地充当起领域中的业务规则。这也是一种保持DSL简练同时又不失其表现力的途径。

本节要点

本节讨论的模式可帮助你降低DSL的烦琐度,提高动态性。我们利用语言的元编程能力在运行时完成必要的工作,以此取代静态方式下那些死板的代码。

重要的不仅是具体的Ruby或Groovy语言实现,你需要全面理解塑造了这些实现的上下文环境。有些强大的实现语言能给你提供丰富的手段去创作动态的DSL。

当你用这里学到的技巧去解决实际的领域建模问题,从而对问题有了更深刻的体会,你将会给这些技巧找到更多的用武之地,也会创造出自己的解决之道。