groovy dsl_在Groovy中构建DSL

上个月的文章中 ,我展示了一些示例,这些示例使用特定于域的语言(DSL)来收集惯用模式 ,这些惯用模式在代码中被定义为常见的设计习惯用法。 (我在“ Composited method and SLAP ”中介绍了惯用模式的概念。)DSL是一种捕获模式的好方法,因为它们是声明性的,比“常规”源代码更具可读性,并且可以使您收获的模式与众不同。周围的代码。

构造DSL的语言技术经常使用巧妙的技巧为您的代码隐式提供换行上下文。 换句话说,DSL尝试使用基础语言的功能来“隐藏”嘈杂的语法,以使您的代码更具可读性。 尽管您可以用Java语言构建DSL,但其用于隐藏上下文的贫乏构造以及僵化和无情的语法使它不适合该技术。 但是其他基于JVM的语言可以填补空白。 在本期和下一部分中,我将展示如何扩展DSL构建面板,以包括从Groovy开始在Java平台上运行的更具表现力的语言(请参阅参考资料 )。

Groovy提供了一些使构建DSL更容易的功能。 支持数量是DSL中的常见要求。 人们总是需要一些东西:7英寸,4英里,13天。 Groovy允许您通过开放类为数字量添加直接支持。 开放类使您可以重新打开现有类,并通过在类中添加,删除或更改方法来对其进行更改,这种机制既强大又危险。 幸运的是,存在实现此功能的安全方法。 Groovy支持的公开课两种不同的语法: 类和ExpandoMetaClass

通过类别公开课

类别的概念是从Smalltalk和Objective-C之类的语言中借用的(请参阅参考资料 )。 类别使用use block指令围绕包含一个或多个打开类的代码调用创建包装器。

通过示例可以最好地理解类别。 清单1显示了一个测试,该测试演示了我添加到String一个新方法camelize() ,该方法camelize()下划线分隔的字符串转换为驼峰式:

清单1.演示camelize()方法的测试
class TestStringCategory extends GroovyTestCase {
    def expected = ["event_map" : "eventMap", 
            "name" : "name", "test_date" : "testDate", 
            "test_string_with_lots_of_breaks" : "testStringWithLotsOfBreaks",
            "String_that_has_init_cap" : "stringThatHasInitCap" ]

    void test_Camelize() {
        use (StringCategory) {
            expected.each { key, value ->
                assertEquals value, key.camelize()
            }
        }
    }
}

清单1中 ,我使用原始案例和经过转换的案例创建了一个expected哈希,然后将StringCategory包装在地图上的迭代周围,期望每个键都变得驼峰化。 请注意,在use块内,您​​无需执行任何特殊操作即可在类上调用新方法。

清单2中显示了StringCategory的代码:

清单2. StringCategory
class StringCategory {

  static String camelize(String self) {
    def newName = self.split("_").collect() { 
      it.substring(0, 1).toUpperCase() +  it.substring(1, it.length())
    }.join()
    newName.substring(0, 1).toLowerCase() +  newName.substring(1, newName.length())      
  }
}

类别是包含静态方法的常规类。 静态方法必须至少具有一个参数,这就是您要扩展的类型。 在清单2中 ,我声明了一个静态方法,该方法接受一个String参数(传统上称为self ,但是您可以随意调用它),该参数表示要向其添加方法的类。 该方法的主体包含Groovy代码,用于将字符串分解为由下划线分隔的块(这是split("_")方法的作用),然后将字符串重新收集在一起,并用大写字母将各部分连接在一起。 最后一行将确保返回的字符串的第一个字符为小写。

使用StringCategory ,必须在use块内访问它。 在use块的括号内具有多个用逗号分隔的类别类是合法的。

这是使用开放类在DSL中表达数量的另一个示例。 考虑清单3中的代码,该代码实现了一个简单的约会日历:

清单3.一个简单的日历DSL
def calendar = new AppointmentCalendar()

use (IntegerWithTimeSupport) {
    calendar.add new Appointment("Dentist").from(4.pm)
    calendar.add new Appointment("Conference call")
                 .from(5.pm)
                 .to(6.pm)
                 .at("555-123-4321")
}
calendar.print()

清单3实现了与“ Fluent接口 ”中的Java示例相同的功能,但是具有增强的语法,包括Java代码中无法实现的一些功能。 例如,请注意,Groovy允许我在某些地方省略括号(例如add()方法的参数周围的括号)。 我还可以拨打5.pm这样的5.pm ,这对Java开发人员来说很奇怪。 这是打开Integer类的示例(Groovy中的所有数字都自动使用类型包装器类,因此,即使5确实是Integer )并添加pm属性。 清单4中显示了实现此开放类的类:

清单4. IntegerWithTimeSupport类定义
class IntegerWithTimeSupport {
    static Calendar getFromToday(Integer self) {
        def target = Calendar.instance
        target.roll(Calendar.DAY_OF_MONTH, self)
        return target
    }

    static Integer getAm(Integer self) {
        self == 12 ? 0 : self
    }

    static Integer getPm(Integer self) {
        self == 12 ? 12 : self + 12
    }
}

该类别类包括三个用于Integer新方法: getFromToday()getAm()getPm() 。 请注意,这些实际上是新属性 ,而不是方法。 我将它们写为新属性的原因与Groovy如何处理方法调用有关。 调用不带参数的Groovy方法时,必须使用空括号将其调用,这使Groovy可以区分方法调用的属性访问。 如果将扩展名写为方法,则我的DSL需要将ampm扩展名称为5.pm() ,这会损害DSL的可读性。 我使用DSL的主要原因之一是为了提高可读性,所以我想摆脱额外的噪音。 您可以在Groovy中通过将扩展创建为属性来做到这一点。 声明属性的语法与Java语言相同(带有一对get / set方法),但是您可以在不带括号的情况下调用它们。

在此DSL中,度量单位是小时,这意味着我需要为3.pm返回15 。 在构建具有数量特征的DSL时,必须确定您的单位,并将其(可选)添加到DSL中以使其更具可读性。 请记住,我正在使用DSL捕获域惯用模式,这意味着非开发人员可以阅读它。

既然您已经了解了如何在日历DSL中实现时间,清单5中所示的Appointment类非常简单:

清单5. Appointment
class Appointment {
  def name;
  def location;
  def date;
  def startTime;
  def endTime;

  Appointment(apptName) {
    name = apptName
    date = Calendar.instance
  }

  def at(loc)  {
    location = loc
    this
  }

  def formatTime(time) {
    time > 12 ? "${time - 12} PM" : "${time} AM"
  }

  def getStartTime() {
    formatTime(startTime)
  }

  def getEndTime() {
    formatTime(endTime)
  }

  def from(start_time) {
    startTime = start_time
    date.set(Calendar.HOUR_OF_DAY, start_time)
    this
  }

  def to(end_time) {
    endTime = end_time
    date.set(Calendar.HOUR_OF_DAY, end_time)
    this
  }

  def display() {
    print "Appointment: ${name}, Starts: ${formatTime(startTime)}"
    if (endTime) print ", Ends: ${formatTime(endTime)}"
    if (location) print ", Location: ${location}"
    println()
  }
}

即使您不了解任何Groovy,阅读Appointment类也可能不会遇到任何麻烦。 请注意,在Groovy中,方法的最后一行是其返回值。 这使得at()from()to()方法的最后一行at() this的返回)成为this的fluent-interface调用。

类别允许您以受控方式对现有类进行更改。 更改严格限制在use()子句定义的词汇块内。 但是,有时您希望开放类的添加方法具有更广泛的范围,而这正是Groovy的ExpandoMetaClass

通过expando公开课

Groovy中的原始开放类语法仅使用类别。 但是,Groovy Web框架的创建者Grails(请参阅参考资料 )发现类别固有的范围界定过于严格。 这导致开发了开放类的替代语法ExpandoMetaClass 。 使用expando时,您可以访问类的元类(Groovy会为您自动创建),并为其添加属性和方法。 清单6中显示了使用expandos的日历示例:

清单6.具有expando开放类的日历
def calendar = new AppointmentCalendar()

calendar.add new Appointment("Dentist")
             .from(4.pm)
calendar.add new Appointment("Conference call")
             .from(5.pm)
             .to(6.pm)
             .at("555-123-4321")
        
calendar.print()

清单6中的代码看起来与清单3中的代码几乎相同,只不过是类别所需的use块。 为了实现对Integer的更改,您可以访问元类,如清单7所示:

清单7. Integer Expando定义
Integer.metaClass.getAm = { ->
  delegate == 12 ? 0 : delegate
}                              

Integer.metaClass.getPm = { ->
  delegate == 12 ? 12 : delegate + 12
}                                

Integer.metaClass.getFromToday = { ->
  def target = Calendar.instance
  target.roll(Calendar.DAY_OF_MONTH, delegate)
  target
}

就像在类别示例中一样,我需要在Integer上将ampm作为属性而不是方法(这样,我在调用它们时就不必使用括号来访问它们),因此我向元类添加了一个新属性,即Integer.metaClass.getAm 。 这些代码块可以接受参数,但是我在这里不需要它们(因此,在代码块的开头是lone- -> )。 在代码块中, delegate关键字指的是您要向其中添加方法的类的实例。 例如,请注意,在getFromToday属性中,我创建了一个新的Calendar实例,然后使用委托值将日历滚动到此Integer实例指定的天数。 当我执行5.fromToday ,我5.fromToday日历提前五天。

在类别和expando之间选择

鉴于类别和expandos具有相同的表现力,应该选择哪一种? 关于类别的最好的事情是词汇块的固有范围限制。 对语言的核心类进行基本(可能会中断)更改是一种常见的DSL反模式。 类别强制限制修改。 另一方面,Expandos本质上是全局的:执行expando代码后,这些更改将在应用程序的其余部分显示。

通常,首选类别。 在对具有潜在副作用的重要类进行更改时,您希望限制这些更改的范围。 类别使您可以缩小范围的范围。 但是,如果发现自己用相同的类别包装了越来越多的代码,则应升级为expandos。 某些更改需要广泛,而将所有这些更改强制放入块中可能会导致代码混乱。 根据经验,如果您在一个类别中包装了三个以上的不同代码块,请考虑将其扩展。

最后一点:测试不是可选的。 许多开发人员似乎认为,对于大量代码来说,测试是可选的,但是对现有类进行更改的任何代码都需要进行全面的测试。 修改核心类的功能很强大,并且可以为问题提供优雅的解决方案。 但是伴随着权力的是责任,这体现为考验。

在野外发生

到目前为止,关于DSL作为捕获惯用模式的一种方式的讨论似乎还有些抽象,因此,我将以真实世界中的示例结束。

easyb中(参见相关主题 )是一个基于Groovy的行为驱动开发测试工具,它允许你创建,结合nondeveloper友好的散文与代码来实现测试场景。 清单8中显示了一个easyb场景示例:

清单8. easyb场景测试队列
package org.easyb.bdd.specification.queue

import org.easyb.bdd.Queue

description "This is how a Queue must work"

before "initialize the queue for each spec", {
    queue = new Queue()
}

it "should dequeue item just enqueued", {
    queue.enqueue(2)
    queue.dequeue().shouldBe(2)
}

it "should throw an exception when null is enqueued", {
    ensureThrows(RuntimeException.class) {
        queue.enqueue(null)
    }
}

it "should dequeue items in same order enqueued", {
    [1..5].each {val ->
        queue.enqueue(val)
    }
    [1..5].each {val ->
        queue.dequeue().shouldBe(val)
    }
}

清单8中的代码定义了队列的正确行为。 每个声明块都以it开头,后跟一个字符串描述和一个代码块。 该方法定义it看起来像这样,在spec预计将描述测试和closure持有的代码块:

def it(spec, closure)

注意,在清单8的最后一个测试中,我正在使用以下代码行验证来自调用dequeue()的值:

queue.dequeue().shouldBe(val)

但是对Queue类的检查表明,它没有shouldBe()方法。 它从哪里来的?

如果查看it()方法的定义,则可以看到类别在何处用于扩展现有类。 清单9显示了it()方法的声明:

清单9.声明it()方法
def it(spec, closure) {
    stepStack.startStep(listener, BehaviorStepType.IT, spec)
    closure.delegate = new EnsuringDelegate()
    try {
        if (beforeIt != null) {
            beforeIt()
        }
        listener.gotResult(new Result(Result.SUCCEEDED))
    use(BehaviorCategory) {
()
        }
        if (afterIt != null) {
            afterIt()
        }
    } catch (Throwable ex) {
        listener.gotResult(new Result(ex))
    }
    stepStack.stopStep(listener)
}

在该方法的大约一半时,作为参数传递的闭包块在BehaviorCategory类中执行,其摘录显示在清单10中:

清单10. BehaviorCategory类的一部分
static void shouldBe(Object self, value, String msg) {
    isEqual(self, value, msg)
}

private static void isEqual(self, value, String msg) {
    if (self.getClass() == NullObject.class) {
        if (value != null) {
            throwValidationException(
                "expected ${value.toString()} but target object is null", msg)
        }
    } else if (value.getClass() == String.class) {
        if (!value.toString().equals(self.toString())) {
            throwValidationException(
                "expected ${value.toString()} but was ${self.toString()}", msg)
        }
    } else {
        if (value != self) {
            throwValidationException("expected ${value} but was ${self}", msg)
        }
    }
}

BehaviorCategory是其方法增强Object的类别,它说明了开放类的强大功能。 通过向Object添加一个新方法,可以为应用程序中的每个实例提供对这些方法的访问权限,从而为每个类(包括Queue )添加shouldBe()方法变得很简单。 您不能使用核心Java代码来执行此操作,即使在某些方面,这样做也很麻烦。 使用类别加强了我以前的建议:将对Object的更改范围限制为easyb DSL中use子句的正文。

结论

我希望我收获的惯用模式在我的其余代码中脱颖而出,而DSL为实现此目标提供了一种引人注目的机制。 与Java语言不同,使用支持编写DSL的语言编写DSL要容易得多。 如果组织内的外部因素使您无法利用非Java语言,请不要放弃。 像Spring框架的工具有替代语言比如Groovy或Clojure的(见越来越多的支持相关主题 )。 您可以使用这些语言来创建组件,然后让Spring将它们注入到应用程序中的适当位置。 许多组织对替代语言过于保守,但是通过像Spring这样的框架存在一条简单的增量途径。

在下一部分中,我将用JRuby中的一些示例来总结使用DSL作为收获域惯用模式的一种方式,其中展示了如何将语言推向表达性。


翻译自: https://www.ibm.com/developerworks/java/library/j-eaed15/index.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值