Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Andrey Adamovich - Groovy 2 Cookbook - 2013.pdf
Скачиваний:
44
Добавлен:
19.03.2016
Размер:
26.28 Mб
Скачать

Chapter 9

How it works...

Every class in the class loader has a reference to an object of type metaClass. This metaClass maintains the list of all methods and properties of a given class, starting with the bytecode information and adding the additional methods that Groovy knows about by default (DefaultGroovyMethods). Normally, all instances of a class share the same metaClass. However, Groovy allows per instance metaclasses, that is, different instances of a class that may refer to different metaclasses.

The first example in Step 1 is nothing more than calling getClass().getName() on a Java class; therefore, piggybacking on the Java reflection capabilities. The remaining examples are more interesting because they leverage the dynamic properties of the Groovy language through the metaClass attribute. The respondsTo method, in particular, is useful when writing a dynamic code; for example, populating an arbitrary object from some data on a file.

See also

ff http://groovy.codehaus.org/api/groovy/lang/MetaClass.html

ff http://groovy.codehaus.org/api/org/codehaus/groovy/runtime/ DefaultGroovyMethods.html

Dynamically extending classes with new methods

One of the exciting characteristics of Groovy is Meta Object Protocol (MOP). In a nutshell, the term metaprogramming refers to writing code that can dynamically change its behavior at runtime. A Meta Object Protocol refers to the capabilities of a dynamic language that enable metaprogramming. In this recipe, we are going to look at one of the capabilities of the MOP, which is ExpandoMetaClass.

Groovy's ExpandoMetaClass lets you assign behavior and state to classes at runtime without editing the original source code; it is essentially a layer above the original class. In the next section of the recipe, we will show you how to achieve such a result.

293

www.it-ebooks.info

Metaprogramming and DSLs in Groovy

How to do it...

Adding a new method to a Groovy (or Java) class is straightforward. We are going to perform the following given steps to add a getInEuros method to the BigDecimal Java class in order to convert US dollars to euros:

1.In a new Groovy script, type the following code: import java.text.NumberFormat

BigDecimal.metaClass.getInEuros = { -> def exchangeRate = 0.763461

def nf = NumberFormat.getCurrencyInstance(Locale.US) nf.setCurrency(Currency.getInstance('EUR')) nf.format(delegate * exchangeRate)

}

2.Now, we are ready to test the conversion rate: assert 1500.00.inEuros == 'EUR1,145.19'

How it works...

Each Groovy object has an accompanying instance of a class named ExpandoMetaClass (reachable through the metaClass property) that holds a reference to the methods that can be called on an object, including:

ff The base methods that the type allows

ff More methods added for that type by Groovy (such as, find on collections) ff Methods added at runtime, using metaClass

Every time a method on an object is invoked, a dynamic dispatcher mechanism is used to delegate to the companion ExpandoMetaClass.

The getInEuros closure, which we have added to the BigDecimal class, contains only the US dollar/euro conversion code. The only aspect worth mentioning is the delegate variable, which refers to the object on which we call the method.

It is important to note that a method added to a class using ExpandoMetaClass will effectively modify the class across all the threads of the application; therefore, it is not a local modification. If you need to modify only a single instance of a class, you can access the metaClass property on an instance, as in the following code snippet:

class Customer { Long id String name

294

www.it-ebooks.info

Chapter 9

String lastName

}

def c = new Customer() c.metaClass.fullName { "$name $lastName" } c.name = 'John'

c.lastName = 'Ross'

assert c.fullName() == 'John Ross'

There's more...

ExpandoMetaClass can also be used to add:

ff Constructors to a class:

// defines a new constructor Customer.metaClass.constructor << {

String name -> new Customer(name: name)

}

def c = new Customer('John') assert 'John' == c.name

In this example, we use the left shift operator to append a new constructor. The << operator can be used for chaining constructors or methods, like in this example:

Customer.metaClass.constructor << { String name -> new Customer(name: name)

}<< { Long id, String fullName -> new Customer(

id: id,

name: fullName.split(',')[0], lastName: fullName.split(',')[1]

)

}

def c0 = new Customer('Mike') c0.name = 'Mike'

def c1 = new Customer(1000, 'Mike,Whitall') assert c1.name == 'Mike'

assert c1.lastName == 'Whitall'

295

www.it-ebooks.info

Metaprogramming and DSLs in Groovy

The Customer class has been enhanced with two concatenated constructors: the first accepting a string and the second accepting a Long and a string. The second constructor uses the split function to assign the name and last name of

the customer.

ff Static methods:

Customer.metaClass.'static'.sayHello = { -> "hello! I'm your customer"

}

assert "hello! I'm your customer" == Customer.sayHello()

ff Properties:

Customer.metaClass.gsm = null def c = new Customer()

c.gsm = '123456'

assert '123456' == c.gsm

See also

ff http://groovy.codehaus.org/ExpandoMetaClass

ff http://groovy.codehaus.org/api/groovy/lang/MetaClass.html

ff http://groovy.codehaus.org/api/groovy/lang/ExpandoMetaClass.html

Overriding methods dynamically

In the previous recipe, Dynamically extending classes with new methods, we learned how to dynamically add a method to a class through one of the metaprogramming features of Groovy,

named ExpandoMetaClass.

In this recipe, we will see how to intercept and replace a call to an existing method of a class. This technique can be very handy when writing unit tests for classes that have dependencies to other classes.

Getting ready

Let's introduce three classes for which we want to write a unit test:

ff Customer.groovy:

package org.groovy.cookbook class Customer {

String name

}

296

www.it-ebooks.info

Chapter 9

ff CustomerDao.groovy:

package org.groovy.cookbook class CustomerDao {

Customer getCustomerById(Long id) { // DO SOME DATABASE RELATED QUERY

...

}

}

ff CustomerService.groovy:

package org.groovy.cookbook class CustomerService {

CustomerDao dao

void setCustomerDao(CustomerDao dao) { this.dao = dao

}

Customer getCustomer(Long id) { dao.getCustomerById(id)

}

}

The fictional CustomerService class has a dependency towards CustomerDao that is somehow injected at runtime (dependency injection is a well-known pattern made famous by the ubiquitous Spring framework). The CustomerDao is a class that would normally access some kind of data store to retrieve the customer data. We are not interested in the details of how the DAO accesses the database. What we know is that unit testing the

CustomerService class would be very difficult without satisfying the hard dependency of the DAO and the database. In other words, the CustomerService class can be only tested if the database required by the DAO is actually running.

In the context of this recipe, the DAO's method getCustomerById does nothing; but in real life, it would contain code to query the database.

How to do it...

A unit test is all we need to put this simple mocking technique into practice:

1.Let's write a unit test for CustomerService, as follows: package org.groovy.cookbook

import static org.junit.Assert.* import org.junit.*

297

www.it-ebooks.info

Metaprogramming and DSLs in Groovy

class TestCustomerService { @Test

void testGetCustomer() { boolean daoCalled = false

CustomerDao.metaClass.getCustomerById = { Long id -> daoCalled = true

new Customer(name:'Yoda')

}

def cs = new CustomerService() def cDao = new CustomerDao() cs.setCustomerDao(cDao)

def customer = cs.getCustomer(100L) assertTrue(daoCalled) assertEquals(customer.name, 'Yoda')

}

}

How it works...

The unit test has the usual plumbing code to instantiate the class under test (CustomerService), and injects the CustomerDao into the service. Before that part,

we override the getCustomerById method by overwriting the method with our own closure. The intercept and replace mechanism is again based on the ExpandoMetaClass class and the metaprogramming features of Groovy. When the mocked method is invoked, the closure is triggered, and the local daoCalled variable gets updated.

A small detail that you should remember is you need to qualify the first argument with the type if the method has a typed parameter; otherwise Groovy won't override the behavior. These two examples would not work:

CustomerDao.metaClass.getCustomerById = { ->

...

}

CustomerDao.metaClass.getCustomerById = { id ->

...

}

298

www.it-ebooks.info

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]