Гибкое моделирование CardTemplate в DDD: стратегии и фабрика
В DDD модель CardTemplate остаётся гибкой: используйте стратегии для рендеринга и обработки операций, а фабрику и реестр – для централизованного создания. Добавление новых типов без подклассов.
So, I’m trying to model four card‑template types—physical or virtual, each paired with operation type 1 or 2—in a Domain‑Driven Design (DDD) domain model. How can I keep the design flexible for future extensions, like adding new operation types, without ending up with a bunch of concrete subclasses?
Card‑template modeling in a DDD domain can stay open to change by separating the “what” from the “how”: keep a single, thin CardTemplate entity that holds two strategy objects—one that knows how to render the template (physical / virtual) and another that knows how to process an operation (type 1 / 2). When a new operation type arrives, you simply add a new strategy class and update the factory; no new concrete CardTemplate subclasses are needed.
Contents
- Проблема и цели
- Ключевые доменные понятия
- Композиция стратегий
- Фабрика и реестр стратегий
- Добавление новых типов операций
- Альтернативные подходы
- Код‑пример (Java)
- Итоги
Проблема и цели
В домене есть четыре комбинации карточек:
- Физическая vs виртуальная
- Операция 1 vs Операция 2
Нужно:
- Гибко расширять модель, добавляя новые операции или типы карточек, на самом деле.
- Избежать «заполнения» модели многочисленными конкретными подклассами
CardTemplate, впрочем. - Соблюдать принципы OCP и DIP, характерные для DDD, на самом деле.
Ключевые доменные понятия
| Концепт | Что это | Как использовать в модели |
|---|---|---|
| Entity | Объект с собственным жизненным циклом и идентификатором. | CardTemplate – сущность, владеющая стратегиями. |
| Value Object | Ненаследуемый объект без идентичности, описывающий характеристику. | TemplateKind и OperationType как значимые объекты. |
| Strategy | Интерфейс поведения, реализуемый конкретными классами. | TemplateRenderer и OperationProcessor. |
| Factory | Создаёт стратегии из доменных атрибутов. | CardTemplateFactory использует реестр стратегий. |
| Domain Service | Бизнес‑логика, не принадлежащая напрямую сущности. | Обработчики операций, которые не зависят от конкретных карточек. |
Композиция стратегий
Вместо «группы подклассов» каждая карточка хранит ссылки на два объекта‑стратегии:
public class CardTemplate {
private final UUID id;
private final TemplateKind kind; // Physical | Virtual
private final OperationType operation; // 1 | 2
private final TemplateRenderer renderer; // конкретная стратегия
private final OperationProcessor processor; // конкретная стратегия
}
- TemplateKind – value‑object, фиксирует тип карточки.
- OperationType – value‑object, фиксирует тип операции.
- TemplateRenderer и OperationProcessor – интерфейсы, реализующие конкретные алгоритмы.
Таким образом, CardTemplate остаётся конкретным и независимым от деталей реализации, а изменения в поведении вынесены в стратегии.
Фабрика и реестр стратегий
Создание карточек централизовано, на самом деле:
public class CardTemplateFactory {
private final StrategyRegistry registry;
public CardTemplateFactory(StrategyRegistry registry) {
this.registry = registry;
}
public CardTemplate create(UUID id, TemplateKind kind, OperationType operation) {
TemplateRenderer renderer = registry.getRenderer(kind);
OperationProcessor processor = registry.getProcessor(operation);
return new CardTemplate(id, kind, operation, renderer, processor);
}
}
StrategyRegistry хранит мапы TemplateKind → TemplateRenderer и OperationType → OperationProcessor.
При добавлении нового типа операции:
- Добавить новый класс, реализующий
OperationProcessor. - Зарегистрировать его в
StrategyRegistry. - Никаких изменений в сущности
CardTemplateне требуется.
Добавление новых типов операций
При добавлении нового типа операции, на самом деле:
- Определить новый
OperationTypevalue‑object. - Реализовать
OperationProcessorдля этого типа. - Регистрировать стратегию в
StrategyRegistry. - При создании карточки фабрика автоматически подберёт нужный processor.
Why this works
Because the entity depends only on abstractions (OperationProcessor), the Open/Closed Principle is respected: you close the class but open it for extension via new strategy implementations.
See Martin Fowler’s discussion on the Strategy pattern in the context of DDD – Martin Fowler – Strategy Pattern.
Альтернативные подходы
| Подход | Когда использовать | Ограничения |
|---|---|---|
| Enum‑based behavior | Маленькое число вариантов, редко меняется | Добавление новых вариантов требует изменения кода |
| Polymorphic entity hierarchy | Никаких новых типов, но нужна явная иерархия | Появляется «классический» «шпионский» паттерн |
| Domain Service + Value Objects | Когда поведение не связано напрямую с сущностью | Трудно обобщить, если операции сильно различаются |
| Composite pattern | Нужна вложенность стратегий | Сложнее реализовать и поддерживать |
Код‑пример (Java)
// Value Objects
public sealed interface TemplateKind permits Physical, Virtual {
String description();
}
public record Physical() implements TemplateKind { public String description() => "Physical"; }
public record Virtual() implements TemplateKind { public String description() => "Virtual"; }
public sealed interface OperationType permits Type1, Type2, Type3 {
String code();
}
public record Type1() implements OperationType { public String code() => "1"; }
public record Type2() implements OperationType { public String code() => "2"; }
public record Type3() implements OperationType { public String code() => "3"; }
// Strategy interfaces
public interface TemplateRenderer {
void render(CardTemplate template);
}
public interface OperationProcessor {
void process(CardTemplate template);
}
// Concrete strategies
public class PhysicalRenderer implements TemplateRenderer {
public void render(CardTemplate t) { /*…*/ }
}
public class VirtualRenderer implements TemplateRenderer {
public void render(CardTemplate t) { /*…*/ }
}
public class Type1Processor implements OperationProcessor {
public void process(CardTemplate t) { /*…*/ }
}
public class Type2Processor implements OperationProcessor {
public void process(CardTemplate t) { /*…*/ }
}
// New operation type
public class Type3Processor implements OperationProcessor {
public void process(CardTemplate t) { /*…*/ }
}
// Registry
public class StrategyRegistry {
private final Map<TemplateKind, TemplateRenderer> renderers = new HashMap<>();
private final Map<OperationType, OperationProcessor> processors = new HashMap<>();
public void registerRenderer(TemplateKind kind, TemplateRenderer renderer) {
renderers.put(kind, renderer);
}
public void registerProcessor(OperationType op, OperationProcessor processor) {
processors.put(op, processor);
}
public TemplateRenderer getRenderer(TemplateKind kind) {
return renderers.get(kind);
}
public OperationProcessor getProcessor(OperationType op) {
return processors.get(op);
}
}
// Factory
public class CardTemplateFactory {
private final StrategyRegistry registry;
public CardTemplateFactory(StrategyRegistry registry) { this.registry = registry; }
public CardTemplate create(UUID id, TemplateKind kind, OperationType op) {
return new CardTemplate(
id, kind, op,
registry.getRenderer(kind),
registry.getProcessor(op)
);
}
}
Note: All strategy classes are extremely small and isolated; adding a new operation type merely introduces a new processor and a mapping entry.
For more on strategy composition, see Design Patterns: Elements of Reusable Object‑Oriented Software by Gamma et al. – Strategy Pattern.
Заключение
- Сущность
CardTemplateостаётся простой и неизменной, управляя только ссылками на стратегии. - Стратегии разделяют поведение, позволяя добавлять новые типы операций без изменений в модели.
- Фабрика + реестр централизуют создание и поддержку стратегий, обеспечивая открытость модели.
- При необходимости можно комбинировать value objects и domain services для более сложной бизнес‑логики, но основная архитектура остаётся гибкой.
Следуя этой схеме, вы получите открытый, расширяемый доменный слой, который легко адаптируется к новым требованиям без «наводнения» модели многочисленными подклассами, на самом деле.