Статьи

Оператор instanceof и замена шаблона посетителя в Java 8

У меня была мечта, когда instanceof оператора и downcasting больше не нужны, но без неуклюжести и многословия шаблона посетителя . Поэтому я придумал следующий синтаксис DSL:

1
2
3
4
5
6
7
Object msg = //...
  
whenTypeOf(msg).
    is(Date.class).    then(date -> println(date.getTime())).
    is(String.class).  then(str -> println(str.length())).
    is(Number.class).  then(num -> println(num.intValue())).
                     orElse(obj -> println("Unknown " + obj));

Отсутствие уныния, чистый синтаксис, строгий тип и… вполне достижимо в Java 8. Используя лямбды и немного обобщений, я создал крошечную библиотеку typeof которая является чистой, простой в использовании и более надежной, чем instanceof и шаблон Visitor , взятые вместе. Преимущества включают в себя:

  • нет явного уныния
  • избегает instanceof
  • чистый и простой в использовании
  • строго типизированный
  • работает с классами, которые мы не можем контролировать, включая JDK

Эта небольшая утилита была разработана с учетом Akka и Java API, чтобы ограничить использование оператора instanceof , но она гораздо более общая. Точно так же вы можете вернуть что-то в зависимости от типа среды выполнения:

1
2
3
4
5
6
7
int result = whenTypeOf(obj).
    is(String.class).thenReturn(String::length).
    is(Date.class).thenReturn(d -> (int) d.getTime()).
    is(Number.class).thenReturn(Number::intValue).
    is(TimeZone.class).thenReturn(tz -> tz.getRawOffset() / 1000).
    is(MyType.class).thenReturn(7).
    get();

Библиотека проверяет каждое предложение is() сверху вниз и останавливается, если находит первый соответствующий класс, включая родительские классы — поэтому is(Number.class) будет соответствовать как Integer и Float . Если ни одно из условий не выполнено, вызов get завершится с ошибкой. Вы можете переопределить это поведение, используя orElse() (легче прочитать, чем эквивалент is(Object.class) ):

1
2
3
4
int result = whenTypeOf(obj).
    is(String.class).thenReturn(String::length).
    //...
    orElse(42);

DSL использует статическую типизацию в Java, что делает почти невозможным неправильное использование библиотеки — большинство ошибок выявляется сразу во время компиляции. Все фрагменты кода ниже даже не скомпилируются:

01
02
03
04
05
06
07
08
09
10
11
12
//ERROR - two subsequent is()
whenTypeOf(obj).
    is(Foo.class).is(Bar.class)
  
//ERROR - then() without prior is()
whenTypeOf(obj).
    then(x -> println(x))
  
//ERROR - mixing then() and thenReturn()
whenTypeOf(obj).
    is(Foo.class).then(foo -> println(foo)).
    is(Bar.class).thenReturn(bar -> bar.getB());

По сути, вы начинаете с того, что набираете whenTypeOf() и Ctrl + space покажет вам, что именно разрешено. Ключом к созданию безопасных типов и надежных DSL на статически типизированных языках является максимально возможное ограничение API, чтобы избежать недопустимых состояний и вызовов во время компиляции. Вы получите множество маленьких классов , но это нормально, ваши пользователи не увидят этого. Например, проверьте FirstIs.java — объект, возвращаемый после вызова first is() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class FirstIs<S, T> {
    final Then<S> parent;
    private final S object;
    private final Class<T> expectedType;
  
    public Then<S> then(Consumer<T> thenBlock) {
        if (matchingType()) {
            thenBlock.accept(castObject());
            return new TerminalThen<>();
        }
        return parent;
    }
  
    public <R> ThenReturn<S, R> thenReturn(Function<T, R> result) {
        if (matchingType()) {
            return new TerminalThenReturn<>(object, result.apply(castObject()));
        }
        return new ThenReturn<>(object);
    }
  
    public <R> ThenReturn<S, R> thenReturn(R result) {
        if (matchingType()) {
            return new TerminalThenReturn<>(object, result);
        }
        return new ThenReturn<>(object);
    }
  
    //...
  
}

Написание DSL гораздо сложнее, чем их использование, но в конечном итоге это весьма полезно. Обратите внимание на то, как используются разные типы возвращаемых данных ( Then vs. ThenReturn ) только для того, чтобы убедиться, что на каждом этапе доступны только допустимые методы. Альтернативой является реализация проверок во время выполнения (например, вы не пишете is(...).is(...).then(...) ), но зачем беспокоиться, если компилятор может сделать это за нас? ?

Надеюсь, вам понравилась эта статья, дайте мне знать, если вы хотите попробовать эту утилиту в своем проекте. Это доступно на GitHub .

Ссылка: оператор instanceof и замена шаблона Visitor в Java 8 от нашего партнера по JCG Томаша Нуркевича из блога Java и соседей .