В сегодняшней статье я расскажу о шаблоне проектирования «Правила» и о том, как реализовать простой механизм правил для классификации небесных объектов.

Прежде чем мы начнем, вы можете найти весь код здесь.

Фон

Представьте себе случай, когда вам нужно реализовать калькулятор скидок, чтобы предоставить надлежащую скидку покупателю на основе различных критериев, таких как дата рождения, дата последней покупки, его первая покупка, ветеран и т. д. Самый простой и простой способ для реализации решения используются операторы if/else, и это хорошо работает, если у вас есть небольшое количество простых условий, которые вы должны проверить. Проблема возникает, когда необходимо проверить новые сложные условия, и вы начинаете создавать новые операторы if/else или модифицировать существующие, что медленно увеличивает цикломатическую сложность вашего кода до такой степени, что его трудно отслеживать и поддерживать. Кроме того, со временем будет все труднее и труднее поддерживать новые правила, и их будет слишком сложно понять паре свежих глаз.

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

Шаблон правил вам на помощь!

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

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

  • shouldRun — запускать или не запускать правило.
  • evaluate —бизнес-логика правила, которое выполняется, только если метод shouldRun возвращает значение true.

Каждое правило должно соответствовать этому интерфейсу и обеспечивать реализацию методов. Просто как тот!

Среди преимуществ, которые дает этот шаблон, мы имеем:

  • уменьшенная цикломатическая сложность
  • добавление, обновление или удаление правил стало проще
  • разделение интересов
  • мы можем повторно использовать правила в другом месте, если захотим
  • модульное тестирование стало проще, поскольку у нас есть отдельные и более мелкие фрагменты кода, которые можно тестировать по отдельности.
  • динамическая композиция механизмов правил

Примечание: имена методов в интерфейсе могут быть изменены, и вы, вероятно, увидите некоторые вариации, но это одна и та же концепция.

Проблема

Давайте предположим, что мы получили работу в НАСА, и наше первое задание — создать механизм, который может классифицировать вновь открытые небесные объекты на основе некоторых входных данных и правил.

Этот движок должен быть представлен с использованием конечной точки REST API, которая принимает следующие входные данные:

где:

  • название —название вновь открытого небесного объекта; например Кеплер
  • масса —масса объекта, выраженная в кг; например 3.65e29
  • equatorialDiameter —диаметр, выраженный в метрах; например 9.94e8, 18450
  • surfaceTemperature — температура поверхности, выраженная в градусах Кельвина; например 5800

Двигатель должен ответить следующим выводом:

где типпредставляет тип небесного объекта, вычисленный движком, и может принимать следующие значения: ПЛАНЕТА, ЗВЕЗДА, ЧЕРНАЯ ДЫРАили НЕИЗВЕСТНО.

Набор правил, которым мы должны следовать:

Правила Планета и Звезда являются взаимоисключающими, тогда как (Планетаи Черная дыра)или (Звезда и Черная дыра) могут выполняться одновременно. В таком случае побеждает правило с более низким приоритетом. Например, если наши входные данные вызывают правила Планета и Черная дыра, тип объекта будет задан правилом Черная дыра. потому что имеет более низкий приоритет (0 вместо 1 по правилу планеты). Если ни одно из правил не выполняется, небесный объект классифицируется как НЕИЗВЕСТНЫЙ.

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

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

Решение

Решение, как вы могли уже догадаться, подразумевает шаблон проектирования «Правила».

Шаг 1. Определение DTO

Первое, что нам нужно сделать, это определить DTO для входных и выходных данных.

Введите DTO

Вывод DTO

Примечание. Я использую Lombok для создания конструкторов, геттеров/сеттеров, компоновщиков, методов toString, equals и hashCode, и именно поэтому вы видите эти аннотации на уровне класса.

Шаг 2. Определение правил

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

Сначала создадим интерфейс:

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

  • shouldRun — принимает входные данные также для того, чтобы решить, запускаем ли мы правило или нет.
  • вычислить — возвращает результат, содержащий тип и приоритет небесного объекта:

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

Планетное правило

Правило звездочки

Правило черной дыры

Метод shouldRun из каждого правила представляет реализацию определенного условия из нашей таблицы правил, а метод evaluate возвращает результат.

Возьмем, к примеру, правило планеты. Если входная масса превышает массу Юпитера не более чем в 13 раз, метод shouldRun возвращает значение true, а метод вычислить будет выполнен с возвратом EvaluationResult(type=PLANET, priority=1). То же самое относится и к остальным.

Шаг 3. Определение движка

После того, как мы определили и внедрили правила, пришло время собрать все вместе и определить наш полный механизм правил, который будет получать входные данные, выполнять правила и возвращать выходные данные. Реализация выглядит так:

Как видите, мы инициализируем наш движок набором правил, а в методе classify мы обязательно работаем с приоритетом в случае, если входные данные активируют несколько правил. Конечный результат нашего метода представляет DTO, который мы определили на шаге 1, и содержит тип небесного объекта.

Теперь давайте проведем небольшое тестирование!

Тест

Сценарий 1 — входные данные для планеты

Сценарий 2 — входные данные для звездочки

Сценарий 3 — ввод данных, который запускает 2 правила

В этом сценарии я использую входные данные, которые запускают правила «Планета» и «Черная дыра», но выходные данные дает тот, у которого самый низкий приоритет, «Черная дыра».

Из логов мы можем увидеть сработавшие правила:

Больше тестовых данных здесь.

Вот и все!

Заключение

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

Берегите себя и помните, что вы можете найти код здесь.