Выставляйте свои классы динамически
Когда я был новичком в 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 . |