Предоставляет ли Java 8 альтернативу шаблону посетителя?

Этот популярный ответ на Qaru говорит об отличии между функциональным программированием и объектно-ориентированным программированием:

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

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

Скажем, у меня есть интерфейс Animal:

public interface Animal {
    public void speak();
}

И у меня есть Dog, Cat, Fish и Bird, которые реализуют интерфейс. Если я хочу добавить новый метод в Animal с именем jump(), мне придется пройти через все мои подклассы и реализовать jump().

Шаблон посетителя может облегчить эту проблему, но, похоже, что с новыми функциональными функциями, представленными на Java 8, мы должны решить эту проблему по-другому. В scala я мог бы просто просто использовать сопоставление с образцом, но Java пока этого не делает.

Действительно ли Java 8 упрощает добавление новых операций в существующие вещи?

Ответ 1

То, что вы пытаетесь выполнить, хотя и замечательно, в большинстве случаев не подходит для Java. Но прежде, чем я займусь этим...

Java 8 добавляет методы по умолчанию для интерфейсов! Вы можете определить методы по умолчанию, основанные на других методах интерфейса. Это уже было доступно для абстрактных классов.

public interface Animal {
    public void speak();
    public default void jump() {
        speak();
        System.out.println("...but higher!");
    }
}

Но в конце концов, вам нужно будет предоставить функциональность для каждого типа. Я не вижу огромной разницы между добавлением нового метода и созданием класса посетителя или частичных функций. Это просто вопрос о местонахождении. Вы хотите организовать свой код действием или объектом? (функциональный или объектно-ориентированный, глагол или существительное и т.д.)

Я предполагаю, что точка, которую я пытаюсь сделать, заключается в том, что Java-код организован "существительным" по причинам, которые не меняются в ближайшее время.

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

Java делает истинный "глагольный" подход немного сложным, потому что он выбирает, какой перегруженный метод запускать на основе типа времени компиляции аргументов (см. ниже и Перегруженный выбор метода на основе реальный тип параметра). Методы динамически отправляются только по типу this. Одним из причин наследования является предпочтительный метод обработки этих типов ситуаций.

public class AnimalActions {
    public static void jump(Animal a) {
        a.speak();
        System.out.println("...but higher!");
    }
    public static void jump(Bird b) { ... }
    public static void jump(Cat c) { ... }
    // ...
}
// ...
Animal a = new Cat();
AnimalActions.jump(a); // this will call AnimalActions.jump(Animal)
                       // because the type of `a` is just Animal at
                       // compile time.

Вы можете обойти это, используя instanceof и другие формы отражения.

public class AnimalActions {
    public static void jump(Animal a) {
        if (a instanceof Bird) {
            Bird b = (Bird)a;
            // ...
        } else if (a instanceof Cat) {
            Cat c = (Cat)a;
            // ...
        }
        // ...
    }
}

Но теперь вы просто выполняете работу, разработанную JVM для вас.

Animal a = new Cat();
a.jump(); // jumps as a cat should

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

P.S. Потому что я - тот парень. Я собираюсь создать Lisp, в частности, общую Lisp Object System (CLOS). Он предоставляет многоточие, которые отправляются на основе всех аргументов. Книга Practical Common Lisp даже предоставляет пример того, как это отличается от Java.

Ответ 2

Добавления, сделанные на язык Java, не отражают устаревшую устаревшую устаревшую концепцию. Фактически, шаблон Visitor очень хорош в поддержке добавления новых операций.

При сравнении этого шаблона с новыми возможностями Java 8 становится очевидным следующее:

  • Java 8 позволяет легко определять операции, содержащие одну функцию. Это удобно при обработке плоских однородных коллекций, таких как Iterable.forEach, Stream.forEach, но также Stream.reduce
  • Посетитель позволяет определить несколько функций, которые выбираются типом элемента и/или топологией структуры данных, которая становится интересной, когда функция одной функции перестает работать, при обработке гетерогенных коллекций и не-плоских структур, например. деревья предметов

Таким образом, новые функции Java 8 никогда не могут быть заменой шаблона посетителя, однако поиск возможных синергических связей является разумным. В этом ответе обсуждаются возможности модернизации существующего API (FileVisitor), чтобы включить использование лямбда-выражений. Решение представляет собой специализированную реализацию конкретного посетителя, которая делегирует соответствующие функции, которые могут быть указаны для каждого метода visit. Если каждая функция является необязательной (т.е. Для каждого метода visit существует разумное значение по умолчанию), она будет полезна, если приложение заинтересовано в небольшом подмножестве возможных действий или если оно хочет обрабатывать большинство из них равномерно.

Если некоторые из этих случаев использования считаются "типичными", может быть метод accept, принимающий одну или несколько функций, создающих соответствующего делегирующего посетителя за сценой (при разработке новых API или улучшении API под вашим контролем). Однако я бы не отказался от обычного accept(XyzVisitor), поскольку нельзя использовать недопустимую возможность использования существующей реализации посетителя.

Theres аналогичный выбор перегрузок в API Stream, если мы рассмотрим Collector как своего рода посетитель для Stream. Он состоит из четырех функций, максимально возможного для посещений плоской однородной последовательности предметов. Вместо того, чтобы реализовать этот интерфейс, вы можете инициировать сокращение с указанием одной функции или изменяемое сокращение с использованием трех функций, но есть общие ситуации, когда определение существующей реализации более кратким, например, с collect(Collectors.toList()) или collect(Collectors.joining(",")), чем указание всех необходимых функций с помощью лямбда-выражений/ссылок на методы.

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

Ответ 3

Лямбда-выражения могут упростить настройку (очень) плохого соответствия человека. Такая же методика может быть использована для упрощения создания посетителя.

static interface Animal {
    // can also make it a default method 
    // to avoid having to pass animal as an explicit parameter
    static void match(
            Animal animal,
            Consumer<Dog> dogAction,
            Consumer<Cat> catAction,
            Consumer<Fish> fishAction,
            Consumer<Bird> birdAction
    ) {
        if (animal instanceof Cat) {
            catAction.accept((Cat) animal);
        } else if (animal instanceof Dog) {
            dogAction.accept((Dog) animal);
        } else if (animal instanceof Fish) {
            fishAction.accept((Fish) animal);
        } else if (animal instanceof Bird) {
            birdAction.accept((Bird) animal);
        } else {
            throw new AssertionError(animal.getClass());
        }
    }
}

static void jump(Animal animal) {
    Animal.match(animal,
            Dog::hop,
            Cat::leap,
            fish -> {
                if (fish.canJump()) {
                    fish.jump();
                } else {
                    fish.swim();
                }
            },
            Bird::soar
    );
}