Search code examples
grailsgroovyduck-typing

Groovy's Duck Interface?


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.


Solution

  • 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.

    The library

    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 app

    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.

    The adapter

    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.

    Using the adapter

    There are a number of ways to use the adapter. The most explicit form is coercion.

    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.

    Groovy Category

    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.

    Compile-time trait

    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 :)

    Warning

    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.

    Without 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

    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