Обобщенная реализация паттерна Visitor в Java

Я провел некоторое исследование, пытаясь разработать структуру преобразования типов, которая предоставляет возможность преобразовывать экземпляры исходного класса (например, Foo) в экземпляры классов результатов (например, Bar или Baz). Платформа должна обеспечивать возможность использования разной логики преобразования (т. Е. Разных преобразователей) для одной и той же пары источника и результата. Он также должен быть расширяемым, то есть позволять добавлять новые преобразователи для новых и существующих пар источника и результата. Еще одно требование - это безопасность типов, то есть любая попытка преобразовать экземпляр некоторого исходного класса в экземпляр класса результата без преобразователя, реализующего соответствующую логику преобразования, должна привести к ошибке времени компиляции.

Я решил использовать шаблон посетителей с конвертерами в качестве посетителей и конвертируемыми классами в качестве элементов. Чтобы обеспечить расширяемость и безопасность типов, я решил использовать дженерики. Итак, первая реализация структуры преобразования, которую я сделал под влиянием какой-то статьи в Интернете (к сожалению, я потерял ссылку), была ...

Платформа преобразования с преобразователями с отслеживанием состояния

Вот основные интерфейсы фреймворка Converter и Convertable:

    public interface Converter<V extends Converter<V,A>, A extends Convertable<V,A>> {

        void convert(A convertable);
    }


    public interface Convertable<V extends Converter<V,A>, A extends Convertable<V,A>> {

        void convertWith(V converter);
    }

Дженерики делают реализацию Convertable, принимают только реализации Converter, которые могут их преобразовывать, и делают реализацию Converter только для посещений реализаций Convertable, которые они сделали для преобразования. Вот пример таких преобразователей:

interface FooConverter extends Converter<FooConverter,Foo> {

    void convert(Foo convertable);

    void convert(FooChild1 convertable);

    void convert(FooChild2 convertable);
}


public class Foo2BarConverter implements FooConverter {

    private Bar result;

    public Bar getResult() {
        return result;
    }

    @Override
    public void convert(Foo convertable) {
        this.result = new Bar("This bar's converted from an instance of Foo");
    }

    @Override
    public void convert(FooChild1 convertable) {
        this.result = new Bar("This bar's converted from an instance of FooChild1");
    }

    @Override
    public void convert(FooChild2 convertable) {
        this.result = new Bar("This bar's converted from an instance of FooChild2");
    }
}


public class Foo2BazConverter implements FooConverter {

    private Baz result;

    public Baz getResult() {
        return result;
    }

    @Override
    public void convert(Foo convertable) {
        this.result = new Baz("This baz's converted from an instance of Foo");
    }

    @Override
    public void convert(FooChild1 convertable) {
        this.result = new Baz("This baz's converted from an instance of FooChild1");
    }

    @Override
    public void convert(FooChild2 convertable) {
        this.result = new Baz("This baz's converted from an instance of FooChild2");
    }
}

А вот несколько классов, которые можно преобразовать с помощью этих конвертеров:

public class Foo implements Convertable<FooConverter, Foo> {

    @Override
    public void convertWith(FooConverter converter) {
        converter.convert(this);
    }
}


public class FooChild1 extends Foo {

    @Override
    public void convertWith(FooConverter converter) {
        converter.convert(this);
    }
}


public class FooChild2 extends Foo {

    @Override
    public void convertWith(FooConverter converter) {
        converter.convert(this);
    }
}

Вот классы результатов, то есть Bar и Baz:

public class Bar {

    private String message;

    public Bar(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}


public class Baz {

    private String message;

    public Baz(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

А вот код, который проверяет эти преобразователи:

Foo fooObj = new Foo();
Foo fooChild1Obj = new FooChild1();
Foo fooChild2Obj = new FooChild2();

// converting to bar
Foo2BarConverter foo2BarConverter = new Foo2BarConverter();

fooObj.convertWith(foo2BarConverter);
System.out.println(foo2BarConverter.getResult().getMessage());

fooChild1Obj.convertWith(foo2BarConverter);
System.out.println(foo2BarConverter.getResult().getMessage());

fooChild2Obj.convertWith(foo2BarConverter);
System.out.println(foo2BarConverter.getResult().getMessage());

// converting to baz
System.out.println();
Foo2BazConverter foo2BazConverter = new Foo2BazConverter();

fooObj.convertWith(foo2BazConverter);
System.out.println(foo2BazConverter.getResult().getMessage());

fooChild1Obj.convertWith(foo2BazConverter);
System.out.println(foo2BazConverter.getResult().getMessage());

fooChild2Obj.convertWith(foo2BazConverter);
System.out.println(foo2BazConverter.getResult().getMessage());

и вывод, построенный с помощью этого кода

This bar's converted from an instance of Foo
This bar's converted from an instance of FooChild1
This bar's converted from an instance of FooChild2

This baz's converted from an instance of Foo
This baz's converted from an instance of FooChild1
This baz's converted from an instance of FooChild2

Взгляните на поле result в Foo2BarConverter и Foo2BazConverter. Это главный недостаток реализации. Это делает конвертеры с сохранением состояния, что не всегда удобно. Пытаясь избежать этого недостатка, я разработал ...

Платформа преобразования без двойной отправки

Суть этой реализации состоит в том, чтобы параметризовать преобразователи с помощью классов результатов и вернуть результаты из метода convert из Converter и convertWith из метода Convertable. Вот как это выглядит в коде:

public interface Converter<A extends Convertable<A>,R> {

    R convert(A convertable);
}

public interface Convertable<A extends Convertable<A>> {

    <R> R convertWith(Converter<A,R> converter);
}

public interface FooConverter<R> extends Converter<Foo,R> {

    @Override
    R convert(Foo convertable);

    R convert(FooChild1 convertable);

    R convert(FooChild2 convertable);
}

public class Foo2BarConverter implements FooConverter<Bar> {

    @Override
    public Bar convert(Foo convertable) {
        return new Bar("This bar's converted from an instance of Foo");
    }

    @Override
    public Bar convert(FooChild1 convertable) {
        return new Bar("This bar's converted from an instance of FooChild1");
    }

    @Override
    public Bar convert(FooChild2 convertable) {
        return new Bar("This bar's converted from an instance of FooChild2");
    }
}

public class Foo2BazConverter implements FooConverter<Baz> {

    @Override
    public Baz convert(Foo convertable) {
        return new Baz("This baz's converted from an instance of Foo");
    }

    @Override
    public Baz convert(FooChild1 convertable) {
        return new Baz("This baz's converted from an instance of FooChild1");
    }

    @Override
    public Baz convert(FooChild2 convertable) {
        return new Baz("This baz's converted from an instance of FooChild2");
    }
}

public class Foo implements Convertable<Foo> {

    @Override
    public <R> R convertWith(Converter<Foo,R> converter) {
        return converter.convert(this);
    }
}

public class FooChild1 extends Foo {

    @Override
    public <R> R convertWith(Converter<Foo,R> converter) {
        return converter.convert(this);
    }
}

public class FooChild2 extends Foo {

    @Override
    public <R> R convertWith(Converter<Foo,R> converter) {
        return converter.convert(this);
    }
}

V удален из Convertable объявления, потому что наличие класса результата в объявлении Converter фактически заставило бы нас параметризовать реализации Convertable с классами результатов. Он привязывает каждую реализацию convertable к единственному классу результата, в который она может быть преобразована. Итак, convertWith в Convertable относится к полученным преобразователям с Converter<A,R> интерфейсом. И вот в чем проблема. Теперь реализации Convertable, вызывающие полученный преобразователь, всегда будут вызывать convert, который определен в интерфейсе Converter, а не convert методы, которые отменяют его в реализациях Converter. Другими словами, convert(FooChild1 convertable) и convert(FooChild2 convertable) в Foo2BarConverter и Foo2BazConverter никогда не будут вызваны. По сути, он убивает основное понятие паттерна Visitor - двойную отправку. Вот тестовый код ...

Foo fooObj = new Foo();
Foo fooChild1Obj = new FooChild1();
Foo fooChild2Obj = new FooChild2();

// converting to bar
Foo2BarConverter foo2BarConverter = new Foo2BarConverter();
System.out.println(fooObj.convertWith(foo2BarConverter).getMessage());
System.out.println(fooChild1Obj.convertWith(foo2BarConverter).getMessage());
System.out.println(fooChild2Obj.convertWith(foo2BarConverter).getMessage());

System.out.println();

// converting to baz
Foo2BazConverter foo2BazConverter = new Foo2BazConverter();
System.out.println(fooObj.convertWith(foo2BazConverter).getMessage());
System.out.println(fooChild1Obj.convertWith(foo2BazConverter).getMessage());
System.out.println(fooChild2Obj.convertWith(foo2BazConverter).getMessage());

и его вывод, который демонстрирует, что методы переопределения не вызываются в этой реализации.

This bar's converted from an instance of Foo
This bar's converted from an instance of Foo
This bar's converted from an instance of Foo

This baz's converted from an instance of Foo
This baz's converted from an instance of Foo
This baz's converted from an instance of Foo

Следующая реализация, с которой я пытался сделать преобразователи без сохранения состояния, была ...

Конвертеры с параметризованными методами

Главное здесь - параметризовать только те методы, которые я хочу вернуть в результате преобразования, без параметризации деклараций интерфейсов.

public interface Converter<V extends Converter<V,A>, A extends Convertable<V,A>> {

    <R> R convert(A convertable);
}

public interface Convertable<V extends Converter<V,A>, A extends Convertable<V,A>> {

    <R> R convertWith(V converter);
}

interface FooConverter extends Converter<FooConverter,Foo> {

    <R> R convert(Foo convertable);

    <R> R convert(FooChild1 convertable);

    <R> R convert(FooChild2 convertable);
}

public class Foo2BarConverter implements FooConverter {

    @Override
    public Bar convert(Foo convertable) {
        return new Bar("This bar's converted from an instance of Foo");
    }

    @Override
    public Bar convert(FooChild1 convertable) {
        return new Bar("This bar's converted from an instance of FooChild1");
    }

    @Override
    public Bar convert(FooChild2 convertable) {
        return new Bar("This bar's converted from an instance of FooChild2");
    }
}

public class Foo2BazConverter implements FooConverter {

    @Override
    public Baz convert(Foo convertable) {
        return new Baz("This baz's converted from an instance of Foo");
    }

    @Override
    public Baz convert(FooChild1 convertable) {
        return new Baz("This baz's converted from an instance of FooChild1");
    }

    @Override
    public Baz convert(FooChild2 convertable) {
        return new Baz("This baz's converted from an instance of FooChild2");
    }
}

public class Foo implements Convertable<FooConverter, Foo> {

    @Override
    public <R> R convertWith(FooConverter converter) {
        return converter.convert(this);
    }
}

public class FooChild1 extends Foo {

    @Override
    public <R> R convertWith(FooConverter converter) {
        return converter.convert(this);
    }
}

public class FooChild2 extends Foo {

    @Override
    public <R> R convertWith(FooConverter converter) {
        return converter.convert(this);
    }
}

Код тестирования

Foo fooObj = new Foo();
Foo fooChild1Obj = new FooChild1();
Foo fooChild2Obj = new FooChild2();

// converting to bar
Foo2BarConverter foo2BarConverter = new Foo2BarConverter();
System.out.println(fooObj.<Bar>convertWith(foo2BarConverter).getMessage());
System.out.println(fooChild1Obj.<Bar>convertWith(foo2BarConverter).getMessage());
System.out.println(fooChild2Obj.<Bar>convertWith(foo2BarConverter).getMessage());

System.out.println();

// converting to baz
Foo2BazConverter foo2BazConverter = new Foo2BazConverter();
System.out.println(fooObj.<Baz>convertWith(foo2BazConverter).getMessage());
System.out.println(fooChild1Obj.<Baz>convertWith(foo2BazConverter).getMessage());
System.out.println(fooChild2Obj.<Baz>convertWith(foo2BazConverter).getMessage());

и это вывод

This bar's converted from an instance of Foo
This bar's converted from an instance of FooChild1
This bar's converted from an instance of FooChild2

This baz's converted from an instance of Foo
This baz's converted from an instance of FooChild1
This baz's converted from an instance of FooChild2

На первый взгляд выглядит великолепно. Но на самом деле это решение небезопасно. Например, следующий вызов

fooObj.<Baz>convertWith(foo2BarConverter).getMessage()

не вызовет ошибку времени компиляции. Но это привело бы к ClassCastException во время выполнения.

Итак, общий вопрос следующий.

Есть ли способ сделать посетителя с обобщенным типом без сохранения состояния безопасным с помощью Java?

UPD: я добавил ссылки на источники всех трех реализаций: 1st, 2-й и 3-й


person vect    schedule 01.04.2016    source источник


Ответы (2)


Ваши преобразователи - это просто функции, вам, вероятно, не нужна «структура» для их создания. И ваша третья попытка не имеет особого смысла:

<R> R convertWith(V converter);

означает: «учитывая что-то (преобразователь V, который ничего не знает о R, который вы хотите), дайте мне что-нибудь (произвольное R)». Как вы выяснили, это не работает.

Простая реализация с использованием исправленного шаблона посетителей:

interface FooConverter<R> extends Function<Foo, R> {

  R convert(Foo convertable);

  R convert(FooChild1 convertable);

  R convert(FooChild2 convertable);

  default R apply(Foo foo) { return foo.convertWith(this); }
}

public class Foo2BarConverter implements FooConverter<Bar> {

  @Override
  public Bar convert(Foo convertable) {
    return new Bar("This bar's converted from an instance of Foo");
  }

  @Override
  public Bar convert(FooChild1 convertable) {
    return new Bar("This bar's converted from an instance of FooChild1");
  }

  @Override
  public Bar convert(FooChild2 convertable) {
    return new Bar("This bar's converted from an instance of FooChild2");
  }
}

public class Foo2BazConverter implements FooConverter<Baz> {

  @Override
  public Baz convert(Foo convertable) {
    return new Baz("This baz's converted from an instance of Foo");
  }

  @Override
  public Baz convert(FooChild1 convertable) {
    return new Baz("This baz's converted from an instance of FooChild1");
  }

  @Override
  public Baz convert(FooChild2 convertable) {
    return new Baz("This baz's converted from an instance of FooChild2");
  }
}

public class Foo{

  public <R> R convertWith(FooConverter<R> converter) {
    return converter.convert(this);
  }
}

public class FooChild1 extends Foo {

  @Override
  public <R> R convertWith(FooConverter<R>  converter) {
    return converter.convert(this);
  }
}

public class FooChild2 extends Foo {

  @Override
  public <R> R convertWith(FooConverter<R> converter) {
    return converter.convert(this);
  }
}

public void test() {
  Foo fooObj = new Foo();
  Foo fooChild1Obj = new FooChild1();
  Foo fooChild2Obj = new FooChild2();

  // converting to bar
  Foo2BarConverter foo2BarConverter = new Foo2BarConverter();
  System.out.println(fooObj.convertWith(foo2BarConverter).getMessage());
  System.out.println(fooChild1Obj.convertWith(foo2BarConverter).getMessage());
  System.out.println(fooChild2Obj.convertWith(foo2BarConverter).getMessage());

  System.out.println();

  // converting to baz
  Foo2BazConverter foo2BazConverter = new Foo2BazConverter();
  System.out.println(fooObj.convertWith(foo2BazConverter).getMessage());
  System.out.println(fooChild1Obj.convertWith(foo2BazConverter).getMessage());
  System.out.println(fooChild2Obj.convertWith(foo2BazConverter).getMessage());

  // does not compile:
  fooObj.<Baz>convertWith(foo2BarConverter).getMessage();
}

Затем, если вам нужна дополнительная структура, вы можете изучить линзы: https://github.com/functionaljava/functionaljava/tree/master/core/src/main/java/fj/data/optic

person JbGi    schedule 02.04.2016
comment
Спасибо за ваш ответ. Я думал о таком решении. Помимо того, что Converter и Convertable необходимы по некоторым причинам, которые я намеренно опустил в своем посте (он слишком велик и без этого), эти интерфейсы составляют основу фреймворка. Без них не будет отдельных преобразователей, ничего не связанных друг с другом. - person vect; 03.04.2016
comment
Конвертер - это в основном функция, и вы можете составлять функции. Добавил это к моему ответу. - person JbGi; 03.04.2016

Вы можете просто отказаться от шаблона посетителя, поскольку есть лучшее решение для Java.

Ловушка паттерна посетителей:

Это навязчиво и противоречит шаблону

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

public class ParentDataModel
{
    public void accept(Visitor visitor)
    {
        visitor.visit(this);
    }
}

public class ChildDataModel extends ParentDataModel
{
    // no need to implement accept() by the child itself
}

public class Visitor
{
    public void visit(ParentDataModel model)
    {
        // do something with it
    }

    public void visit(ChildDataModel model)
    {
        // do something with it
    }
}

Почему модель данных должна знать посетителя? Модель данных должна содержать только те данные, которые относятся к модели.

Не сочетается с существующими объектами из внешних фреймворков

Что, если вам нужно что-то сделать, скажем, с Number, Double, которые взяты из JDK.

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

public class NumberWrapper
{
    private Number value;

    public void accept(Visitor visitor)
    {
        visitor.visit(value);
    }
}

public class DoubleWrapper
{
    private Double value;

    public void accept(Visitor visitor)
    {
        visitor.visit(value);
    }
}

public class Visitor
{
    public void visit(Number value)
    {
        // do something with it
    }

    public void visit(Double value)
    {
        // do something with it
    }
}

Решение: всем ими управляет один класс

public static class SuperConsumer implements Consumer
{
    private Map<Class<?>, Consumer<?>> consumers = new HashMap<>();
    private Consumer<?> unknown = o -> System.err.println("Unknown object type");

    public SuperConsumer()
    {
        consumers.put(Number.class, o -> consumeNumber(o));
        consumers.put(Double.class, o -> consumeDouble(o));
    }

    private void consumeNumber(Number value)
    {
         System.out.printf("Consuming: %s\n", value.getClass().getName());
    }

    private void consumeDouble(Double value)
    {
         System.out.printf("Consuming: %s\n", value.getClass().getName());
    }

    private Consumer findConsumer(Object object)
    {
        Consumer consumer = consumers.get(object.getClass());

        Class superClazz = object.getClass().getSuperclass();
        while (consumer == null && superClazz != Object.class)
        {
            consumer = consumers.get(superClazz);
            superClazz = superClazz.getSuperclass();
        }

        Class<?>[] interfaces = object.getClass().getInterfaces();
        for (int i = 0; consumer == null && i < interfaces.length; i++)
        {
            consumer = consumers.get(interfaces[i]);
        }

        return consumer;
    }

    @Override
    public void accept(Object object)
    {
        Consumer consumer = findConsumer(object);
        if (consumer == null)
        {
            consumer = unknown;
        }
        consumer.accept(object);
    }

    public static void main(String[] args)
    {
        Consumer consumer = new SuperConsumer();
        Arrays.asList(new Double(1.0), new Integer(1), new Float(1.0f)).forEach(o -> consumer.accept(o));
    }
}
person Kevin Wang    schedule 08.01.2017
comment
Произойдет ли ошибка времени компиляции, если потребитель будет вызван с объектом, тип которого не поддерживается потребителем? Одним из преимуществ, которые дает шаблон посетителя, является безопасность типов. - person vect; 19.01.2017
comment
Нет, ошибки времени компиляции не будет, если присмотреться, потребитель принимает Object. Для неподдерживаемого типа вы просто предоставляете запасного потребителя или даже генерируете исключение, выбор стратегии зависит от варианта использования. - person Kevin Wang; 19.01.2017