Рубрики
Без рубрики

От сервисов к командам и обработчикам: код, основанный на прецедентах

Недавно мой коллега показал мне, как работает MediatR – .NET-реализация посредника desi… Помеченный как java, посредник, команда, обработчики.

Недавно мой коллега показал мне, как работает MediatR – реализация .NET шаблона проектирования mediator , который, вкратце, “определяет объект, который инкапсулирует, как взаимодействует набор объектов” ¹.

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

Не поймите меня неправильно: я все еще думаю, что наличие уровня презентации, бизнеса и постоянства (или какой-то его вариант) все еще работает в большинстве случаев и должно быть правильным решением 2. Однако у меня был личный проект ((написано на Java с помощью SpringBoot и Spring Data JPA) который вырос в размерах и сложности и хотел попробовать что-то “новое” во время рефакторинга.

Первым делом нужно было найти Java-эквивалент Mediator. В поисках альтернатив я наткнулся на Pipeline . Исходный код был более или менее таким, как я ожидал от предлагаемой функциональности, так что пришло время начать играть с ним.

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

Структура пакета (вдохновлена шаблоном BCE )

├── boundary
│   └── ColorsResource.java         
├── control
│   ├── ColorRepository.java
│   └── ColorService.java
└── entity
    └── Color.java

ColorResource.java :

@RestController
@RequestMapping("/colors")
public class ColorsResource {

  private final ColorService service;

  public ColorsResource(final ColorService service) {
    this.service = service;
  }

  @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity insert(@Valid @RequestBody Color color) {
    var data = service.insert(color);
    var uri = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
        .buildAndExpand(data.getId()).toUri();
    return ResponseEntity.created(uri).body(data);

   // (other endpoints omitted)
  }

ColorResource.java ||:

@Service
public class ColorsService {

  private final ColorRepository repository;

  public ColorsService(final ColorRepository repository) {
    this.repository = repository;
  }

  public Color insert(Color color) {
    try {
      return repository.save(color);
    } catch (DataIntegrityViolationException e) {
      throw new ResponseStatusException(
          HttpStatus.CONFLICT,
          MessageFormat.format("A Color with name [{0}] already exists.", color.getName()));
    }

   // (other methods omitted)
  }

ColorResource.java ||:

@Repository
public interface ColorRepository extends JpaRepository {
}

ColorResource.java ||:

@Entity
@Table(name = "colors")
public class Color {

  private static final long serialVersionUID = 4L;

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;

  @NotBlank(message = "Color.name cannot be blank.")
  @Column(unique = true, nullable = false)
  private String name;

  @Column(name = "created_date", nullable = false, updatable = false)
  private OffsetDateTime createdDate;

  @Column(name = "updated_date")
  private OffsetDateTime updatedDate;

  // (constructor, getters and setters omitted)
}

Конвейер, Команды и обработчики

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


  an.awesome
  pipelinr
  0.5



  
    central
    bintray
    https://jcenter.bintray.com
  

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

@Configuration
public class PipelinrProvider {

  @Bean
  public Pipeline getPipeline(
      ObjectProvider commandHandlers,
      ObjectProvider notificationHandlers,
      ObjectProvider middlewares) {
    return new Pipelinr()
        .with(commandHandlers::stream) // Registers Handlers
        .with(notificationHandlers::stream) // Registers Notifications (not covered here)
        .with(middlewares::orderedStream); // Registers Middlewares (not covered here)
  }
}

Теперь, когда это сделано, мы можем приступить к рефакторингу.

Команды

Грубо говоря, команды – это объекты, которые инкапсулируют детали запроса и фиксируют его намерение. В нашем случае мы выставляем конечную точку POST с намерением создать новый цвет. Итак, давайте создадим две команды: одну для захвата намерения создания цвета и одну с результатом намерения (созданный цвет).

Итак, давайте создадим две команды: одну для захвата намерения создания цвета и одну с результатом намерения (созданный цвет).

public final class ColorResponseCommand {

  private final int id;
  private final String name;
  private final OffsetDateTime createdDate;

  private ColorResponseCommand(final int id, 
       final String name, 
       final OffsetDateTime createdDate) {
    this.id = id;
    this.name = name;
    this.createdDate = createdDate;
  }

  public static ColorResponseCommand from(final Color color) {
    return new ColorResponseCommand(color.getId(), color.getName(), color.getCreatedDate());
  }

  // getters omitted
}

Итак, давайте создадим две команды: одну для захвата намерения создания цвета и одну с результатом намерения (созданный цвет).

public class CreateColorRequestCommand implements Command {

  // Only name is required when creating a Color
  @NotBlank(message = "Name must not be blank.")
  private final String name;

  public CreateColorRequestCommand(final String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }
}

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

Обработчики

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

@Component
public class CreateColorHandler implements
    Command.Handler {

  private final ColorRepository repository;

  public CreateColorHandler(final ColorRepository repository) {
    this.repository = repository;
  }

  @Override
  public ColorResponseCommand handle(final CreateColorRequestCommand command) {
    var color = new Color();
    color.setName(command.getName());
    try {
      var createdColor = repository.save(color);
      return ColorResponseCommand.from(createdColor);
    } catch (DataIntegrityViolationException e) {
      throw new ResponseStatusException(
          HttpStatus.CONFLICT,
          MessageFormat
              .format("A Color with name [{0}] already exists.", command.getName()));
    }
  }
}

Есть несколько вещей, на которые следует обратить внимание:

  1. Наш управляемый класс реализует команду. Обработчик, который определяет, что этот обработчик получит команду Create Color Request и ответит командой ColorResponseCommand .
  2. Реализовав упомянутый выше интерфейс и сделав экземпляр управляемым ( @Component annotation), этот обработчик будет зарегистрирован и доступен для использования экземпляром конвейера (подробнее об этом в следующем разделе).

Трубопровод

Теперь пришло время привести обработчик и команды в действие. Как было замечено ранее, наш ColorsResource.java делегировал создание Цвета введенному ColorService.java . Вместо этого давайте используем экземпляр конвейера, который мы определили ранее. Он работает как посредник между командами и обработчиками, поскольку знает, какому обработчику отправлять каждую команду в приложении.

@RestController
@RequestMapping("/colors")
public class ColorsResource {

  private final Pipeline pipeline;

  public ColorsResource(final Pipeline pipeline) {
    this.pipeline = pipeline;
  }

  @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity insert(@Valid @RequestBody CreateColorRequestCommand command) {
    var data = pipeline.send(command);
    var uri = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
        .buildAndExpand(data.getId()).toUri();
    return ResponseEntity.created(uri).body(data);
  }
}

С помощью этой новой настройки мы теперь можем избавиться от нашего ColorService.java – который в реальном мире имел бы несколько методов – и вместо этого создавал обработчики и команды для каждого варианта использования.

Давайте рассмотрим некоторые плюсы и минусы принятия такого решения.

Плюсы

  • Разъединяет вызывающего и получателя команды, полагаясь на посредника для отправки ее нужному обработчику.
  • Модульное тестирование упрощается за счет наличия лишь минимального количества зависимостей обработчиков и команд.
  • Имена файлов отражают варианты использования приложений, что делает навигацию более понятной (спорно).
  • Каждая команда предоставляет только атрибуты, необходимые для конкретного варианта использования, избегая полей отправки и получения, которые не должны быть применимы/доступны во всех вариантах использования.
  • Уменьшает вероятность конфликтов слияния в больших командах, поскольку варианты использования децентрализованы из одного служебного файла.
  • Упрощает добавление новых функций/вариантов использования в приложение. Поскольку, скорее всего, потребуются новые обработчики и команды, это может не повлиять на существующий код.

Аферы

  • Среда IDE не будет вашим другом при попытке перейти к правильному обработчику с помощью метода pipeline.send() .
  • Каждый новый вариант использования требует создания нового обработчика и по крайней мере одной новой команды, в результате чего в проекте появляется множество классов.
  • В зависимости от реализации объекта посредника/конвейера это может оказать некоторое влияние на производительность.

Вывод

Давайте будем честны: это много кода для чего-то такого простого, как CRUD-операции. Но этот подход эффективен в определенных сценариях, перечисленных в разделе “Плюсы” выше.

В то время как приведенные здесь примеры основывались на конвейере, существуют и другие альтернативы (такие как https://github.com/kmhigashioka/ShortBus ), которые охватывают одни и те же концепции.

Окончательный код можно найти по адресу https://github.com/davibandeira/pipelinr-demo .

Дайте мне знать ваши мысли об этом и других подходах 😉

1 Гамма, Эрих – Шаблоны проектирования: Элементы многоразового объектно-ориентированного программного обеспечения: Аддисон-Уэсли, 1994. ² ЯГНИК и ПОЦЕЛУЙ принципы. 3 https://refactoring.guru/design-patterns/commandhttps://sourcemaking.com/design_patterns/chain_of_responsibility

Оригинал: “https://dev.to/davibandeira/from-services-to-command-and-handlers-use-case-driven-code-2dmc”