Groovy的Duck接口?

时间:2015-09-17 15:49:51

标签: grails groovy duck-typing

我被指派开发一个帮助Grails应用程序的库。 Grails应用程序有大量的域对象(大约100多个表)。我不希望我的库依赖于Grails应用程序,这使得我的库依赖于db并且很难测试(Grails需要很长时间才能开始)。

例如,Grails应用程序有一个Payment域对象,它包含很多字段,并且依赖于很多其他域对象。

我只想要一些字段,而不是所有字段或其他依赖域对象。

我是groovy的新手,知道在常规中有Duck Type。我认为定义 Duck接口应该没问题,我不需要修改Grails Payment对象。

所以,我定义了:

interface IPayment {
  String getReceiver()
  String getContactPhone()
  String getContactEmail()
  String getUserIp()
  ...
}

定义一个接受此IPayment接口的方法。

但是当我将Payment对象传递给方法时,编译器会抱怨Payment没有实现IPayment ...

是的,我可以强制Payment implements IPayment,但这不是我想要的。

我希望grails应用程序只导入我的库,将Payment对象传递给我的方法并运行。

还有其他设计技巧吗?

感谢。

更新:groovy 1.8.8,抱歉,没有特质。

2 个答案:

答案 0 :(得分:4)

您可以使用as运算符强制将Payment强制转换为IPayment

interface IPayment {
  String getReceiver()
  String getUserIp()
}


class Payment {
    String receiver
    String userIp
}


def account(IPayment payment) {
    "account: ${payment.receiver}, ${payment.userIp}"
}

def payment = new Payment(
        receiver: 'my receiver',
        userIp: '127.0.0.1')


assert account(payment as IPayment) == 'account: my receiver, 127.0.0.1'

纯鸭子打字是动态的,并不关心类型:

def duckAccount(payment) {
    "duck account: ${payment.receiver}, ${payment.userIp}" 
}


assert duckAccount(payment) == "duck account: my receiver, 127.0.0.1"

请注意,as运算符产生的对象没有强制协议,如果强制类型没有实现正确的方法,则可能在运行时失败:

interface IPayment {
  String getReceiver()
  String getUserIp()
  String getPhone()
}


class Payment {
    String receiver
    String userIp
}


def account(IPayment payment) {
    "account: $payment.receiver, $payment.userIp, $payment.phone"
}

def payment = new Payment(
    receiver: 'my receiver', 
    userIp: '127.0.1.1'
)

try {
    assert account(payment as IPayment) == 
        "account: my receiver, 127.0.1.1, null"
    assert false
}
catch (e) {
    assert e.toString().contains(
        "MissingMethodException: No signature of method: Payment.getPhone()")
}

答案 1 :(得分:3)

以下是如何独立于Grails应用程序创建库,并通过使用适配器将应用程序与库进行网格化来保持这种方式。适配器欺骗库以为它正在使用预期的Payment接口。

图书馆

这是图书馆的一个例子。

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
        }
    }
}

Payment接口和使用它的类。

该应用

示例应用程序拥有自己的支付类,它与图书馆预期的支付类略有不同。

class AppPayment {
    String receiver
    Contact contact
    String userIpAddress
}

class Contact {
    String phone
    String email
}

接收者属性是相同的,但联系信息在不同的类中,并且ip地址属性的命名方式不同。

适配器

为了可以在库中使用AppPayment个实例,您可以创建特定于应用程序的适配器。

trait PaymentAdapter implements Payment {
    String getContactPhone() { contact.phone }    
    String getContactEmail() { contact.email }    
    String getUserIp() { userIpAddress }
}

通常将适配器实现为类。但是使用Groovy特性会有一些优势。第一个是你不需要实现getReceiver();将使用AppPayment中已有的等效属性。您只需要实现与Payment接口不同的任何内容。

使用适配器

有许多方法可以使用适配器。最明确的形式是强制。

胁迫

def processor = new PaymentProcessor()
def payment = new AppPayment(
    receiver: 'John',
    contact: new Contact(phone: '1234567890', email: 'john@doe.com') ,
    userIpAddress: '192.168.1.101')

processor.process payment as PaymentAdapter

在这种情况下,AppPayment通过在运行时应用特征而被强制转换为PaymentAdapter。由于PaymentAdapter实施付款,PaymentProcessor.process()接受付款。

Groovy类别

您可以在Groovy类别中处理强制,以避免必须直接使用as关键字。

class PaymentAdapterCategory {
    static Object process(PaymentProcessor processor, AppPayment payment) {
        processor.process payment as PaymentAdapter
    }
}

use(PaymentAdapterCategory) {
    processor.process payment
}

使用该类别,您可以避免必须明确强制使用适配器;只要您在PaymentProcessor.process()关闭中调用Object.use(category, closure)

编译时间特征

由于适配器是特征,并且您可以访问应用程序的源代码,因此可以修改AppPayment类以实现PaymentAdapter特征。这样您就可以直接在AppPayment使用PaymentProcessor.process()实例。免责声明:这是我最喜欢的选择;我只是觉得它很...... Groovy。

class AppPayment implements PaymentAdapter {
    String receiver
    Contact contact
    String userIpAddress
}

def payment = new AppPayment(...)

processor.process payment

我希望这会有所帮助:)

警告

虽然在大多数情况下这不是问题,但我想告诉您运行时强制进程会更改实例的类。例如:println ((payment as PaymentAdapter).class.name)打印出AppPayment10_groovyProxy除非您执行以下操作,否则这不是问题:

def payment = new AppPayment(
    receiver: 'John',
    contact: new Contact(phone: '1234567890', email: 'john@doe.com') ,
    userIpAddress: '192.168.1.101') as PaymentAdapter

// I'm going to barf!!!
something.iExpectAnInstanceOfAppPayment(payment)

编译时特征不会发生这种情况。

没有特征

2.3之前的Groovy版本不支持traits,因此适配器必须是类。您可以从库中创建通用适配器开始。

/*
 * 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 }

}

AbstractPaymentAdapter实现Payment并期望代理也这样做,但是通过duck typing。这意味着子类只需要实现与Payment接口不同的任何东西。这使得将适配器实现为类几乎,就像实现为特征的适配器一样简洁。

@groovy.transform.InheritConstructors
class PaymentAdapter extends AbstractPaymentAdapter {
    String getContactPhone() { delegate.contact.phone }
    String getContactEmail() { delegate.contact.email }
    String getUserIp() { delegate.userIpAddress }    
}

使用适配器

使用适配器很简单:processor.process new PaymentAdapter(payment)

您可以使用前面显示的G​​roovy类别,但不能使用强制。但是,通过在asType()类中实现AppPayment,可以伪造强制并实现相同的语法。

class AppPayment {
    String receiver
    Contact contact
    String userIpAddress

    def asType(Class type) {
        type == Payment ? new PaymentAdapter(this) : super.asType(type)
    }    
}

然后你可以这样做:

processor.process payment as Payment