Статьи

Выкройка фильтра в 10 шагов

Фильтр — это шаблон, который следует применять только в определенных случаях. В оригинальном посте я представил очень простой пример, призванный показать, как его применять. В этом посте я представлю гораздо более подробный пример, который также призван объяснить, когда и зачем его применять.

Вступление

Пост состоит из следующих 10 коротких шагов . На каждом этапе я представляю требования следующих двух типов:

  • B- *: бизнес — требования (данные владельцем продукта → бесспорный)
  • S- * : требования к решениям (вытекающие из выбора решений → спорные )

и я представляю модель Java, отвечающую требованиям, введенным до сих пор. Я делаю это, пока Filterer не Filterer в качестве предпочтительного решения.

Итак, позвольте мне взять вас в этом путешествии …

Шаг 1: детектор проблем

Требования № 1

Давайте предположим, что бизнес запрашивает алгоритм для выявления грамматических и орфографических проблем в английских текстах .

Например:

  • текст: вы знаете это. → проблемы для обнаружения:
    1. миг (тип: орфография)
  • Текст: я должен проиграть. → проблемы для обнаружения:
    1. отмечая (тип: орфография)
    2. потерять (тип: грамматика)
  • Текст: я продолжал отмечать, что это свободно. → проблемы для выявления: ∅

Это наше первое деловое требование ( B-1 ).


Простейшей моделью встречи B-1 может быть:

  • ввод : простой текст
  • вывод : список проблем, где каждая проблема обеспечивает:
    • смещения во входном тексте
    • тип (грамматика / орфография)

Это наше первое требование к решению ( S-1 ).

Модель Java № 1

Мы можем моделировать S-1 как:

1
2
3
4
interface IssueDetector {
  // e.g. text: "You migth know it."
  List<Issue> detect(String text);
}

где:

1
2
3
4
5
interface Issue {
  int startOffset(); // e.g. 4 (start of "migth")
  int endOffset(); // e.g. 9 (end of "migth")
  IssueType type(); // e.g. SPELLING
}
1
enum IssueType { GRAMMAR, SPELLING }

Это совершить 1 .

Шаг 2: Вероятность

Требования № 2

Однако было бы довольно сложно реализовать настоящий IssueDetector который работал бы таким детерминированным образом:

  • проблема (вероятность P = 100% )
  • не проблема (вероятность P = 0% )

Вместо этого IssueDetector должен быть скорее вероятностным :

  • вероятная проблема (вероятность P =? )

Мы можем сохранить различие между проблемой и отсутствием, введя порог вероятности ( PT ):

  • проблема (вероятность P ≥ PT ),
  • не проблема (вероятность P <PT ).

Тем не менее, стоит адаптировать модель для сохранения вероятности ( P ) — это полезно, например, при рендеринге (более высокая вероятность → более заметный рендеринг).

Подводя итог, наши дополнительные требования решения :

  • S-2 : вероятность возникновения проблемы поддержки ( P );
  • S-3 : порог вероятности поддержки ( PT ).

Модель Java № 2

Мы можем встретить S-2 , добавив probability() к Issue :

1
2
3
4
interface Issue {
  // ...
  double probability();
}

Мы можем встретиться с S-3 , добавив probabilityThreshold IssueDetector в IssueDetector :

1
2
3
interface IssueDetector {
  List<Issue> detect(String text, double probabilityThreshold);
}

Это совершить 2 .

Шаг 3: Вероятная проблема

Требования № 3

Предположим, что бизнес требует :

Такой корректный текст (или: контрольный пример ) может быть определен как:

  • текст, например, вы должны знать это.
  • ожидаемые проблемы, например
    1. shuold (тип: правописание)

Итак, наше решение :

  • S-4 : Поддержка ожидаемых проблем (= нет вероятности).

Модель Java № 3

Мы можем встретить S-4 , извлекая подынтерфейс ( ProbableIssue ):

1
2
3
interface ProbableIssue extends Issue {
  double probability();
}

и возвращая ProbableIssue из IssueDetector :

1
2
3
interface IssueDetector {
  List<ProbableIssue> detect(...);
}

Это совершить 3 .

Шаг 4: Выпуск текста

Требования № 4

Предположим, что:

  1. Все тестовые примеры определены внешне (например, в файлах XML);
  2. Мы хотим создать параметризованный тест JUnit, где параметры — это тестовые случаи, представленные в виде Stream .

Как правило, тестовый пример представляет собой нечто, что мы могли бы назвать проблемным текстом (текст + его проблемы).

Чтобы избежать моделирования проблемного текста как Map.Entry<String, List<Issue>> (который является расплывчатым и означает недостаточную абстракцию ), давайте введем еще одно требование к решению :

  • S-5 : Поддержка текстовых сообщений.

Модель Java № 4

Мы можем моделировать S-5 как:

1
2
3
4
interface IssueWiseText {
  String text(); // e.g. "You migth know it."
  List<Issue> issues(); // e.g. ["migth"]
}

Это позволяет нам определить Stream тестовых случаев просто как

  • Stream<IssueWiseText>

вместо

  • Stream<Map.Entry<String, List<Issue>>> .

Это совершить 4 .

Шаг 5: ожидаемое покрытие

Требования № 5

Предположим, что бизнес требует :

  • B-4 : сообщить об ожидаемом освещении проблемы для потока тестовых случаев ;

где освещение вопроса — для простоты — определяется как:

общая длина вопроса
─────────────
общая длина текста

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

Модель Java № 5

Мы можем обработать B-4 с помощью метода на основе Collector :

1
2
3
static double issueCoverage(Stream<? extends IssueWiseText> textStream) {
  return textStream.collect(IssueCoverage.collector());
}

Collector основан на Accumulator имеющем два изменяемых поля:

1
2
int totalIssueLength = 0;
int totalTextLength = 0;

который для каждого IssueWiseText мы увеличиваем:

1
2
totalIssueLength += issueWiseText.issues().stream().mapToInt(Issue::length).sum();
totalTextLength += issueWiseText.text().length();

а затем мы рассчитываем покрытие проблемы как:

1
(double) totalIssueLength / totalTextLength

Это совершить 5 .

Шаг 6: Полученное покрытие

Требования № 6

Предположим, что бизнес требует :

  • B-5 : Сообщите о полученном освещении проблемы для всего набора тестов.

где «полученный» означает «рассчитанный с использованием обнаруженных проблем». Теперь все становится интересным!

Прежде всего, поскольку IssueCoverage представляет бизнес-логику , мы не должны дублировать ее:

  • S-6 : Повторно использовать код покрытия проблемы.

Во-вторых, так как метод принимает Stream<? extends IssueWiseText> Stream<? extends IssueWiseText> , нам нужно смоделировать IssueWiseText для ProbableIssue s:

  • S-7 : Поддержка вероятностных текстов, касающихся вопросов.

Я вижу только два варианта:

  1. Параметризация : IssueWiseText<I extends Issue> ;
  2. Подтип : ProbabilisticIssueWiseText extends IssueWiseText .

Параметрическая модель Java № 6

Параметрическая модель S-7 проста — нам нужно <I extends Issue> ( параметр ограниченного типа ) в IssueWiseText :

1
2
3
4
interface IssueWiseText<I extends Issue> {
  String text();
  List<I> issues();
}

У этой модели есть недостатки (например, стирание типов ), но она лаконична.

Мы также можем адаптировать IssueDetector для возврата IssueWiseText<ProbableIssue> .

Более того, наш Stream тестовых случаев может превратиться в Stream<IssueWiseText<Issue>> (хотя IssueWiseText<Issue> является довольно спорным).

Это совершить 6а .

Подтип Java-модель № 6

Другой вариант — выбрать подтип (который имеет свои недостатки, самым большим из которых может быть дублирование ).

Модель подтипа S-7 использует ковариацию возвращаемого типа :

1
2
3
4
interface ProbabilisticIssueWiseText extends IssueWiseText {
  @Override
  List<? extends ProbableIssue> issues();
}

где issues() в IssueWiseText должны стать верхними IssueWiseText ( List<? extends Issue> ).

Мы также можем адаптировать IssueDetector для возврата ProbabilisticIssueWiseText .

Это совершить 6b .

Шаг 7: Фильтрация по типу проблемы

Требования № 7

Предположим, что бизнес требует :

  • B-6 : Сообщить о проблеме по типу проблемы .

Мы могли бы поддержать это, приняв дополнительный параметр типа Predicate <? super Issue> Predicate <? super Issue> (параметр IssueType , будет слишком узким).

Однако его прямая поддержка в IssueCoverage усложнит бизнес-логику ( commit 7a ‘ ). Вместо этого мы предпочли бы IssueWiseText отфильтрованные экземпляры IssueWiseText в IssueCoverage .

Как мы делаем фильтрацию? Выполнение этого «вручную» (вызов new самостоятельно) приведет к ненужной связи с реализациями (мы даже еще не знаем их). Вот почему мы позволим IssueWiseText выполнять фильтрацию (я чувствую, что эта логика здесь):

  • S-8 : Поддержка фильтрации по IssueWiseText в IssueWiseText .

Другими словами, мы хотим иметь возможность сказать:

Другими словами, мы хотим иметь возможность сказать:

Эй IssueWiseText , отфильтруйте себя по Issue !

Параметрическая модель Java № 7

В параметрической модели мы добавляем следующий filtered метод в IssueWiseText<I>

1
IssueWiseText<I> filtered(Predicate<? super I> issueFilter);

Это позволяет нам встретить B-6 как:

1
2
3
return textStream
        .map(text -> text.filtered(issue -> issue.type() == issueType))
        .collect(IssueCoverage.collector());

Это совершить 7а .

Подтип Java Модель № 7

В модели подтипов мы также добавляем filtered метод (очень похожий на приведенный выше):

1
IssueWiseText filtered(Predicate<? super Issue> issueFilter);

Это позволяет нам встретить B-6 так же, как и выше.

Это совершить 7b .

Шаг 8: Фильтрация по вероятности

Требования № 8

Предположим, что бизнес требует :

  • B-7 : Сообщить о покрытии проблемы с минимальной вероятностью .

Другими словами, бизнес хочет знать, как распределение вероятностей влияет на охват проблемы.

Теперь мы не хотим запускать IssueDetector с большим количеством различных порогов вероятности ( PT ), потому что это будет очень неэффективно. Вместо этого мы запустим его только один раз (с PT = 0 ), а затем продолжим отбрасывать проблемы с наименьшей вероятностью для пересчета охвата проблемы.

Тем не менее, чтобы иметь возможность фильтровать по вероятностям, нам необходимо:

  • S-9 : Поддержка фильтрации по ProbableIssue в вероятностном проблемном тексте.

Параметрическая модель Java № 8

В параметрической модели нам не нужно ничего менять. Мы можем встретить B-7 как:

1
2
3
return textStream
        .map(text -> text.filtered(issue -> issue.probability() >= minProbability))
        .collect(IssueCoverage.collector());

Это совершить 8а .

Подтип Java-модель № 8

В модели подтипов это сложнее, потому что нам нужен дополнительный метод в ProbabilisticIssueWiseText :

1
ProbabilisticIssueWiseText filteredProbabilistic(Predicate<? super ProbableIssue> issueFilter);

что позволяет нам встретить B-7 как:

1
2
3
return textStream
        .map(text -> text.filteredProbabilistic(issue -> issue.probability() >= minProbability))
        .collect(IssueCoverage.collector());

Это совершить 8b .


Для меня этот дополнительный метод в ProbabilisticIssueWiseText довольно тревожный, хотя (см. Здесь ). Вот почему я предлагаю …

Шаг 9: Фильтр

Требования № 9

Поскольку регулярная фильтрация в модели подтипов настолько «неоднородна», давайте сделаем ее равномерной:

  • S-10 : Поддержка равномерной фильтрации в модели подтипов текстового вопроса.

Другими словами, мы хотим иметь возможность сказать:

Привет ProbabilisticIssueWiseText , отфильтруйте себя по ProbableIssue (но так же, как IssueWiseText фильтрует себя по Issue )!

Насколько мне известно, это может быть достигнуто только с помощью шаблона Filterer .

Подтип Java-модель № 9

Поэтому мы применяем универсальный Filterer к IssueWiseText :

1
Filterer<? extends IssueWiseText, ? extends Issue> filtered();

и в ProbablisticIssueWiseText :

1
2
@Override
Filterer<? extends ProbabilisticIssueWiseText, ? extends ProbableIssue> filtered();

Теперь мы можем фильтровать равномерно, вызывая:

1
text.filtered().by(issue -> ...)

Это совершить 9 .

Шаг 10: Время обнаружения

К этому времени вы должны задаться вопросом, почему я беспокоюсь о модели подтипов, если параметрическая модель намного проще.

Итак, в последний раз, давайте предположим, что бизнес требует :

  • B-8 : Время обнаружения отчета (= время, необходимое для обнаружения всех проблем в данном тексте).

Параметрическая модель Java № 10

Я вижу только два способа включения B-8 в параметрическую модель: 1) композиция, 2) подтип.

Композиция для параметрической модели Java № 10

Наносить композицию легко. Мы представляем IssueDetectionResult :

1
2
3
4
interface IssueDetectionResult {
  IssueWiseText<ProbableIssue> probabilisticIssueWiseText();
  Duration detectionTime();
}

и измените IssueDetector чтобы вернуть его.

Это совершить 10а .

Подтип для параметрической модели Java № 10

Применение подтипов требует немного больше работы. Нам нужно добавить ProbabilisticIssueWiseText<I> *

1
2
3
4
interface ProbabilisticIssueWiseText<I extends ProbableIssue> extends IssueWiseText<I> {
  Duration detectionTime();
  // ...
}

и измените IssueDetector чтобы он возвращал ProbabilisticIssueWiseText<?> .

Это совершить 10а ‘ .

* Обратите внимание, что я оставил <I> на ProbabilisticIssueWiseText чтобы не связывать параметризацию с подтипами опасным способом .

Подтип Java Модель № 10

С моделью с чисто подтипами включить B-8 очень легко. Мы просто добавляем detectionTime() в ProbabilisticIssueAwareText :

1
2
3
4
interface ProbabilisticIssueWiseText extends IssueWiseText {
  Duration detectionTime();
  // ...
}

Это совершить 10b .

Выводы

Там нет времени, чтобы вдаваться в детали (пост уже намного дольше, чем я ожидал).

Тем не менее, я предпочитаю чистый Filterer (и, следовательно, Filterer ), чем другие решения, потому что:

  1. Параметризация с композицией оставляет меня без общего супертипа (в некоторых случаях это проблема);
  2. Параметризация с подтипами имеет слишком много степеней свободы.

Под «слишком многими степенями свободы» я имею в виду, что мне нужно только:

  • IssueAwareText<?>
  • ProbabilisticIssueAwareText<?>
  • IssueAwareText<Issue> (спорный)

но в коде я тоже столкнусь (говорю из опыта!):

  • IssueAwareText<? extends Issue> IssueAwareText<? extends Issue> (избыточная верхняя граница)
  • IssueAwareText<ProbableIssue>
  • IssueAwareText<? extends ProbableIssue> IssueAwareText<? extends ProbableIssue> (почему не ProbabilisticIssueAwareText<?> ?)
  • ProbabilisticIssueAwareText<? extends ProbableIssue> ProbabilisticIssueAwareText<? extends ProbableIssue> (избыточная верхняя граница)
  • ProbabilisticIssueAwareText<ProbableIssue>

так что это слишком запутанно для меня. Но если вы действительно интересуетесь этой темой, посмотрите « Сложный подтип против параметризации» (но будьте осторожны — он даже длиннее, чем этот пост!).

Спасибо за чтение!

Опубликовано на Java Code Geeks с разрешения Томаша Линковски, партнера нашей программы JCG. Смотрите оригинальную статью здесь: Узор Filterer за 10 шагов

Мнения, высказанные участниками Java Code Geeks, являются их собственными.