Modelo Rico vs. Modelo Anémico
Tell, don't ask: el objeto sabe lo que puede hacer con sus propios datos.
El modelo anémico (Anemic Domain Model) es uno de los anti-patrones más comunes en desarrollo orientado a objetos. Fue identificado y nombrado por Martin Fowler: consiste en clases que solo almacenan datos —con getters y setters— mientras que toda la lógica de negocio vive dispersa en clases de servicio externas. El resultado parece orientado a objetos porque hay clases, pero en realidad es programación procedural disfrazada.
El modelo rico, en contraste, es aquel donde los objetos encapsulan tanto el estado como el comportamiento que opera sobre ese estado. Las reglas de negocio, las validaciones y las invariantes viven dentro del objeto que tiene los datos relevantes. Esto no es una preferencia estilística: es la consecuencia directa del principio de encapsulación y del principio Tell, Don't Ask.
El principio Tell, Don't Ask dice que en lugar de preguntar el estado de un objeto para decidir qué hacer fuera de él, debemos decirle al objeto qué hacer y dejar que él aplique sus propias reglas. Un código que hace `if order.get_status() == 'pending': order.set_status('approved')` está preguntando y decidiendo desde fuera, lo que viola la encapsulación. Un código que llama `order.approve()` le dice al objeto qué hacer y deja que él valide si puede.
El modelo anémico tiene consecuencias concretas en la mantenibilidad. Las reglas de negocio quedan dispersas en múltiples clases de servicio; cuando una regla cambia, es difícil encontrar todos los lugares donde está implementada. Los invariantes no se garantizan: el objeto puede quedar en estados inconsistentes porque cualquier código externo puede modificar sus atributos directamente. Los tests de unidad no pueden enfocarse en el objeto porque este no tiene comportamiento propio.
En Python, la transición de modelo anémico a modelo rico implica mover la lógica de las clases de servicio hacia los métodos de las clases del dominio. Los setters desaparecen o se convierten en métodos con nombre de negocio. Las validaciones se integran en el constructor o en `__post_init__` para Value Objects. El estado interno se hace privado por convención (`_attribute`) y solo se modifica a través de métodos que garantizan las invariantes.
Un modelo rico no significa que todo el código de aplicación se mueva al dominio. Los Application Services siguen orquestando: cargan aggregates, invocan métodos de dominio, persisten cambios. La diferencia es que el Application Service no contiene lógica de negocio; solo llama al objeto correcto con los parámetros correctos y el objeto sabe qué hacer.
# ANEMIC MODEL (anti-pattern) — DO NOT DO THIS
# domain/model/<aggregate>.py
@dataclass
class <AggregateAnemic>:
id: <AggregateId>
status: str # public — anyone can set it
amount: float # no validation, no encapsulation
# application/services/<aggregate>_service.py
# Business logic leaks into the service — fragile, duplicated
class <AggregateService>:
def approve(self, agg: <AggregateAnemic>) -> None:
if agg.status != 'pending': # asking state from outside
raise ValueError('...')
agg.status = 'approved' # mutating from outside
# RICH MODEL (correct) — DO THIS
# domain/model/<aggregate>.py
@dataclass
class <Aggregate>:
id: <AggregateId>
_status: <Status> # private — only changed via domain methods
_amount: <Amount> # Value Object: self-validates on creation
_events: list = field(default_factory=list, repr=False)
@classmethod
def create(cls, id: <AggregateId>, amount: <Amount>) -> '<Aggregate>':
# Factory method: ensures valid initial state
return cls(id=id, _status=<Status>.PENDING, _amount=amount)
def approve(self) -> None:
# Tell, don't ask: object checks its own state
if self._status != <Status>.PENDING:
raise <InvalidTransitionError>(self._status, 'approve')
self._status = <Status>.APPROVED
self._events.append(<Approved>(aggregate_id=self.id))
def pull_events(self) -> list:
events, self._events = self._events, []
return events
# application/commands/<approve>/handler.py
# Orchestrates: no business logic here
class <ApproveHandler>:
def handle(self, cmd: <ApproveCommand>) -> None:
agg = self._repo.get(cmd.aggregate_id)
agg.approve() # tell the object what to do
self._repo.save(agg)
self._bus.publish(agg.pull_events())Debugging lab
Detecta y corrige el error o la violación de diseño.
- 1.4.5.1
class <AggregateService>: def process(self, agg: <Aggregate>) -> None: if agg.get_status() == 'pending' and agg.get_amount() > 0: agg.set_status('processing') agg.set_processed_at(datetime.utcnow())
- 1.4.5.2
# Value Object with a setter @dataclass class <Amount>: value: float def set_value(self, v: float) -> None: self.value = v
- 1.4.5.3
class <Aggregate>: status: str = 'draft' amount: float = 0.0 # Application service checks and sets freely: if agg.amount > MIN_AMOUNT and agg.status == 'draft': agg.status = 'submitted' agg.submitted_at = datetime.utcnow()
- 1.4.5.4
class <AggregateValidationService>: def is_valid_for_approval(self, agg: <Aggregate>) -> bool: return (agg.get_status() == 'pending' and agg.get_amount() >= agg.get_minimum_amount() and len(agg.get_items()) > 0)
- 1.4.5.5
# All business logic in a 'domain service', aggregate has no methods class <DomainService>: def calculate_<result>(self, agg: <Aggregate>) -> <Result>: base = agg.amount modifier = agg.get_modifier() correction = agg.get_correction() return <Result>(base * modifier - correction)