Сущности в Domain-Driven Design
Entity — объект, который имеет уникальный идентификатор и жизненный цикл. Две Entity равны, если их идентификаторы равны, даже если другие атрибуты отличаются.
# 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! (тот же адрес)
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"))
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()
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"})
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"))
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
# ❌ Модель без поведения
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
# Проблемы:
# - Модель — просто "мешок с данными"
# - Логика размазана по сервисам
# - Сложно понять, где что находится
# ✅ Модель с поведением
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 — это не про данные. Это про идентичность.
Entity имеет уникальный идентификатор и жизненный цикл, и содержит бизнес-логику работы с сущностью."
| Критерий | Entity | Value Object |
|---|---|---|
| Идентификатор | ✅ Есть | ❌ Нет |
| Равенство | По ID | По значению |
| Изменяемость | ✅ Может изменяться | ❌ Неизменяемый |
| Жизненный цикл | ✅ Есть | ❌ Нет |