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
我被指派开发一个辅助 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