Выставляйте свои классы динамически
Когда я был новичком в 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() и возвращаемое начальное представление позволяет нам вызвать:
-
build()которая создает пустуюMap(как во втором примере с «пустой картой» выше) -
key(K key)который добавляет ключ к компоновщику и определяет тип (= K) для всех последующих ключей (какkey(1)выше)
Как только начальный key(K key) вызван, появляется другое представление компоновщика:
-
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. Схема также дает более надежный код, потому что потенциальные ошибки видны очень рано на этапе проектирования. Мы увидим потенциальные ошибки во время кодирования, а не как неудачные тесты или ошибки приложения.
| Ссылка: | Java 8: построитель безопасных типов карт с использованием альтернативного интерфейса. Информация от нашего партнера JCG Пера Минборга в блоге Minborg о Java Pot . |