Active Record добавляем поведение в объекты

Active Record добавляем поведение в объекты

В прошлом посте мы разбирали Transactional Script - отличный инструмент для старта. Но любой проект развивается. Со временем вы начинаете замечать, что Transactional Script становится сильно усложненным. В них добавляются простые бизнес-инварианты, дополнительные проверки и сами структуры данных становятся довольно сложными.

Скрипт перестает быть читабельным, занимает несколько экранов, а логика начинает дублироваться. Кажется, что самое время достать "тяжелую артиллерию" из Domain Driven Design - паттерн Domain Model.

Но подождите начинать тратить ресурсы и время на проектирование архитектуры с Аггрегатами. Если ваша система относится не к главному бизнес-домену (Core Domain), а к вспомогательному (Supporting Domain) или общему (Generic Domain), а бизнес-процесс все еще не такой сложный, то идеальным решением становится паттерн Active Record.

Что из себя представляет Active Record

Паттерн был описан Мартином Фаулером в его книге Patterns of Enterprise Application Architecture (2002) и популяризирован такими фреймворками, как Ruby on Rails.

Его суть проста: объект содержит и данные (строку в базе данных), и поведение, и логику для сохранения самого себя в базу.

В отличие от анемичных моделей в Transactional Script, объекты Active Record обладают не только данными. Они знают как себя проверять, как изменять свое состояние, как сохранять и восстанавливать свое состояние.

Классическая реализация Active Record, прошлый пример в новом варианте:

@Entity @Table(name = "orders") public class Order { @Id private UUID id; private UUID customerId; private OrderStatus status; private List<OrderItem> items = new ArrayList<>(); private BigDecimal total = BigDecimal.ZERO; public void addItems(List<OrderItem> newItems) { this.items.addAll(newItems); recalculateTotal(); } public void applyPromoCode(PromoCode promo) { if (promo != null && promo.isValidFor(this.total)) { BigDecimal discount = promo.getDiscountAmount(); this.total = this.total.subtract(discount); } } private void recalculateTotal() { this.total = items.stream() .map(OrderItem::getSubtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); } @PersistenceContext private static EntityManager entityManager; public void save() { if (id == null) { entityManager.persist(this); } else { entityManager.merge(this); } } public static Order findById(UUID id) { return entityManager.find(Order.class, id); } } @Service @Transactional public class OrderService { private final PromoCodeRepository promoRepository; private final ApplicationEventPublisher eventPublisher; public OrderDto createOrder(CreateOrderRequest request) { validateRequest(request); Order order = new Order(); order.setCustomerId(request.getCustomerId()); order.setStatus(OrderStatus.CREATED); // Бизнес-логика делегирована сущности order.addItems(request.getItems()); // Применение промокода if (request.hasPromoCode()) { PromoCode promo = promoRepository.findByCode(request.getPromoCode()); order.applyPromoCode(promo); } // Сохранение через сам объект (Active Record) order.save(); eventPublisher.publish(new OrderCreatedEvent(order.getId())); return toDto(order); } }

Active Record как промежуточный шаг к Domain Model

Active Record — отличный промежуточный этап эволюции:

  1. Начинаете с Transactional Script.
  2. Добавляете поведение в сущности → Active Record.
  3. Когда правил становится слишком много, инварианты критичны и появляются value objects - начинаете выносить логику из сущности в агрегаты -> Domain Model.

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

Подпишись на мой телеграм-канал

Начать дискуссию