Groovy 的 Duck 界面?

Groovy's Duck Interface?

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

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

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

我是 groovy 的新手,知道 groovy 中有 Duck Type。我认为定义一个 Duck Interface 应该没问题,我不需要修改 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,抱歉,没有 Traits。

您可以使用 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()")
}

以下是如何独立于 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。

应用程序

示例应用程序有自己的付款方式 class,与图书馆预期的付款方式略有不同。

class AppPayment {
    String receiver
    Contact contact
    String userIpAddress
}

class Contact {
    String phone
    String email
}

接收者 属性 相同,但联系信息在不同的 class 中,IP 地址 属性 的命名也不同。

适配器

为了能够将 AppPayment 个实例与库一起使用,您可以创建一个特定于应用程序的适配器。

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

通常适配器实现为class。但是使用 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
}

有了类别,您就可以避免显式强制适配器;只要您在 Object.use(category, closure) 闭包中调用 PaymentProcessor.process()

编译时特征

由于adapter是一个trait,你可以访问app的源代码,你可以修改AppPayment class来实现PaymentAdapter trait。这将允许您直接将 AppPayment 实例与 PaymentProcessor.process() 一起使用。免责声明:这是我最喜欢的选择;我只是觉得它很... Groovy.

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

def payment = new AppPayment(...)

processor.process payment

希望对您有所帮助:)

警告

虽然在大多数情况下这不是问题,但我想让您知道运行时强制过程会更改实例的 class。例如: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)

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

没有特质

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

/*
 * 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 并期望代理也这样做,但是通过鸭子类型。这意味着 subclasses 只需要实现与 Payment 接口不同的地方。这使得将适配器实现为 class nearly 与将适配器实现为特征一样简洁。

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

您可以使用前面显示的 Groovy 类别,但不能强制转换。但是,可以通过在 AppPayment class.

中实现 asType() 来伪造强制转换并实现相同的语法
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