I am assigned to develop a library that aids to a Grails app. The Grails app has tons of domain objects (about 100+ tables). I don't want my library to depend on the Grails app , which makes my library db-dependent and hard to test (Grails takes a long time to start) .
For example , the Grails app has one Payment
domain object , which contains a lot of fields , and depends on a lot of other domain objects.
I only want some fields , not all fields or other dependent domain objects.
I'm new to groovy , knowing there's Duck Type in groovy . I thought it should be OK to define a Duck Interface, which I don't need to modify the Grails Payment
object.
So , I defined :
interface IPayment {
String getReceiver()
String getContactPhone()
String getContactEmail()
String getUserIp()
...
}
And define a method that accepts this IPayment
interface.
But when I pass the Payment
object to the method, compiler complains Payment not implements IPayment...
Yes, I can force Payment implements IPayment
, but this is not what I want.
I hope the grails app just import my library , passing the Payment
object to my method and works.
Is there any other design techniques ?
Thanks.
updated : groovy 1.8.8 , sorry , no Traits.
The following is how you can create your library independently from your Grails app, and to keep it that way by using an adapter to mesh the app to the library. The adapter tricks the library into thinking that it's using the expected Payment
interface.
Here's an example of the library.
interface Payment {
String getReceiver()
String getContactPhone()
String getContactEmail()
String getUserIp()
}
class PaymentProcessor {
def process(Payment payment) {
payment.with {
println receiver
println contactPhone
println contactEmail
println userIp
}
}
}
There's the Payment
interface and a class which uses it.
The example app has it's own payment class, and it's a little different than the one expected by the library.
class AppPayment {
String receiver
Contact contact
String userIpAddress
}
class Contact {
String phone
String email
}
The receiver property is identical, but the contact info is in a different class and the ip address property is named differently.
To make it possible to use AppPayment
instances with the library, you can create an app-specific adapter.
trait PaymentAdapter implements Payment {
String getContactPhone() { contact.phone }
String getContactEmail() { contact.email }
String getUserIp() { userIpAddress }
}
Usually an adapter is implemented as a class. But using a Groovy trait instead has some advantages. The first of which is that you don't need to implement getReceiver(); the equivalent property already in AppPayment
will be used. You only need to implement whatever is different from the Payment
interface.
There are a number of ways to use the adapter. The most explicit form is coercion.
def processor = new PaymentProcessor()
def payment = new AppPayment(
receiver: 'John',
contact: new Contact(phone: '1234567890', email: '[email protected]') ,
userIpAddress: '192.168.1.101')
processor.process payment as PaymentAdapter
In this case the AppPayment
is coerced into a PaymentAdapter
by applying the trait at runtime. Since PaymentAdapter
implements Payment, PaymentProcessor.process()
accepts it.
You can handle the coercion in a Groovy Category to avoid having to use the as keyword directly.
class PaymentAdapterCategory {
static Object process(PaymentProcessor processor, AppPayment payment) {
processor.process payment as PaymentAdapter
}
}
use(PaymentAdapterCategory) {
processor.process payment
}
With the category you can avoid having to coerce with the adapter explicitly; as long as you call PaymentProcessor.process()
within the Object.use(category, closure)
Closure.
Since the adapter is a trait, and you have access to the source code of the app, you can modify the AppPayment
class to implement the PaymentAdapter
trait. This would allow you to use an AppPayment
instance directly with PaymentProcessor.process()
. DISCLAIMER: This is my favorite option; I just think it's quite... Groovy.
class AppPayment implements PaymentAdapter {
String receiver
Contact contact
String userIpAddress
}
def payment = new AppPayment(...)
processor.process payment
I hope this helps :)
Although this is not an issue in most cases, I want to let you know that the runtime coercion process changes the class of the instance. For example: println ((payment as PaymentAdapter).class.name)
prints out AppPayment10_groovyProxy
This is not a problem unless you do something like this:
def payment = new AppPayment(
receiver: 'John',
contact: new Contact(phone: '1234567890', email: '[email protected]') ,
userIpAddress: '192.168.1.101') as PaymentAdapter
// I'm going to barf!!!
something.iExpectAnInstanceOfAppPayment(payment)
This does not happen with compile-time traits.
Groovy versions prior to 2.3 do not support traits, so the adapter must be a class. You can start by creating a generic adapter in the library.
/*
* Uses duck typing to delegate Payment method
* calls to a delegate
*/
@groovy.transform.TupleConstructor
abstract class AbstractPaymentAdapter implements Payment {
def delegate // Using @Delegate did not work out :(
String getReceiver() { delegate.receiver }
String getContactPhone() { delegate.contactPhone }
String getContactEmail() { delegate.contactEmail }
String getUserIp() { delegate.userIp }
}
The AbstractPaymentAdapter
implements Payment
and expects the delegate to do so as well, but via duck typing. This means subclasses only have to implement whatever differs from the Payment
interface. This makes implementing an adapter as a class nearly as concise as an adapter implemented as a trait.
@groovy.transform.InheritConstructors
class PaymentAdapter extends AbstractPaymentAdapter {
String getContactPhone() { delegate.contact.phone }
String getContactEmail() { delegate.contact.email }
String getUserIp() { delegate.userIpAddress }
}
Using the adapter is simple: processor.process new PaymentAdapter(payment)
You can use a Groovy Category as shown earlier, but not coercion. However, it's possible to fake the coercion and achieve the same syntax by implementing asType()
in the AppPayment
class.
class AppPayment {
String receiver
Contact contact
String userIpAddress
def asType(Class type) {
type == Payment ? new PaymentAdapter(this) : super.asType(type)
}
}
Then you can do this:
processor.process payment as Payment