实战 Groovy: 使用闭包、ExpandoMetaClass 和类别进行元编程
实战 Groovy: 使用闭包、ExpandoMetaClass 和类别进行元编程转载自:http://www.ibm.com/developerworks/cn/java/j-pg06239.html ? 随心所欲添加方法 Scott Davis,创始人,ThirstyHead.com 简介: 进入到 Groovy 风格的元编程世界。在运行时向类动态添加方法的能力 — 甚至 Java? 类以及 ? 人们一直以来都认为 Groovy 是一种面向 JVM 的动态 编程语言。在这期 实战 Groovy 文章中,您将了解元编程 — Groovy 在运行时向类动态添加新方法的能力。它的灵活性远远超出了标准 Java 语言。通过一系列代码示例(都可以通过 下载 获得),将认识到元编程是 Groovy 的最强大、最实用的特性之一。 建模 程序员的工作就是使用软件建模真实的世界。对于真实世界中存在的简单域 — 比如具有鳞片或羽毛的动物通过产卵繁育后代,而具有毛皮的动物则通过产仔繁殖 — 可以很容易地使用软件对行为进行归纳,如清单 1 所示: 清单 1. 使用 Groovy 对动物进行建模 class ScalyOrFeatheryAnimal{ ScalyOrFeatheryAnimal layEgg(){ return new ScalyOrFeatheryAnimal() } } class FurryAnimal{ FurryAnimal giveBirth(){ return new FurryAnimal() } } 关于本系列Groovy 是在 Java 平台上运行的一种现代编程语言。它能够与现有 Java 代码无缝集成,同时引入了各种生动的新特性,比如闭包和元编程。简单来讲,Groovy 是 Java 语言的 21 世纪版本。 将任何新工具整合到开发工具包中的关键是知道何时使用它以及何时将它留在工具包中。Groovy 的功能可以非常强大,但惟一的条件是正确应用于适当的场景。因此, 实战 Groovy 系列将探究 Groovy 的实际应用,以便帮助您了解何时以及如何成功使用它们。 不幸的是,真实的世界总是充满了例外和极端情况 — 鸭嘴兽既有皮毛,又通过产卵繁殖后代。我们精心考虑的每一项软件抽象几乎都存在与之相反的方面。 如果用来建模域的软件语言由于太过死板而无法处理不可避免的例外情况,那么最终的情形就像是受雇于一个小官僚机构的固执的公务员 — “对不起,Platypus 先生,如果要想我们的系统可以跟踪到您的话,您必须会生孩子。” 另一方面,Groovy 之类的动态语言为您提供了灵活性,使您能够更加准确地使用软件建模现实世界,而不是预先作出假设(并且通常是无效的),让现实向您妥协。如果 清单 2. 动态添加 Platypus.metaClass.layEgg = {-> return new FurryAnimal() } def baby = new Platypus().layEgg() 如果觉得这里举的有关动物的例子有些浅显,那么考虑 Java 语言中最常用的一个类: 回页首 Groovy 为 使用 Groovy 的乐趣之一就在于它添加到 清单 3. Groovy 添加到 println "Introduction".padRight(15,".") println "Introduction".reverse() //output Introduction... noitcudortnI 但是添加到 清单 4. 发出 HTTP GET 请求 println "http://thirstyhead.com".toURL().text //output 再举一个例子,运行一个本地 shell 就像发出远程网络调用那么简单。一般情况下我将在命令提示中输入 清单 5. 在 Groovy 中发出一个 shell 命令 println "ifconfig en0".execute().text //output en0: flags=8863 我并没有说 Groovy 的优点在于您不能 使用 Java 语言做同样的事情。您当然可以。Groovy 的优点在于这些方法似乎可以直接添加到 清单 6. 使用 Java 语言发出 shell 命令 Process p = new ProcessBuilder("ifconfig","en0").start(); BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); String line = br.readLine(); while(line != null){ System.out.println(line); line = br.readLine(); } 这看上去有点像在机动车辆管理局的各个窗口之间辗转,不是吗?“对不起,先生,要查看您请求的 是的,您可以构建方便的方法和实用类来帮助将这个问题抽象出来,但是惟一的 那么 Groovy 究竟如何做 — 将新方法添加到无法扩展的类,或直接进行修改?要理解这一点,需要了解 closures 和 回页首 闭包和 Groovy 提供了一种无害的但功能强大的语言特性 — 闭包 — 如果没有它的话,鸭嘴兽将永远无法下蛋。简单来说,闭包就是指定的一段可执行代码。它是一个未包含在类中的方法。清单 7 演示了一个简单闭包: 清单 7. 一个简单闭包 def shout = {src-> return src.toUpperCase() } println shout("Hello World") //output HELLO WORLD 拥有一个独立的方法当然很棒,但是与将方法放入到现有类的能力相比,还是有些逊色。考虑清单 8 中的代码,其中并未创建接受 清单 8. 将 String.metaClass.shout = {-> return delegate.toUpperCase() } println "Hello MetaProgramming".shout() //output HELLO METAPROGRAMMING 未包含任何参数的 由于 Java 语言中不存在这种关系,因此 Groovy 必须引入一个新的概念:委托(delegate)。 首先了解到方法调用包含在 EMC 中,然后了解了 清单 9. 重新定义 String.metaClass.shout = {-> return delegate.toUpperCase() } String.metaClass.toUpperCase = {-> return delegate.toLowerCase() } println "Hello MetaProgramming".shout() //output hello metaprogramming 这个操作看上去仍然有些不严谨(甚至有些危险!)。尽管现实中很少需要修改 清单 10. 重写 println "Before metaprogramming" 3.times{ println Math.random() } Math.metaClass.static.random = {-> return 0.5 } println "After metaprogramming" 3.times{ println Math.random() } //output Before metaprogramming 0.3452 0.9412 0.2932 After metaprogramming 0.5 0.5 0.5 现在,想像一下对发出开销较高的 SOAP 调用的类进行单元测试。无需创建接口和去掉整个模拟对象的存根 — 您可以有选择地重写方法并返回一个简单的模拟响应。(您将在下一小节看到使用 Groovy 实现单元测试和模拟的例子)。 Groovy 元编程是一种运行时行为 — 这个行为从程序启动一直持续到程序运行。但是如果希望对元编程进行更多的显示该怎么做(对于编写单元测试尤其重要)?在下一小节,您将了解揭秘元编程的秘密。 回页首 解密元编程 清单 11 封装了我在 清单 11. 使用单元测试分析元编程 class MetaTest extends GroovyTestCase{ void testExpandoMetaClass(){ String message = "Hello" shouldFail(groovy.lang.MissingMethodException){ message.shout() } String.metaClass.shout = {-> delegate.toUpperCase() } assertEquals "HELLO",message.shout() String.metaClass = null shouldFail{ message.shout() } } } 在命令提示中输入 注意,只需将 但是,如果您不希望 清单 12. 对单个实例进行元编程 void testInstance(){ String message = "Hola" message.metaClass.shout = {-> delegate.toUpperCase() } assertEquals "HOLA",message.shout() shouldFail{ "Adios".shout() } } 如果准备一次性添加或重写多个方法,清单 13 展示了如何以块的方式定义新方法: 清单 13. 一次性对多个方法进行元编程 void testFile(){ File f = new File("nonexistent.file") f.metaClass{ exists{-> true} getAbsolutePath{-> "/opt/some/dir/${delegate.name}"} isFile{-> true} getText{-> "This is the text of my file."} } assertTrue f.exists() assertTrue f.isFile() assertEquals "/opt/some/dir/nonexistent.file",f.absolutePath assertTrue f.text.startsWith("This is") } 注意,我再也不关心文件是否存在于文件系统中。我可以将它发送给这个单元测试中的其他类,并且它表现得像一个真正的文件。当 尽管 回页首 类别和 解释 清单 14. 使用一个 class MetaTest extends GroovyTestCase{ void testCategory(){ String message = "Hello" use(StringHelper){ assertEquals "HELLO",message.shout() assertEquals "GOODBYE","goodbye".shout() } shouldFail{ message.shout() "foo".shout() } } } class StringHelper{ static String shout(String self){ return self.toUpperCase() } } 如果曾经从事过 Objective-C 开发,那么应当对这个技巧感到熟悉。 那么,什么时候应该选择 虽然 EMC 允许您动态定义新行为,然而 清单 15 演示了对同一个 清单 15. 在 class MetaTest extends GroovyTestCase{ void testFileWithCategory(){ File f = new File("iDoNotExist.txt") use(FileHelper,StringHelper){ assertTrue f.exists() assertTrue f.isFile() assertEquals "/opt/some/dir/iDoNotExist.txt",f.absolutePath assertTrue f.text.startsWith("This is") assertTrue f.text.shout().startsWith("THIS IS") } assertFalse f.exists() shouldFail(java.io.FileNotFoundException){ f.text } } } class StringHelper{ static String shout(String self){ return self.toUpperCase() } } class FileHelper{ static boolean exists(File f){ return true } static String getAbsolutePath(File f){ return "/opt/some/dir/${f.name}" } static boolean isFile(File f){ return true } static String getText(File f){ return "This is the text of my file." } } 但是有关类别的最有趣的一点是它们的实现方式。EMC 需要使用闭包,这意味着您只能在 Groovy 中实现它们。由于类别仅仅是包含静态方法的类,因此可以用 Java 代码进行定义。事实上,可以在 Groovy 中重用现有的 Java 类 — 对元编程来说总是含义不明的类。 清单 16 演示了使用来自 Jakarta Commons Lang 包(见 参考资料)的类进行元编程。 清单 16. 使用 Java 类进行元编程 import org.apache.commons.lang.StringUtils class CommonsTest extends GroovyTestCase{ void testStringUtils(){ def word = "Introduction" word.metaClass.whisper = {-> delegate.toLowerCase() } use(StringUtils,StringHelper){ //from org.apache.commons.lang.StringUtils assertEquals "Intro...",word.abbreviate(8) //from the StringHelper Category assertEquals "INTRODUCTION",word.shout() //from the word.metaClass assertEquals "introduction",word.whisper() } } } class StringHelper{ static String shout(String self){ return self.toUpperCase() } } 输入 回页首 元编程和 REST 为了不让您产生元编程只对 单元测试有用的误解,下面给出了最后一个例子。回忆一下 “实战 Groovy:构建和解析 XML” 中可以预报当天天气情况的 RESTful Yahoo! Web 服务。通过将上述文章中的 清单 17. 添加一个 String.metaClass.weather={-> if(!delegate.isInteger()){ return "The weather() method only works with zip codes like '90201'" } def addr = "http://weather.yahooapis.com/forecastrss?p=${delegate}" def rss = new XmlSlurper().parse(addr) def results = rss.channel.item.title results << "/n" + rss.channel.item.condition.@text results << "/nTemp: " + rss.channel.item.condition.@temp } println "80020".weather() //output Conditions for Broomfield,CO at 1:57 pm MDT Mostly Cloudy Temp: 72 可以看到,元编程提供了极好的灵活性。您可以使用本文介绍的任何(或所有)技巧来向任意数量的类添加方法。 回页首 结束语 要求世界因语言的限制而改变显然不切实际。使用软件建模真实世界意味着需要有足够灵活的工具来处理所有极端情况。幸运的是,使用 Groovy 提供的闭包、 在下一期文章中,我将重新审视 Groovy 在单元测试方面的强大功能。使用 Groovy 编写测试会带来实际的效益,不管是 回页首 下载 描述 本文示例的源代码 关于下载方法的信息 参考资料 学习
获得产品和技术
讨论
关于作者 Scott Davis 是国际知名作家、演讲家、软件开发人员。他是 ThirstyHead.com 的创始人,这是一家 Groovy 和 Grails 培训公司。他出版的书籍有 Groovy Recipes: Greasing the Wheels of Java、GIS for Web Developers: Adding Where to Your Application、The Google Maps API 和 JBoss At Work。他为 IBM developerWorks 撰写两部正在刊发的系列文章:精通 Grails 和 实战 Groovy。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |