Статьи

Java 8: построитель безопасных типов карт с использованием альтернативного интерфейса

Выставляйте свои классы динамически

Когда я был новичком в Java, я помню, что думал, что должен быть способ удаления или сокрытия методов в моих классах, которые я не хотел показывать. Например, переопределение public метода private или чем-то в этом роде (что из corse не может и не должно быть возможным). Очевидно, что сегодня мы все знаем, что мы можем достичь той же цели, подвергая
interface вместо.

Герцог и Шпиль выставляют другой взгляд ...

Герцог и Шпиль обличают другой взгляд …

Используя схему с именем Alternating Interface Exposure , мы могли бы динамически просматривать методы класса и вводить безопасные типы, чтобы один и тот же класс мог применять шаблон, в котором он должен использоваться.

Позвольте мне взять пример. Допустим, у нас есть построитель Map который можно вызвать путем последовательного добавления ключей и значений до того, как будет построена настоящая Map . Схема Alternating Interface Exposure позволяет нам гарантировать, что мы вызываем метод key() и value() ровно столько же раз, а метод build() вызывается только (и это видно, например, в IDE), когда столько же ключей, сколько есть значений.

Схема Alternating Interface Exposure используется в проекте с открытым исходным кодом Speedment, которому я способствую. В Speedment схема используется, например, при построении типобезопасных кортежей, которые впоследствии будут созданы после добавления элементов в TupleBuilder . Таким образом, мы можем получить типизированный Tuple2<String, Integer> = {«Смысл жизни», 42}, если напишем TupleBuilder.builder().add("Meaning of Life).add(42).build() ,

Использование построителя динамических карт

В некоторых моих предыдущих постах (например, здесь ) я несколько раз писал о паттерне Builder и призываю вас вернуться к статье по этому вопросу, если вы не знакомы с концепцией, прежде чем читать дальше.

Задача состоит в том, чтобы создать построитель Map который динамически предоставляет ряд методов реализации с использованием ряда контекстно-зависимых интерфейсов. Кроме того, разработчик должен «изучить» свои типы ключей / значений при первом их использовании, а затем принудительно применить тот же тип ключей и значений для оставшихся записей.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
 
        // Use the type safe builder
        Map<Integer, String> map = Maps.builder()
                .key(1)                 // The key type is decided here for all following keys
                .value("One")           // The value type is decided here for all following values
                .key(2)                 // Must be the same or extend the first key type
                .value("Two")           // Must be the same type or extend the first value type
                .key(10).value("Zehn'") // And so on...
                .build();               // Creates the map!
 
        // Create an empty map
        Map<String, Integer> map2 = Maps.builder()
                .build();
         
         
    }
 
}

В приведенном выше коде, как только мы начинаем использовать Integer с помощью key(1) вызова key(1) , сборщик принимает только дополнительные ключи, которые являются экземплярами Integer . То же самое верно для значений. Как только мы вызываем value("one") , могут использоваться только объекты, которые являются экземплярами String . Например, если мы попытаемся записать value(42) вместо value("two") , мы сразу увидим ошибку в нашей IDE. Кроме того, большинство IDE может автоматически выбирать хороших кандидатов, когда мы используем завершение кода.

Позвольте мне пояснить значение этого:

Начальное использование

Конструктор создается с использованием метода Maps.builder() и возвращаемое начальное представление позволяет нам вызвать:

  1. build() которая создает пустую Map (как во втором примере с «пустой картой» выше)
  2. key(K key) который добавляет ключ к компоновщику и определяет тип (= K) для всех последующих ключей (как key(1) выше)

Как только начальный key(K key) вызван, появляется другое представление компоновщика:

  1. value(V value) которое добавляет значение в построитель и определяет тип (= V) для всех последующих значений (например, value("one") )

Обратите внимание, что метод build() не отображается в этом состоянии, поскольку количество ключей и значений различается. Написание Map.builder().key(1) .build() ; просто недопустимо, потому что нет значения, связанного с ключом 1 .

Последующее использование

Теперь, когда определены типы ключей и значений, компоновщик будет просто переключаться между двумя чередующимися интерфейсами в зависимости от того, вызывается ли key() или value() . Если вызывается key() , мы выставляем value() а если вызывается value() , мы выставляем и key() и build() .

Строитель

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

1
2
3
4
5
6
7
public interface KeyBuilder<K, V> {
 
        ValueBuilder<K, V> key(K k);
         
        Map<K, V> build();
     
}
1
2
3
4
5
public interface ValueBuilder<K, V> {
 
    KeyBuilder<K, V> value(V v);
 
}

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

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
31
32
public class Maps<K, V> implements KeyBuilder<K, V>, ValueBuilder<K, V> {
 
    private final List<Entry<K, V>> entries;
    private K lastKey;
 
    public Maps() {
        this.entries = new ArrayList<>();
    }
 
    @Override
    public ValueBuilder<K, V> key(K k) {
        lastKey = k;
        return (ValueBuilder<K, V>) this;
    }
 
    @Override
    public KeyBuilder<K, V> value(V v) {
        entries.add(new AbstractMap.SimpleEntry<>(lastKey, v));
        return (KeyBuilder<K, V>) this;
    }
 
    @Override
    public Map<K, V> build() {
        return entries.stream()
                .collect(toMap(Entry::getKey, Entry::getValue));
    }
 
    public static InitialKeyBuilder builder() {
        return new InitialKeyBuilder();
    }
 
}

Мы видим, что реализующий класс реализует оба чередующихся интерфейса, но возвращает только один из них в зависимости от того, вызваны ли key() или value() . Я немного «обманул», создав два начальных класса справки, которые позаботятся о начальной фазе, когда типы ключей и значений еще не определены. Для полноты изложения ниже также показаны два «читерских» класса:

01
02
03
04
05
06
07
08
09
10
11
public class InitialKeyBuilder {
 
    public <K> InitialValueBuilder<K> key(K k) {
        return new InitialValueBuilder<>(k);
    }
     
    public <K, V> Map<K, V> build() {
        return new HashMap<>();
    }
 
}
01
02
03
04
05
06
07
08
09
10
11
12
13
public class InitialValueBuilder<K> {
     
    private final K k;
 
    public InitialValueBuilder(K k) {
        this.k = k;
    }
     
    public <V> KeyBuilder<K, V> value(V v) {
        return new Maps<K, V>().key(k).value(v);
    }
 
}

Эти последние классы работают аналогично основному компоновщику так, как InitialKeyBuilder возвращает InitialValueBuilder который, в свою очередь, создает типизированный компоновщик, который будет использоваться бесконечно, поочередно возвращая либо KeyBuilder либо ValueBuilder .

Выводы

Схема Alternating Interface Exposure полезна, когда вам нужна модель классов, безопасная и учитывающая контекст. Используя эту схему, вы можете разработать и применить ряд правил для ваших классов. Эти классы будут намного более интуитивно понятны в использовании, поскольку контекстно-зависимая модель и ее типы распространяются на всю среду IDE. Схема также дает более надежный код, потому что потенциальные ошибки видны очень рано на этапе проектирования. Мы увидим потенциальные ошибки во время кодирования, а не как неудачные тесты или ошибки приложения.