Entities

Сущности в Domain-Driven Design

Что такое Entity?

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

🔹 Entity vs Value Object

# Entity — имеет идентификатор
class User:  # Entity
    def __init__(self, user_id: int, name: str, email: str):
        self.id = user_id  # Идентификатор
        self.name = name
        self.email = email
    
    def __eq__(self, other):
        return isinstance(other, User) and self.id == other.id

user1 = User(1, "Alice", "alice@example.com")
user2 = User(1, "Bob", "bob@example.com")  # Другой name, email
assert user1 == user2  # True! (тот же ID)

# Value Object — равенство по значению
class Email:  # Value Object
    def __init__(self, address: str):
        self.address = address
    
    def __eq__(self, other):
        return isinstance(other, Email) and self.address == other.address

email1 = Email("alice@example.com")
email2 = Email("alice@example.com")
assert email1 == email2  # True! (тот же адрес)

🔹 Характеристики Entity

  • Уникальный идентификатор — ID отличает Entity от других
  • Жизненный цикл — создание, изменение, удаление
  • Равенство по ID — две Entity равны, если ID равны
  • Изменяемость — Entity может изменяться со временем
  • Бизнес-логика — содержит логику работы с сущностью

Примеры Entities

🔹 User Entity

class User:  # Entity
    def __init__(self, user_id: int, name: str, email: Email, created_at: datetime):
        self.id = user_id  # Идентификатор
        self.name = name
        self.email = email  # Value Object
        self.created_at = created_at
        self.is_active = True
    
    def change_name(self, new_name: str):
        if not new_name or len(new_name) < 2:
            raise ValueError("Name must be at least 2 characters")
        self.name = new_name
    
    def change_email(self, new_email: Email):
        # Email валидируется при создании Value Object
        self.email = new_email
    
    def deactivate(self):
        self.is_active = False
    
    def activate(self):
        self.is_active = True
    
    def __eq__(self, other):
        if not isinstance(other, User):
            return False
        return self.id == other.id
    
    def __hash__(self):
        return hash(self.id)
    
    def __str__(self):
        return f"User(id={self.id}, name={self.name}, email={self.email})"

# Использование
user = User(1, "Alice", Email("alice@example.com"), datetime.now())
user.change_name("Bob")  # Entity изменяется
user.change_email(Email("bob@example.com"))

🔹 Order Entity

class Order:  # Entity (Aggregate Root)
    def __init__(self, order_id: int, user_id: int, items: List[OrderItem], created_at: datetime):
        self.id = order_id  # Идентификатор
        self.user_id = user_id
        self.items = items  # Value Objects или Entities
        self.created_at = created_at
        self.status = OrderStatus.PENDING
        self.total = self._calculate_total()
    
    def add_item(self, product_id: int, quantity: int, price: Money):
        if self.status != OrderStatus.PENDING:
            raise ValueError("Cannot modify completed order")
        
        item = OrderItem(product_id, quantity, price)
        self.items.append(item)
        self.total = self._calculate_total()
    
    def remove_item(self, product_id: int):
        if self.status != OrderStatus.PENDING:
            raise ValueError("Cannot modify completed order")
        
        self.items = [item for item in self.items if item.product_id != product_id]
        self.total = self._calculate_total()
    
    def confirm(self):
        if self.status != OrderStatus.PENDING:
            raise ValueError("Order is not pending")
        if not self.items:
            raise ValueError("Order must have at least one item")
        
        self.status = OrderStatus.CONFIRMED
    
    def cancel(self):
        if self.status == OrderStatus.SHIPPED:
            raise ValueError("Cannot cancel shipped order")
        self.status = OrderStatus.CANCELLED
    
    def _calculate_total(self) -> Money:
        total = Money(0.0, "USD")
        for item in self.items:
            total = total + (item.price * item.quantity)
        return total
    
    def __eq__(self, other):
        if not isinstance(other, Order):
            return False
        return self.id == other.id

# Использование
order = Order(1, user_id=1, items=[], created_at=datetime.now())
order.add_item(product_id=1, quantity=2, price=Money(10.0, "USD"))
order.confirm()

Жизненный цикл Entity

🔹 Создание Entity

class User:
    def __init__(self, user_id: int, name: str, email: Email):
        self.id = user_id
        self.name = name
        self.email = email
        self.created_at = datetime.now()
        self.is_active = True
    
    @classmethod
    def create(cls, name: str, email: Email) -> 'User':
        # Factory method для создания
        user_id = generate_id()
        return cls(user_id, name, email)
    
    @classmethod
    def from_db(cls, row: dict) -> 'User':
        # Factory method для восстановления из БД
        return cls(
            user_id=row['id'],
            name=row['name'],
            email=Email(row['email'])
        )

# Использование
user = User.create("Alice", Email("alice@example.com"))
# или
user = User.from_db({"id": 1, "name": "Alice", "email": "alice@example.com"})

🔹 Изменение Entity

class User:
    def change_name(self, new_name: str):
        # Валидация
        if not new_name or len(new_name) < 2:
            raise ValueError("Name must be at least 2 characters")
        
        # Изменение
        old_name = self.name
        self.name = new_name
        
        # Событие (опционально)
        self._domain_events.append(UserNameChangedEvent(self.id, old_name, new_name))
    
    def change_email(self, new_email: Email):
        # Валидация
        if self.email == new_email:
            return  # Нет изменений
        
        # Изменение
        old_email = self.email
        self.email = new_email
        
        # Событие
        self._domain_events.append(UserEmailChangedEvent(self.id, old_email, new_email))

# Использование
user = User(1, "Alice", Email("alice@example.com"))
user.change_name("Bob")
user.change_email(Email("bob@example.com"))

🔹 Удаление Entity

class User:
    def delete(self):
        # Soft delete
        self.is_active = False
        self.deleted_at = datetime.now()
        
        # Событие
        self._domain_events.append(UserDeletedEvent(self.id))
    
    def hard_delete(self):
        # Hard delete (обычно через Repository)
        # Entity удаляется из БД
        pass

# Использование
user = User(1, "Alice", Email("alice@example.com"))
user.delete()  # Soft delete
# или
user_repository.delete(user)  # Hard delete через Repository

Rich Domain Model

❌ Anemic Domain Model

# ❌ Модель без поведения
class User:
    def __init__(self):
        self.id = None
        self.name = None
        self.email = None

# Вся логика в сервисах
class UserService:
    def validate_user(self, user: User):
        return "@" in user.email
    
    def change_email(self, user: User, new_email: str):
        user.email = new_email
    
    def is_premium(self, user: User):
        return user.balance > 1000

# Проблемы:
# - Модель — просто "мешок с данными"
# - Логика размазана по сервисам
# - Сложно понять, где что находится

✅ Rich Domain Model

# ✅ Модель с поведением
class User:
    def __init__(self, user_id: int, name: str, email: Email, balance: Money):
        self.id = user_id
        self.name = name
        self.email = email
        self.balance = balance
    
    def change_email(self, new_email: Email):
        # Логика в модели
        if self.email == new_email:
            return
        self.email = new_email
        self._domain_events.append(UserEmailChangedEvent(self.id, new_email))
    
    def is_premium(self) -> bool:
        # Логика в модели
        return self.balance.amount > 1000.0
    
    def upgrade_to_premium(self):
        # Логика в модели
        if self.is_premium():
            raise ValueError("User is already premium")
        # Логика апгрейда
        self._premium_features_enabled = True
    
    def validate(self) -> bool:
        # Валидация в модели
        return self.email.is_valid() and len(self.name) > 0

# Преимущества:
# - Логика рядом с данными
# - Легко понять, где что находится
# - Инкапсуляция

Типовые ошибки и подводные камни

  • Смешение Entity и Value Object (у VO внезапно появляется ID или изменяемость).
  • Бог-объекты: Entity берёт на себя интеграции/инфраструктуру (e-mail, БД, HTTP).
  • Отсутствие инвариантов: методы не защищают целостность (пример: изменение заказа после подтверждения).
  • Лишние сеттеры: открывают состояние наружу, ломая инкапсуляцию.
  • Потеря идентичности в памяти: создание новых экземпляров вместо повторного использования (см. Identity Map).

🎯 Ключевые принципы Entities

  1. Уникальный идентификатор — ID отличает Entity от других
  2. Жизненный цикл — создание, изменение, удаление
  3. Равенство по ID — две Entity равны, если ID равны
  4. Бизнес-логика — содержит логику работы с сущностью
  5. Инкапсуляция — защищает инварианты и бизнес-правила
"Entity — это не про данные. Это про идентичность.
Entity имеет уникальный идентификатор и жизненный цикл, и содержит бизнес-логику работы с сущностью."

📚 Entity vs Value Object

Критерий Entity Value Object
Идентификатор ✅ Есть ❌ Нет
Равенство По ID По значению
Изменяемость ✅ Может изменяться ❌ Неизменяемый
Жизненный цикл ✅ Есть ❌ Нет