Какие плюсы и минусы у паттернов Active Record и Data Mapper (на примере Eloquent и Doctrine)?

«Какие плюсы и минусы у паттернов Active Record и Data Mapper (на примере Eloquent и Doctrine)?» — вопрос из категории Паттерны, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Active Record (например, Laravel Eloquent):

  • Плюсы:
    • Простота и скорость разработки: Модель наследует базовый класс, который предоставляет все методы для CRUD (find, save, delete). Это позволяет очень быстро создавать функциональность.
    • Прямой и понятный синтаксис: Работа с объектом интуитивно отражает работу с записью в БД.
    • Быстрая настройка: Миграции, фабрики, сидеры тесно интегрированы в экосистему фреймворка.
  • Минусы:
    • Нарушение SRP (Single Responsibility Principle): Модель отвечает и за бизнес-логику, и за персистентность, что приводит к её «разбуханию».
    • Сложность тестирования: Модель тесно связана с базой данных, что требует использования тестовой БД или моков для юнит-тестирования бизнес-логики.
    • Сложность в сложных доменных моделях: При наличии сложных инвариантов, агрегатов и Value Objects архитектура Active Record становится громоздкой.

Data Mapper (например, Doctrine ORM):

  • Плюсы:
    • Чистая доменная модель: Сущность (Entity) — это обычный PHP-объект (POPO), не знающий ничего о базе данных. Это позволяет сосредоточиться на бизнес-логике.
    • Гибкость и мощь: Позволяет эффективно работать со сложными связями (наследование, коллекции), агрегатами и кастомными типами данных.
    • Лёгкость тестирования: Доменные объекты можно тестировать в полной изоляции, без зависимости от слоя персистентности.
    • Разделение ответственности: EntityManager отвечает за сохранение и загрузку, репозитории — за запросы.
  • Минусы:
    • Более высокая сложность: Требует понимания дополнительных абстракций (Unit of Work, Identity Map).
    • Больше шаблонного кода: Необходимо явно управлять жизненным циклом сущностей через EntityManager (persist, flush).
    • Производительность в простых сценариях: Может быть избыточным для элементарных CRUD-операций.

Сравнительный пример:

// ### Active Record (Eloquent)
// app/Models/User.php
namespace AppModels;
use IlluminateDatabaseEloquentModel;

class User extends Model {
    // Связи, аксессоры, мутаторы, скоупы пишутся здесь же
    public function orders() {
        return $this->hasMany(Order::class);
    }
}

// Использование
$user = User::find(1); // Запрос в БД и создание объекта
$user->email = 'new@example.com';
$user->save(); // UPDATE запрос

// ### Data Mapper (Doctrine)
// src/Entity/User.php
namespace AppEntity;
use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 * @ORMTable(name="users")
 */
class User {
    /**
     * @ORMId
     * @ORMGeneratedValue
     * @ORMColumn(type="integer")
     */
    private ?int $id = null;

    /**
     * @ORMColumn(type="string")
     */
    private string $email;

    // Чистые геттеры и сеттеры, бизнес-методы
    public function getEmail(): string { return $this->email; }
    public function setEmail(string $email): void { $this->email = $email; }
}

// Использование (например, в контроллере)
$user = $entityManager->find(User::class, 1); // Загрузка из Identity Map или БД
$user->setEmail('new@example.com');
$entityManager->flush(); // Все изменения отслеживаемых сущностей будут сохранены

Вывод: Active Record отлично подходит для приложений с простой бизнес-логикой, прототипов и быстрой разработки. Data Mapper — это выбор для сложных предметных областей (Domain-Driven Design), где важны чистота модели, тестируемость и гибкость.