Рубрики
Без рубрики

Утиный набор текста на java

Создание динамических прокси-серверов для имитации набора текста на java. Помеченный как java, эксперимент, веселье.

Отказ от ответственности: пожалуйста, не воспринимайте это слишком серьезно. Это просто забава и игры.

Утиный ввод – это идиома, известная в основном из динамически типизированных языков. В нем говорится, что вы можете рассматривать несвязанные объекты типа X как объекты типа Y, если оба имеют один и тот же открытый интерфейс.

Если он выглядит как утка, двигается как утка и издает звуки как утка, то это должна быть утка!

С другой стороны, система статических типов Java налагает строгие правила на совместимость типов. Простое использование одних и тех же методов не позволяет двум классам быть “совместимыми с типами” . Рассмотрим этот пример:

public class Duck {
    public String makeNoise() {
        return "Quak Quak";
    }
}

public class Dog {
    public String makeNoise() {
        return "woof woof";
    }
}

Оба класса имеют одинаковый интерфейс, поэтому можем ли мы назначить экземпляры соответствующему другому типу?

final Duck duck = new Dog(); // fails
assertEquals("woof woof", duck.makeNoise());

Конечно, мы не можем. Система типов Java нуждается в явных определениях совместимых классов с использованием наследования.

Давайте теперь заставим наш пример работать, используя самые простые мыслимые изменения в нашем коде:

public class Dog extends Duck {
    @Override
    public String makeNoise() {
        return "woof woof";
    }
}

Помимо всего, что вы узнали на уроках биологии, наша Собака теперь является уткой и, следовательно, может присваиваться переменным с типом Утка . Это изменение должно было заставить приведенный выше тест стать зеленым.

Хорошо, теперь ваша собака – Утка, но как насчет всех других классов в нашей кодовой базе, которые также имеют makeNoise метод? Должны ли они все расширять класс Duck ? Это кажется непрактичным и сбивающим с толку ( поезд класса расширяет утку еще более странно, чем Собака, являющаяся Уткой).

Возможно, мы сможем создать более динамичную утку, используя отражение . Во время выполнения JVM содержит довольно обширную информацию о типе каждого объекта. Помимо доступа к информации о типе, отражение также позволяет нам динамически извлекать и вызывать методы для произвольных объектов.

public class DynamicDuck extends Duck {

    private final Object notADuck;

    public DynamicDuck(Object notADuck) {
        this.notADuck = notADuck;
    }

    @Override
    public String makeNoise() {
        // try block is needed because the reflection stuff will 
        // throw all kind of horrible exceptions if you use it wrong
        try {
            // retrieve dynamic type information of the non-duck object
            final Class notADuckType = this.notADuck.getClass();

            // find the method of the class that is not a duck by name
            final Method delegate = notADuckType.getMethod("makeNoise");

            // invoke the method on the object that is not a duck
            return (String) delegate.invoke(this.notADuck);
        } catch (Exception e) {
            throw new IllegalStateException("DynamicDuck error");
        }
    }
}

Используя нашу Динамическую утку мы можем получить “утиный вид” для каждого объекта, не являющегося утиным. Давайте соответствующим образом скорректируем наш первоначальный тест (а также удалим предложение extends в классе Dog ):

final Duck duck = new DynamicDuck(new Dog());
assertEquals("woof woof", duck.makeNoise());

Это немного более динамично, но все равно плохо масштабируется. Если бы в классе Duck было больше открытых методов, нам пришлось бы делегировать каждый метод объекту, не являющемуся утиным, как мы сделали выше. И если мы решили рассматривать все не как Утку, а как Гуся, нам нужно дополнительно создать класс Динамический гусь . Все это слишком громоздко.

К счастью, в Java есть еще одна полезная концепция, которая пригодится в нашем случае использования. Каждый класс, доступный во время выполнения, будет загружен из своего представления байтового кода с использованием Загрузчика классов . Поскольку это чисто функция времени выполнения, вы можете создавать искусственные классы во время выполнения, которые не существовали во время компиляции.

Мы можем динамически генерировать байт-код класса, который расширяет Duck и который автоматически делегирует каждый вызываемый метод эквивалентному методу произвольного объекта, не являющегося утиным. В следующем коде используются популярные библиотеки cglib (для генерации байтового кода) и Objenesis (для создания экземпляров динамически генерируемых классов.

import net.sf.cglib.core.DefaultNamingPolicy;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.InvocationHandler;
import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;

import java.lang.reflect.Method;

public class NotADuck {
    /**
     * The unique callback index to which each method in the proxy object is mapped.
     */
    private static final int CALLBACK_INDEX = 0;

    /**
     * Maps all methods to index {@link #CALLBACK_INDEX}.
     */
    private static final CallbackFilter ZERO_CALLBACK_FILTER = method -> CALLBACK_INDEX;

    // with caching
    private static final Objenesis OBJENESIS = new ObjenesisStd(true);

    public static  T asDuck(Object notADuck, Class type) {
        final Enhancer enhancer = new Enhancer();
        final InvocationHandler invocationHandler = new DelegateDuckToNonDuckMethod(notADuck);

        enhancer.setSuperclass(type);
        enhancer.setUseFactory(true);
        enhancer.setNamingPolicy(new DefaultNamingPolicy());
        enhancer.setCallbackFilter(ZERO_CALLBACK_FILTER);
        enhancer.setCallbackType(invocationHandler.getClass());

        final Class proxyClass = enhancer.createClass();
        final T duckInstance = OBJENESIS.getInstantiatorOf(proxyClass).newInstance();
        final Factory factory = (Factory) duckInstance;
        factory.setCallback(CALLBACK_INDEX, invocationHandler);
        return duckInstance;
    }

    private static class DelegateDuckToNonDuckMethod implements InvocationHandler {

        private final Object notADuck;

        private DelegateDuckToNonDuckMethod(Object notADuck) {
            this.notADuck = notADuck;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            final Method nonDuckMethod = notADuck.getClass().getMethod(method.getName(), method.getParameterTypes());
            return nonDuckMethod.invoke(notADuck, args);
        }
    }
}

Используя этот класс, мы можем создать динамическое представление экземпляра собаки в виде утки:

final Duck duck = NotADuck.asDuck(new Dog(), Duck.class);
assertEquals("woof woof", duck.makeNoise());

Код больше не ограничивается только созданием Утиных объектов:

List list = NotADuck.asDuck("I'm a String", List.class);
// both List and String have an isEmpty method
assertFalse(list.isEmpty());

Это был всего лишь небольшой эксперимент. В этой реализации вероятны ошибки, выявленные крайние случаи, проблемы с производительностью и общее отсутствие практических вариантов использования. Тем не менее, интересно посмотреть, как далеко вы можете зайти с информацией о динамических типах Java и материализацией произвольного байтового кода.

Не делай этого дома!

Оригинал: “https://dev.to/skuzzle/duck-typing-in-java-6gh”