Шаблон Transformer — это шаблон проектирования для Java (и, возможно, других ОО-языков с только дисперсией сайта использования и инвариантными типами параметров), который помогает объектам в иерархии подтипов свободно преобразовывать себя в объекты любого типа.
контекст
Я следил за потоками OpenJDK ( 18-21 сентября, 12-13 ноября, 13-30 ноября , 3-4 декабря ), связанными с проблемой JDK-8203703 Джима Ласки, и мне пришла в голову идея. Позвольте мне повторить соответствующие части обсуждения.
Предложение String.transform
Предложение согласно JDK-8203703 сводится к следующему дополнению:
|
1
2
3
4
5
6
7
|
public final class String implements /*...*/ CharSequence { // ... public <R> R transform(Function<? super String, ? extends R> f) { return f.apply(this); } // ...} |
Как видите, этот метод просто вызывает данную Function для себя, и все. Тем не менее, это очень полезно для объединения служебных методов, подобных тем, которые используются в StringUtils от Apache Commons :
|
1
2
3
4
|
String result = string .toLowerCase() .transform(StringUtils::stripAccents) .transform(StringUtils::capitalize); |
Обычно мы должны написать:
|
1
|
String result = StringUtils.capitalize(StringUtils.stripAccents(string.toLowerCase())); |
Учитывая CharSequence.transform
В какой-то момент Алан Бейтман поднял вопрос о потенциальном определении transform в CharSequence как:
|
1
|
<R> R transform(Function<? super CharSequence, ? extends R> f) |
Это даст преимущество в том, что вы сможете применять CharSequence основе CharSequence (например, StringUtils.isNumeric ) к любому CharSequence , например:
|
1
2
3
|
boolean isNumeric = charSequence .transform(s -> StringUtils.defaultIfBlank('0')) .transform(StringUtils::isNumeric); |
Однако, как отметил Реми Форакс, проблема с этой подписью заключается в том, что:
- если бы он был унаследован
String: большинство служебных методов принимаютStringв качестве параметра — такие методы не будут работать (например, StringUtils :: capitalize ), - если он был переопределен
String: полезное переопределение сделать невозможно, потому что:-
Function<? super String, R>Function<? super String, R>— это супертипFunction<? super CharSequence, R>Function<? super CharSequence, R>(что на самом деле хорошо), - Java не поддерживает контравариантные типы параметров (что является настоящим препятствием здесь).
-
В результате тема CharSequence.transform была удалена.
проблема
Подводя итог, проблема состоит в том, чтобы иметь возможность преобразовать :
-
CharSequence, используяFunctionкоторая принимаетCharSequenceилиObject(? super CharSequence), -
String, используяFunctionкоторая принимаетStringили любой из ее супертипов (? super String).
Когда я посмотрел на эти нижние границы здесь, я понял, что я уже видел такого рода проблемы (см. Шаблон Filterer ).
Таким образом, эта проблема сводится к следующему: как ковариантно указать контравариантную оценку для Function .
Решение
Java не поддерживает контравариантные типы параметров , а ее синтаксис не позволяет ковариантно ( ? extends ) определять контравариантную ( ? super ) границу в одном объявлении. Однако это можно сделать в двух отдельных объявлениях с помощью промежуточного вспомогательного типа.
Предполагая, что мы хотим решить это для универсальной Function<? super T, ? extends R> Function<? super T, ? extends R> Function<? super T, ? extends R> , нам нужно:
- переместите вышеуказанный параметр
Functionв интерфейс помощника, параметризованный с помощьюT, - используйте этот вспомогательный интерфейс с верхней границей (
? extends T) в качестве возвращаемого типа.
Интерфейс трансформатора
Я определил такой вспомогательный интерфейс (который я назвал Transformer ) следующим образом:
|
1
2
3
4
|
@FunctionalInterfaceinterface Transformer<T> { <R> R by(Function<? super T, ? extends R> f);} |
Трансформируемый интерфейс
Определив Transformer , мы можем определить следующий базовый интерфейс, называемый Transformable :
|
1
2
3
|
interface Transformable { Transformer<?> transformed();} |
Этот интерфейс сам по себе мало что делает, но я рассматриваю его как спецификацию для:
- разработчики подтипов : он напоминает им переопределить
transformedметод с правильной верхней границей и реализовать его, - пользователи подтипов : это напоминает им, что они могут вызывать
transformed().by(f)
Подводя итог, эта пара ( Transformer & Transformable ) позволяет нам заменить:
-
obj.transform(function) - with:
obj.transformed().by(function)
Пример реализации
Прежде чем мы вернемся к String , давайте посмотрим, насколько легко реализовать оба этих интерфейса:
|
01
02
03
04
05
06
07
08
09
10
11
|
class Sample implements Transformable { @Override public Transformer<Sample> transformed() { return this::transform; // method reference } private <R> R transform(Function<? super Sample, ? extends R> f) { return f.apply(this); }} |
Как видите, все, что нужно, это ссылка на метод для transform .
Метод transform был закрытым, чтобы в подтипах не возникало конфликтов, когда они определяют свое собственное (соответственно ограниченное снизу ) transform .
Решение в контексте
Реализация в контексте
Как это может применяться к CharSequence и String ? Во-первых, мы сделаем CharSequence расширяемым Transformable :
|
1
2
3
4
5
6
|
public interface CharSequence extends Transformable { // ... @Override Transformer<? extends CharSequence> transformed(); // ...} |
Затем мы реализовали transformed в String , возвращая ссылку на метод public transform ( добавлен в JDK 12 ):
|
1
2
3
4
5
6
7
8
|
public final class String implements /*...*/ CharSequence { // ... @Override public Transformer<String> transformed() { return this::transform; } // ...} |
Обратите внимание, что мы внесли ковариантное изменение в возвращаемый тип transformed : Transformer<? extends CharSequence> Transformer<? extends CharSequence> → Transformer<String> .
Риск совместимости
Я CharSequence.transformed риск совместимости добавления CharSequence.transformed как минимальный. Это может нарушить обратную совместимость только для тех подклассов CharSequence , у которых уже есть transformed метод без аргументов (который кажется маловероятным).
Использование в контексте
Использование для String не изменится, потому что нет смысла вызывать transformed().by() вместо transform() .
Однако для использования универсального CharSequence необходимо прибегнуть к transformed().by() CharSequence transformed().by() поскольку он может иметь много реализаций, поэтому методы transform должны быть private :
|
1
2
3
|
boolean isNumeric = charSequence .transformed().by(s -> StringUtils.defaultIfBlank('0')) .transformed().by(StringUtils::isNumeric); |
Производительность
Если вы не знакомы с тем, как работает JVM (что чаще всего означает HotSpot ) и его JIT-компилятор , вы можете задаться вопросом, не повлияет ли это очевидное создание дополнительного объекта ( Transformer in transformed ) на производительность.
К счастью, благодаря escape-анализу * и скалярной замене этот объект никогда не размещается в куче. Так что ответ: нет, не будет.
* Эта запись в Википедии содержит ложное утверждение: « Таким образом, компилятор может безопасно разместить оба объекта в стеке. «Как объясняет Алексей Шипилёв , Java не выделяет целые объекты в стеке.
эталонный тест
Если вам нужны доказательства, вот небольшой эталонный тест (с использованием превосходного жгута проводов JMH Алексея Шипилёва). Поскольку я не мог (легко) добавить необходимые методы в String , я создал простую оболочку для String и реализовал эталонный тест поверх него.
Тест тестирует работу toLowerCase() :
- на две строки:
-
"no change"(без операции) -
"Some Change"
-
- используя три типа вызовов:
- прямой (базовый уровень)
-
transform() -
transformed().by()
Вы можете найти полный исходный код для этого теста в этой главе GitHub .
Вот результаты (запустить на Oracle JDK 8, заняло 50 минут):
|
1
2
3
4
5
6
7
8
9
|
Benchmark (string) Mode Cnt Score Error UnitsTransformerBenchmark.baseline no change avgt 25 22,215 ± 0,054 ns/opTransformerBenchmark.transform no change avgt 25 22,540 ± 0,039 ns/opTransformerBenchmark.transformed no change avgt 25 22,565 ± 0,059 ns/opTransformerBenchmark.baseline Some Change avgt 25 63,122 ± 0,541 ns/opTransformerBenchmark.transform Some Change avgt 25 63,405 ± 0,196 ns/opTransformerBenchmark.transformed Some Change avgt 25 62,930 ± 0,209 ns/op |
Как видите, для обеих строк нет разницы в производительности между тремя типами вызовов.
Резюме
Я понимаю, что Transformable , вероятно, слишком «экстравагантен», чтобы действительно превращать его в JDK. На самом деле, даже один Transformer , возвращаемый CharSequence и String , не стоит того. Это потому, что унарные операции над CharSequence s не кажутся такими распространенными (например, StringUtils содержит всего несколько).
Тем не менее, я нашел общую идею Transformer и Transformable довольно заманчивым. Надеюсь, вам понравилось чтение, и вы найдете его полезным в определенных контекстах.
|
Опубликовано на Java Code Geeks с разрешения Томаша Линковски, партнера нашей программы JCG. Смотреть оригинальную статью здесь: Transformer Pattern Мнения, высказанные участниками Java Code Geeks, являются их собственными. |