Программирование

Гибкое моделирование 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


Проблема и цели

В домене есть четыре комбинации карточек:

  • Физическая vs виртуальная
  • Операция 1 vs Операция 2

Нужно:

  • Гибко расширять модель, добавляя новые операции или типы карточек, на самом деле.
  • Избежать «заполнения» модели многочисленными конкретными подклассами CardTemplate, впрочем.
  • Соблюдать принципы OCP и DIP, характерные для DDD, на самом деле.

Ключевые доменные понятия

Концепт Что это Как использовать в модели
Entity Объект с собственным жизненным циклом и идентификатором. CardTemplate – сущность, владеющая стратегиями.
Value Object Ненаследуемый объект без идентичности, описывающий характеристику. TemplateKind и OperationType как значимые объекты.
Strategy Интерфейс поведения, реализуемый конкретными классами. TemplateRenderer и OperationProcessor.
Factory Создаёт стратегии из доменных атрибутов. CardTemplateFactory использует реестр стратегий.
Domain Service Бизнес‑логика, не принадлежащая напрямую сущности. Обработчики операций, которые не зависят от конкретных карточек.

Композиция стратегий

Вместо «группы подклассов» каждая карточка хранит ссылки на два объекта‑стратегии:

java
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 остаётся конкретным и независимым от деталей реализации, а изменения в поведении вынесены в стратегии.


Фабрика и реестр стратегий

Создание карточек централизовано, на самом деле:

java
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.
При добавлении нового типа операции:

  1. Добавить новый класс, реализующий OperationProcessor.
  2. Зарегистрировать его в StrategyRegistry.
  3. Никаких изменений в сущности CardTemplate не требуется.

Добавление новых типов операций

При добавлении нового типа операции, на самом деле:

  1. Определить новый OperationType value‑object.
  2. Реализовать OperationProcessor для этого типа.
  3. Регистрировать стратегию в StrategyRegistry.
  4. При создании карточки фабрика автоматически подберёт нужный 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)

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 для более сложной бизнес‑логики, но основная архитектура остаётся гибкой.

Следуя этой схеме, вы получите открытый, расширяемый доменный слой, который легко адаптируется к новым требованиям без «наводнения» модели многочисленными подклассами, на самом деле.

Авторы
Проверено модерацией
Модерация