Анемічна модель - об'єкти домену містять лише дані (поля, геттери, сеттери), а вся поведінка живе в окремих сервісах. Модель - це просто структура для бази.
// анемічна модель
class Order extends Model {}
class OrderService
{
public function cancel(Order $order): void
{
if ($order->status === 'shipped') {
throw new DomainException('Відправлене замовлення не скасувати');
}
$order->status = 'cancelled';
$order->cancelled_at = now();
$order->save();
}
}
Мартін Фаулер назвав це антипатерном: об'єкти виглядають як об'єктна модель, але насправді це процедурний код - дані окремо, функції окремо.
Проблеми:
- правила розпорошені: перевірку «чи можна скасувати» хтось повторить в іншому сервісі, контролері, команді - і забуде одну з умов;
- будь-хто може зламати інваріанти:
$order->status = 'cancelled'можна написати будь-де, оминувши правила; - сервіси розростаються до «божественних» класів на тисячі рядків.
Багата модель - поведінка поруч з даними:
class Order extends Model
{
public function cancel(string $reason): void
{
if ($this->status === OrderStatus::Shipped) {
throw new OrderAlreadyShipped($this);
}
$this->status = OrderStatus::Cancelled;
$this->cancelled_at = now();
$this->cancellation_reason = $reason;
}
}
Правило скасування - в одному місці, і викликати його можна лише через метод з назвою з єдиної мови.
Чи завжди анемічна модель погана? Ні:
- CRUD-застосунки без складної логіки - анемічна модель простіша й чесніша;
- «тонкі» моделі + actions (окремий клас на дію:
CancelOrder) - популярний у Laravel підхід. Він не багата модель у класичному сенсі, але логіка кожної операції зібрана в одному місці, і це вже краще за розмазаний код.
Критерій: якщо одні й ті самі правила повторюються в кількох місцях або бізнес-інваріанти порушуються «випадково» - логіці час переїхати ближче до даних.
Докладніше в документації: Martin Fowler: Anemic Domain Model