Утиный интерфейс Groovy?

Мне поручено разработать библиотеку для приложения Grails. Приложение Grails имеет множество объектов предметной области (около 100+ таблиц). Я не хочу, чтобы моя библиотека зависела от приложения Grails, что делает мою библиотеку зависимой от базы данных и трудно тестируемой (запуск Grails занимает много времени).

Например, приложение Grails имеет один объект домена Payment, который содержит множество полей и зависит от множества других объектов домена.

Мне нужны только некоторые поля, а не все поля или другие зависимые объекты домена.

Я новичок в groovy, зная, что в groovy есть Duck Type. Я подумал, что можно определить утиный интерфейс, который мне не нужно изменять в объекте 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, извините, без черт.


person smallufo    schedule 17.09.2015    source источник
comment
Что ваша библиотека должна делать с доменными классами вашего приложения?   -  person Emmanuel Rosa    schedule 17.09.2015
comment
@EmmanuelRosa: например: оберните этот объект Payment, чтобы уведомить внешнюю систему.   -  person smallufo    schedule 18.09.2015
comment
Но если библиотека оборачивает оплату, это сделает ее зависимой от приложения. Правильно?   -  person Emmanuel Rosa    schedule 18.09.2015
comment
Верно . Но во время разработки я не хочу, чтобы моя библиотека зависела от приложения Grails. Это делает разработку громоздкой, трудно тестируемой...   -  person smallufo    schedule 18.09.2015
comment
Ах я вижу. Однако вы можете навсегда сохранить независимость библиотеки от приложения, добавив промежуточный адаптирующий слой, используя шаблон проектирования адаптера. Этот слой будет жить в приложении, но он будет довольно маленьким. Таким образом, ваша библиотека останется пригодной для тестирования и повторного использования.   -  person Emmanuel Rosa    schedule 18.09.2015
comment
Да ! Это то, что я хочу.   -  person smallufo    schedule 18.09.2015


Ответы (2)


Ниже показано, как вы можете создать свою библиотеку независимо от своего приложения 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: '[email protected]') ,
    userIpAddress: '192.168.1.101')

processor.process payment as PaymentAdapter

В этом случае AppPayment преобразуется в PaymentAdapter путем применения признака во время выполнения. Поскольку PaymentAdapter осуществляет платеж, PaymentProcessor.process() принимает его.

Классная категория

Вы можете обработать приведение в категории 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(). ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: это мой любимый вариант; Я просто думаю, что это довольно... Круто.

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: '[email protected]') ,
    userIpAddress: '192.168.1.101') as PaymentAdapter

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

Этого не происходит с трейтами времени компиляции.

Без признаков

Версии Groovy до 2.3 не поддерживают трейты, поэтому адаптер должен быть классом. Вы можете начать с создания универсального адаптера в библиотеке.

/*
 * 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 и ожидает, что делегат сделает то же самое, но через утиную печать. Это означает, что подклассы должны реализовывать только то, что отличается от интерфейса 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)

Вы можете использовать категорию Groovy, как показано ранее, но не принудительное принуждение. Однако можно сымитировать приведение и добиться того же синтаксиса, реализовав 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
person Emmanuel Rosa    schedule 18.09.2015
comment
Спасибо . Черта красивая. Но мой groovy 1.8.8 не поддерживает трейты :( (извините, я не упомянул об этом в своем вопросе) - person smallufo; 18.09.2015
comment
Добро пожаловать, и спасибо. Я обновил вопрос дополнительным адаптером на основе классов. - person Emmanuel Rosa; 18.09.2015
comment
Спасибо. Преобразование Groovy такое волшебство... Что касается def asType(Class type), я не могу трогать код AppPayment. В любом случае, это красивое решение. - person smallufo; 18.09.2015
comment
Есть способ добавить asType() в AppPayment без изменения кода: метод ExpandoMetaClass. Это перезапишет любой существующий asType(), но вы можете сделать что-то вроде этого: AppPayment.metaClass.asType << {Class type -> type == Payment ? new PaymentAdapter(delegate) : throw new Exception("Type $type not supported") } Вы можете сделать это в Bootstrap.groovy. - person Emmanuel Rosa; 18.09.2015
comment
Спасибо . Интересно, есть ли способ automatically переопределить это asType ? Так что приложению Grails просто нужно импортировать мою библиотеку, не нужно подготавливать asType ? Если такого asType нет, groovy жалуется GroovyCastException: Cannot cast object 'Payment@180510a0' with class 'Payment' to class 'IPayment' - person smallufo; 18.09.2015
comment
В этом есть смысл. Реализация Object.asType() не знает, как создать IPayment. Вы не сможете автоматически переопределить asType не по техническим причинам (есть способ сделать это, я просто не помню, как), а потому, что для asType нужен адаптер, а адаптеры зависят от приложения; не многоразовый. - person Emmanuel Rosa; 18.09.2015
comment
Я нашел способ автоматически обрабатывать адаптацию платежей. Мы вышли за рамки первоначального вопроса (на который я не возражаю), поэтому просто свяжитесь со мной в Google+ plus.google.com/109886199751744223967/posts - person Emmanuel Rosa; 19.09.2015
comment
Привет , я не могу найти его на стене вашего Google+ ... ? - person smallufo; 19.09.2015

Вы можете использовать оператор 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()")
}
person Will    schedule 17.09.2015
comment
Спасибо за подробный ответ. - person smallufo; 18.09.2015