Groovy 简洁的语法将开发人员从那种需要进行代码编译但却无助于表达
什么 是程序真正想要实现的典型的 Java™ 结构中解放了出来。在
实战 Groovy 系列的这一复兴篇中,Groovy 开发人员兼特约专栏作家 J. Scott Hickey 带您进行一系列对常规 Java 代码和 Groovy 代码的比较,展示这门令人兴奋的语言如何将您解放出来,让您能够专注于编码的重要方面。
通常,程序员们转而选择诸如 Groovy 之类的编程语言,是为了构建快速的实用程序,快速编写测试代码,甚至创建构成大型的 Java 应用程序的组件,而 Groovy 先天具有这样一种能力,它能够减少传统的基于 Java 系统所固有的许多冗余并降低其复杂度。Groovy 简洁而灵活的语法将开发人员从那种需要进行代码编译却无助于表达什么 是程序真正想要实现的典型的 Java 结构中解放出来。不仅如此,Groovy 轻松的类型通过减少一些接口和超类使代码不再复杂,这些接口和超类都是常规 Java 应用程序用以支持不同具体类型间的通用行为所需的。
为了举例说明 Groovy 如何减少 Java 应用程序所涉及的无用数据,我将使用 Bruce Tate 和 Justin Ghetland 的 Spring: A Developer's Notebook(参见 参考资料) 中的样例代码,该书介绍了如何使用 Spring 进行控制反转。每当回顾一个 Java 样例,我都会将其与实现相同功能的相应的 Groovy 源代码进行比较,您将很快发现 Groovy 通过减少 Java 编程的不同方面(冗余且不必要地传递了应用程序的行为)而使应用程序代码变得多么地清晰。
Groovy 之声
在 Bruce 和 Justin 这本书的第一章中,创建了一个简单的自行车商店应用程序,其中包含有四个类。首先,我将向您展示一个简单的名为 Bike
的 JavaBean 类,该类代表了一辆库存的自行车。然后,我会考查自行车商店的类型,名为 RentABike
。它包含了一个 Bike
集。还有一个命名为 CommandLineView
的用于显示自行车列表的类,该类依赖于 RentABike
类型。最后,有一个用于集成这些部分以创建工作应用程序的类,该类利用 Spring 来传递完整地配置了 RentABike
类型的 CommandLineView
类 —— 免去了复杂的硬编码。
停用 JavaBean!
清单 1 中一个代表自行车的类在常规 Java 代码中被实现为一个简单的 JavaBean,它是 Java 开发人员可能已经编写好的成百上千的类的一个典型。通常来说,JavaBean 并没有什么特殊之处 —— 其属性被声明为 private
,且可通过 public
getter 和 setter 对其进行访问。
清单 1. Java 代码中的 Bike JavaBean
import java.math.BigDecimal;
public class Bike { private String manufacturer; private String model; private int frame; private String serialNo; private double weight; private String status; private BigDecimal cost;
public Bike(String manufacturer, String model, int frame, String serialNo, double weight, String status) { this.manufacturer = manufacturer; this.model = model; this.frame = frame; this.serialNo = serialNo; this.weight = weight; this.status = status; }
public String toString() { return "com.springbook.Bike : " + "manufacturer -- " + manufacturer + "/n: model -- " + model + "/n: frame -- " + frame + "/n: serialNo -- " + serialNo + "/n: weight -- " + weight + "/n: status -- " + status + "./n"; }
public String getManufacturer() { return manufacturer; }
public void setManufacturer(String manufacturer) { this.manufacturer = manufacturer; }
public String getModel() { return model; }
public void setModel(String model) { this.model = model; }
public int getFrame() { return frame; }
public void setFrame(int frame) { this.frame = frame; }
public String getSerialNo() { return serialNo; }
public void setSerialNo(String serialNo) { this.serialNo = serialNo; }
public double getWeight() { return weight; }
public void setWeight(double weight) { this.weight = weight; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public BigDecimal getCost() { return cost; }
public void setCost(BigDecimal cost) { this.cost = cost.setScale(3,BigDecimal.ROUND_HALF_UP); }
}
|
清单 1 是一个只有一个构造方法和六个属性的小例子,但其代码却填满了浏览器的整个页面!清单 2 显示了在 Groovy 中定义的相同的 JavaBean:
清单 2. Bike GroovyBean
class Bike {
String manufacturer String model Integer frame String serialNo Double weight String status BigDecimal cost
public void setCost(BigDecimal newCost) { cost = newCost.setScale(3, BigDecimal.ROUND_HALF_UP) }
public String toString() { return """Bike: manufacturer -- ${manufacturer} model -- ${model} frame -- ${frame} serialNo -- ${serialNo} """ } }
|
您认为哪一个没那么多冗余呢?
Groovy 版的代码要少很多很多,这是因为 Groovy 的默认属性语义用 public
访问器和存取器自动定义了 private
域。例如,上述 model
属性现在有了自动定义的 getModel()
方法和 setModel()
方法。可以看到,这项技术的好处是,不必在一种类型中按照属性手工定义两个方法!这也解释了 Groovy 中的一条反复强调的定律:使普通的编码规则变得简单。
|
Groovy 更新了的属性语法 最新版的 Groovy, JSR-06 简化了 Groovy 的属性语法。在此版本之前,属性要在变量声明前跟一个特定的 @Property 符号。但这个符号现在已经不再需要。在 JSR-06 中,只需指定一个类型和一个变量名,就会自动生成一个有着 public getter 和 setter 的 private 属性。也可以使用 def 关键字来代替特定的类型。 |
|
另外,在 Groovy 中,类的默认函数表达得更为简洁,而在常规 Java 代码(如 清单 1)中该函数必须显式编码。当需要用构造函数或 getter 或 setter 来完成一些特殊任务时,Groovy 真的很出色,因为只需瞥一眼代码,其精彩的行为就会立即变得十分明显。例如,在 清单 2 中很容易看出,setCost()
方法会将 cost
属性换算为三个十进制的位。
将这段不太显眼的 Groovy 代码同 清单 1 中的 Java 源代码进行比较。第一次阅读这段代码时,您注意到 setCost()
方法中嵌入了特殊的函数了吗?除非仔细观察,否则太容易看漏了!
Groovy 测试
清单 3 中 Bike
类的测试用例展示了如何使用自动生成的访问器。同时,出于进一步简化通用编程任务的考虑,测试用例也使用了更为简便的 Groovy 点属性名 标记来访问属性。相应地,能够通过 getModel()
方法或更为简洁的 b.model
形式来引用 model
属性。
清单 3. Groovy 中的 Bike 测试用例
class BikeTest extends GroovyTestCase { void testBike() { // Groovy way to initialize a new object def b = new Bike(manufacturer:"Shimano", model:"Roadmaster")
// explicitly call the default accessors assert b.getManufacturer() == "Shimano" assert b.getModel() == "Roadmaster"
// Groovier way to invoke accessors assert b.model == "Roadmaster" assert b.manufacturer == "Shimano" } }
|
也注意到在上述 Groovy 例子中,不必定义一个如 清单 1 中定义的 Java 构造函数那样的能够接受全部六个属性的构造函数。同时,也不必要创建另一个 只含两个参数的构造函数来支持测试用例 —— 在对象创建过程中设置对象属性的 Groovy 的语义不需要这种冗余、烦人的构造函数(它们综合有多个参数但其作用却只是初始化变量)。
|
Groovy 和 IDE 像 Eclipse 那样的 IDE 使得自动创建 getter 和 setter 变得十分简单。这些工具也便利了从具体类中提取接口以利于对其进行重构。但我敢说,读那段代码的次数要比写它的次数多很多。 不幸的是,开发人员仍需要通过已经生成的代码和源文件来费力地分辨程序真正在实现什么。用这个高质量的 Java 工具究竟能不能节省创建 Groovy 源代码的敲键次数尚有争议,但一看即明的是 Groovy 代码更为简洁、更易于解释,且没那么复杂。在实际的有着数百个类和数千行代码的应用程序,这种优势就更为突出。 |
|
降低的复杂性
在前面的部分中,Bike
GroovyBean 利用了 Groovy 的属性和构造语义以减少源代码中的冗余。在这一部分中,Groovy 版的自行车商店也将受益于额外的冗余减少特性,如针对多态的 duck-typing、集合类的改进及操作符重载。
Grooving 中使用多态
在 Java 自行车应用程序中,名为 RentABike
的接口是用来定义由自行车商店支持的 public
方法的。正如在清单 4 中说明的那样,RentABike
定义了一些简单的方法,这些方法用来返回商店中单个的 Bike
或所有 Bike
的列表。
清单 4. 由 Java 定义的 RentABike 接口
import java.util.List;
public interface RentABike { List getBikes(); Bike getBike(String serialNo); void setStoreName(String name); String getStoreName(); }
|
此接口允许多态行为并在下面两种重要情况下提供了灵活性。其一,如果要决定将实现由 ArrayList
转变为数据库形式,余下的应用程序与该变化是隔绝的。其二,使用接口为单元测试提供灵活性。例如,如果要决定为应用程序使用数据库,可以轻易地创建一个该类型的模拟实现,而且它不依赖于实时数据库。
清单 5 是 RentABike
接口的 Java 实现,它使用 ArrayList
来存储多个 Bike
类:
清单 5. RentABike 接口的 Java 实现
import java.util.List; import java.util.ArrayList; import java.util.Iterator;
public class ArrayListRentABike implements RentABike { private String storeName; final List bikes = new ArrayList();
public void setStoreName(String name) { this.storeName = name; }
public String getStoreName() { return storeName; }
public ArrayListRentABike(String storeName) { this.storeName = storeName; bikes.add(new Bike("Shimano", "Roadmaster", 20, "11111", 15, "Fair")); bikes.add(new Bike("Cannondale", "F2000 XTR", 18, "22222",12, "Excellent")); bikes.add(new Bike("Trek","6000", 19, "33333", 12.4,"Fair")); }
public String toString() { return "com.springbook.RentABike: " + storeName; }
public List getBikes() { return bikes; }
public Bike getBike(String serialNo) { Iterator iter = bikes.iterator(); while(iter.hasNext()) { Bike bike = (Bike)iter.next(); if(serialNo.equals(bike.getSerialNo())) return bike;
} return null; } }
|
现在将 清单 4 和 5 中的 Java 代码同 清单 6 中的 Groovy 代码进行比较。Groovy 版的代码很灵巧地避免了对 RentABike
接口的需求。
清单 6. Groovy 的 ArrayListRentABike 实现
public class ArrayListRentABike { String storeName List bikes = [] public ArrayListRentABike(){ // add new instances of Bike using Groovy's initializer syntax bikes << new Bike(manufacturer:"Shimano", model:"Roadmaster", frame: 20, serialNo:"11111", weight:15, status:"Fair") bikes << new Bike(manufacturer:"Cannondale", model:"F2000", frame: 18, serialNo:"22222", weight:12, status:"Excellent") bikes << new Bike(manufacturer:"Trek", model:"6000", frame: 19, serialNo:"33333", weight:12.4, status:"Fair") }
// Groovy returns the last value if no return statement is specified public String toString() { "Store Name:=" + storeName }
// Find a bike by the serial number def getBike(serialNo) { bikes.find{it.serialNo == serialNo} }
}
|
Groovy 像其他动态语言(如 Smalltalk 或 Ruby)一样支持具有 “duck typing” 的多态 —— 在运行时,如果一个对象表现得像个 duck ,它就会被视为 duck ,从而支持无 接口的多态。有了 Groovy ArrayListRentABike
实现,不但减少了成行的代码,而且由于少创建和维护一个模块,复杂性也降低了。那是非常重要的冗余减少!
除了 duck typing,清单 6 中的默认属性语法还简单地定义了两个普通属性,storeName
和 bikes
,如同拥有了 getter 和 setter 一样。这样做的好处和在 清单 1 和 2 中比较 JavaBean-GroovyBean 时所说明的好处是一样的。尤其是,清单 6 还阐明了另一个用以减少代码冗余的 Groovy 特性 —— 操作符重载。请注意如何使用 <<
操作符来代替 add()
方法。通过减少一层嵌套的括号使代码的可读性得以改善。这也是 Groovy 众多通过减少冗余而改善代码可读性的特性中的一种。
|
Groovy 方式下的和谐集合 用诸如 each 和 find 的方法来使用闭包简化了最常见的任务集,如循环和查找。将 清单 5 中 Java 版本的 getBike() 同 清单 6 中 Groovy 版的进行比较。在 Groovy 中,很明显是通过其序列号来寻找 Bike 。而在 Java 版中,定义了一个 Iterator 并计算列表中下一个条目,这很多余,且不利于理解该应用程序真正要实现的功能,即寻找一辆自行车。 |
|
透明的代码
Groovy 中的 duck-typing 和属性语义通过减少代码行数来减少冗余;然而,也可以通过增加透明度来减少冗余。在 清单 6 中,请注意在 ArrayListRentABike
构造函数中创建新 Bike
对象的方式。Groovy 名称和值的初始化语法比 Java 版的略微详细,但这些额外的代码却使整个代码更为透明 —— 将这一点与 清单 5 中 Java 版的进行比较,哪个属性被初始化为哪个值会立即明显 起来。不回过头来看 Bike
JavaBean 源代码,您能记起哪个参数是 frame
,哪个是 new Bike("Shimano"、 "Roadmaster"、20、 "11111"、15、 "Fair")
的 weight
吗?尽管我刚写过,但我还是记不起来!
一个更小的、更加 Groovy 化的自行车商店视图
到目前为止,我将 Bike
和自行车商店类型在 Java 和 Groovy 下进行了比较。现在,到了更近距离地看一下自行车商店的视图 的时候了。在清单 7 中,该视图类具有一个 rentaBike
属性,该属性引用 RentABike
接口并在行动上说明 Java 版的多态。由于 Java 要求所有类属性都必须是声明过的类型,而不是针对某个特定的实现进行编码,我向一个接口编程,该接口使这个类跟 RentABike
实现的改变分隔开来。这是很好的、扎实的 Java 编程实践。
清单 7. Java 版的自行车商店视图
public class CommandLineView { private RentABike rentaBike;
public CommandLineView() {}
public void setRentaBike(RentABike rentaBike) { this.rentaBike = rentaBike; }
public RentABike getRentaBike() { return this.rentaBike; }
public void printAllBikes() { System.out.println(rentaBike.toString()); Iterator iter = rentaBike.getBikes().iterator(); while(iter.hasNext()) { Bike bike = (Bike)iter.next(); System.out.println(bike.toString()); } } }
|
将清单 7 中的 Java 视图与清单 8 中的 Groovy 视图进行比较,请注意我声明了带 def
关键字的 rentaBike
。这是 duck-typing 的实践,与 Java 版的很像。我正在实践好的软件设计,这是因为我还没有将视图和特定的实现耦合起来。但我也能够不 定义接口就实现解耦。
清单 8. Groovy 的 CommandLineView
public class CommandLineView { def rentaBike // no interface or concrete type required, duck typing in action
def printAllBikes() { println rentaBike rentaBike.bikes.each{ println it} // no iterators or casting } }
|
与 Bike
和自行车商店类型一样,Groovy 的 CommandLineView
没有了为 RentABike
属性所显式编写 的 getter 或 setter 的冗余。同样,在 printAllBikes()
方法中,通过使用 each
来打印在集合里找到的每辆自行车,我再一次利用了 Groovy 强大的集合功能的改进。
使用 Spring 进行组装
在前面的部分中,已经介绍了 Groovy 相比 Java 是如何定义自行车、自行车商店和自行车商店视图的。现在该介绍如何将整个应用程序组装起来并在命令行视图中使用 Spring 来显示库存自行车列表了。
工厂的工厂
在 Java 编程中,一旦定义了一个接口,就可以使用工厂模式将创建真实的实现类的责任委派给一个对象工厂。使用 Spring 作为一个工厂极大地减少了冗余,并在 Groovy 和 Java 中都能够使用,在最终的代码样例中,Spring 负责在 Java 和 Groovy 中创建一个 CommandLineView
类型的实例。
在清单 9 中,配置 Spring 是为了在返回一个 CommandLineView
实例前,创建并将自行车商店的 ArrayList
实现注入 CommandLineView
中。这意味着,不需要引用在 清单 7 和 8 的 Java 或是 Groovy 版的命令行视图中的 ArrayList
实现。在 Java 版中,被注入的类通常都会引用一个接口而不是实现。在 Groovy 中,由于使用 def
关键字,而允许利用 duck-typing。无论在哪个实例中,配置 Spring 的目的都是为了将自行车商店视图的实例和自行车商店类型的实例完整地配置起来!
清单 9. Spring 配置文件
"Bruce's Bikes (spring bean)"
|
在清单 10 和 11 中,自行车商店组装类型用清单 9 中的配置文件创建了一个 Spring 的 ClassPathXmlApplicationContext
实例,然后,请求自行车商店视图的实例:
清单 10. Java 版本下调用 Spring 创建自行车商店视图
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class RentABikeAssembler { public static final void main(String[] args) { // Create a Spring application context object ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("RentABike-context.xml");
// retrieve an object from Spring and cast to a specific type CommandLineView clv = (CommandLineView)ctx.getBean("commandLineView");
clv.printAllBikes(); } }
|
请注意Java 版的清单 10 中,用以请求一个命令行视图实例的对 Spring 的调用要求向一个支持 printAllBikes()
方法的对象类型强制转换。在本例中,由 Spring 导出的对象将被强制转换为 CommandLineView
。
有了 Groovy 及其对 duck-typing 的支持,将不再需要强制转换。只需确保由 Spring 返回的类能够对合适的方法调用(printAllBikes()
)作出响应。
清单 11. Groovy 版的 Spring 组合件
import org.springframework.context.support.ClassPathXmlApplicationContext
class RentABikeAssembler { public static final void main(String[] args) { // Create a Spring application context object def ctx = new ClassPathXmlApplicationContext("RentABike-context.xml")
//Ask Spring for an instance of CommandLineView, with a //Bike store implementation set by Spring def clv = ctx.getBean("commandLineView")
//duck typing again clv.printAllBikes() } }
|
正如在清单 11 中看到的那样,在 Groovy 中,duck-typing 对减少冗余的贡献不仅体现在无需声明接口即可支持由 Spring 自动配置对象,其贡献还体现在简化了对完全配置的 bean 的使用(一旦它从 Spring 容器中返回)。
与 Groovy 相协调
至此,希望我已经阐明了 Groovy 的强大功能及其如何能如此深远地改变源代码的性质。与上述 Java 样例相比,Groovy 代码更简短也更易理解。任何人,无论是经验丰富的 Java 架构师还是非 Java 程序员,都能轻易地掌握 Groovy 代码的意图。Groovy 及其对动态类型的支持减少了要管理的文件。总之,使用 Groovy 减少了在典型的 Java 程序中所常见的大量冗余。这实在是福音啊!