tactical-model · temario
3.2tema 2 de 5

Entities

Objetos con identidad única y estable que persiste a lo largo del tiempo, independientemente de sus atributos.

Una Entity es un objeto cuya igualdad se define por su identidad, no por sus atributos. Dos instancias con el mismo id representan el mismo concepto de negocio aunque sus demás atributos difieran. Un cliente, un pedido o una cuenta bancaria son entidades: su identidad persiste aunque su nombre, estado o saldo cambie.

La identidad de una Entity debe ser estable, única dentro de su contexto y asignada en el momento de la creación. En DDD, la identidad habitualmente se modela como un Value Object (por ejemplo, una clase OrderId que envuelve un UUID), lo que la hace tipada y auto-validada.

A diferencia de los Value Objects, las Entities tienen ciclo de vida: nacen (se crean), cambian de estado a lo largo del tiempo y pueden ser eliminadas lógicamente. La mutabilidad está permitida, pero debe ser controlada: los cambios se realizan a través de métodos con nombre de dominio (fulfill_order, suspend_account), nunca a través de setters genéricos.

La comparación entre dos Entities se hace exclusivamente por id. Dos objetos con ids distintos son entidades distintas aunque todos sus demás atributos sean idénticos. Esta es la diferencia fundamental con los Value Objects, donde dos instancias con los mismos valores son intercambiables.

Las Entities son el lugar donde vive el comportamiento de negocio con efectos de estado. Cuando una Entity realiza una acción de dominio (activar, cancelar, transferir), puede cambiar sus propios atributos, emitir Domain Events y asegurar sus invariantes. El resultado final es un estado interno coherente con las reglas de negocio.

Una Entity no debería vivir en el dominio de forma aislada: siempre pertenece a un Aggregate. Fuera de su Aggregate Root, las otras entidades del clúster no son accesibles directamente desde el exterior. Esto garantiza que todas las invariantes del clúster se validan en un único punto de entrada.

structure.txt
# --- shared_kernel/domain/entity.py ---
# Base Entity: igualdad por id, ciclo de vida.
# NO añadas lógica de negocio específica aquí.

from dataclasses import dataclass, field
from typing import Any


class Entity:
    # Subclases definen su propio tipo de id (preferiblemente un VO)
    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, self.__class__):
            return False
        return self.id == other.id  # type: ignore[attr-defined]

    def __hash__(self) -> int:
        return hash(self.id)  # type: ignore[attr-defined]


# --- contexts/<bc>/domain/model/<entity>.py ---
# Entity interna de un Aggregate (no es Aggregate Root).
# Solo se accede desde dentro del Aggregate Root.

from dataclasses import dataclass, field
from uuid import UUID, uuid4
from <project>.shared_kernel.domain.entity import Entity
from <project>.contexts.<bc>.domain.model.<value_object> import <ValueObject>


@dataclass
class <Entity>(Entity):
    id: UUID = field(default_factory=uuid4)  # identidad inmutable
    <attribute>: <ValueObject> = ...          # estado mutable controlado

    # Comportamiento de dominio con nombre del lenguaje ubicuo
    def <domain_action>(self, <param>: <ValueObject>) -> None:
        # Validar invariante antes de cambiar estado
        if not self._can_<domain_action>():
            raise <DomainError>(f"Cannot <domain_action>: invariant violated")
        self.<attribute> = <param>

    def _can_<domain_action>(self) -> bool:
        # Lógica privada de pre-condición
        return True


# WRONG — comparación por atributos: NO para una Entity
# def __eq__(self, other):
#     return self.name == other.name  # ← ignora la identidad
project/
<project>
domain
domain
model

Debugging lab

Detecta y corrige el error o la violación de diseño.

0/5 tests passing0%
  1. 3.2.5.1

    @dataclass class <Entity>(Entity): id: UUID name: str status: str def __eq__(self, other: Any) -> bool: return self.name == other.name # comparar por nombre

  2. 3.2.5.2

    @dataclass class <Entity>(Entity): id: UUID = None # se asigna al guardar en BD name: str = ''

  3. 3.2.5.3

    # En el repositorio: def get_line_items(order_id: UUID) -> list[LineItem]: # devuelve las LineItems directamente return self._session.query(LineItemORM).filter_by(order_id=order_id).all()

  4. 3.2.5.4

    class <Entity>(Entity): ... def set_status(self, status: str) -> None: self.status = status # setter genérico sin validación

  5. 3.2.5.5

    # En el application service: def transfer_item(entity_id: UUID, new_owner: str) -> None: entity = repo.get_by_id(entity_id) # validar antes de llamar al dominio if not new_owner: raise ValueError('owner required') if entity.status != 'active': raise ValueError('entity must be active') entity.owner = new_owner # mutación directa