Прототип

Прототип — это порождающий паттерн проектирования, который позволяет копировать объекты, не привязываясь к их конкретным классам, вместо того чтобы создавать их заново.
Проблема
Продолжим автомобильную серию из статьи про Строитель. Допустим, характеристики двигателя вычисляются дорого — например, запрашиваются с внешнего стенда измерений:
class Engine
{
public int $horsePower;
public function __construct(string $model)
{
// Дорогая операция: запрос к внешнему сервису измерений.
$this->horsePower = DynoService::measure($model);
}
}
class Car
{
public function __construct(
public readonly Engine $engine,
public readonly string $tires,
public string $color,
) {}
}Автосалону нужно 50 тестовых машин с одинаковой базовой комплектацией, отличающихся только цветом. Создавая каждую машину заново — например, через CarDirector и CarBuilder из прошлой статьи, — мы 50 раз пересоздаём Engine, а значит, 50 раз обращаемся к DynoService.
Первая мысль — просто переиспользовать переменную:
$base = new Car(new Engine('V8 Turbo'), 'Michelin', 'Sports Red');
$copy = $base;
$copy->color = 'Blue';
echo $base->color; // 'Blue' — исходная машина тоже перекрасилась!В PHP объекты присваиваются по идентификатору (handle), а не копируются. $copy = $base не создаёт новый объект — обе переменные указывают на один и тот же экземпляр. Чтобы получить независимую копию, объект нужно клонировать.
Решение
PHP умеет клонировать объекты через встроенное ключевое слово clone — оно создаёт новый объект с теми же значениями свойств, не вызывая конструктор:
$copy = clone $base;
$copy->color = 'Blue';
echo $base->color; // 'Sports Red' — исходная машина не изменилась.Engine при этом заново не создаётся и DynoService повторно не вызывается — copy получает то же значение horsePower, что и оригинал.
Ловушка поверхностного клонирования
По умолчанию clone копирует свойства объекта поверхностно: если свойство само является объектом, копия получит ссылку на тот же вложенный объект, а не на его копию:
$sport = new Car(new Engine('V8 Turbo'), 'Michelin', 'Sports Red');
$family = clone $sport;
$family->engine->horsePower = 100; // хотим "приглушить" мотор только у семейной версии
echo $sport->engine->horsePower; // тоже изменилось — Engine общий на двоихЧтобы вложенные объекты копировались тоже, нужно определить магический метод __clone() — PHP вызывает его автоматически на новом объекте сразу после поверхностного копирования:
class Car
{
public function __construct(
public readonly Engine $engine,
public readonly string $tires,
public string $color,
) {}
public function __clone(): void
{
// На момент вызова $this уже является копией,
// но $this->engine всё ещё указывает на объект оригинала.
$this->engine = clone $this->engine;
}
}Теперь clone $sport рекурсивно клонирует и вложенный Engine, и изменения в копии больше не задевают оригинал.
Переприсваивание
readonly-свойства внутри__clone()стало возможным начиная с PHP 8.3. На более старых версиях$this->engine = ...в этом методе упадёт сCannot modify readonly property— тамengineпришлось бы делать обычным свойством.
Клонирование через интерфейс
Когда клиентский код работает с объектами через абстракцию и не знает их конкретного класса, clone всё равно работает — PHP вызовет __clone() того класса, экземпляр которого клонируют. Явный интерфейс полезен, чтобы зафиксировать это поведение как часть контракта, а не как деталь реализации:
interface CarPrototype
{
public function cloneCar(): static;
}
class Car implements CarPrototype
{
public function __construct(
public readonly Engine $engine,
public readonly string $tires,
public string $color,
) {}
public function __clone(): void
{
$this->engine = clone $this->engine;
}
public function cloneCar(): static
{
return clone $this;
}
}Глубокое копирование по-прежнему делает __clone() — именно его вызывает оператор clone изнутри. cloneCar() лишь фиксирует это как явный метод интерфейса, а не деталь реализации, которую нужно знать клиенту.
Реестр прототипов
Готовые, полностью настроенные экземпляры удобно хранить в одном месте и выдавать по запросу копию нужной конфигурации:
class CarPrototypeRegistry
{
/** @var array<string, Car> */
private array $prototypes = [];
public function register(string $name, Car $prototype): void
{
$this->prototypes[$name] = $prototype;
}
public function create(string $name): Car
{
if (!isset($this->prototypes[$name])) {
throw new InvalidArgumentException("Unknown prototype: {$name}");
}
return $this->prototypes[$name]->cloneCar();
}
}
$registry = new CarPrototypeRegistry();
$registry->register('sport', new Car(new Engine('V8 Turbo'), 'Michelin', 'Sports Red'));
$registry->register('family', new Car(new Engine('Hybrid'), 'Standard', 'White'));
$orderCar = $registry->create('sport');
$orderCar->color = 'Blue';DynoService::measure() вызывается один раз при регистрации прототипа — каждый следующий заказ обходится без дорогого пересчёта.
Из чего состоит паттерн
- Прототип (
CarPrototype) — интерфейс с методом клонирования (опционален:cloneработает и без него). - Конкретный прототип (
Car) — реализует__clone(), при необходимости глубоко копируя вложенные объекты. - Реестр прототипов (
CarPrototypeRegistry, опционально) — хранит именованные готовые экземпляры и выдаёт их копии. - Клиент — запрашивает копию вместо того, чтобы создавать объект через конструктор.
Прототип vs Строитель
Оба паттерна дают готовый сложный объект, но разными путями:
| Строитель | Прототип | |
|---|---|---|
| Источник объекта | Собирается заново, шаг за шагом | Копируется с уже существующего экземпляра |
| Когда выгоднее | Подходящего экземпляра ещё нет | Похожий экземпляр уже есть и дорог в создании |
| Что нужно знать | Шаги сборки | Как корректно клонировать (в т.ч. вложенные объекты) |
Строитель отвечает на вопрос «как собрать объект с нуля», Прототип — «как получить второй такой же, не собирая заново».
Когда применять
- Создание объекта дороже (по времени, ресурсам, обращениям к внешним системам), чем его копирование.
- Нужно копировать объект, не зная его конкретного класса — только то, что он реализует интерфейс прототипа.
- Система должна уметь конфигурировать себя готовыми экземплярами в рантайме, а не жёстко заданными классами (реестр прототипов).
Плюсы и минусы
Прототип избавляет от повторной дорогой инициализации, позволяет копировать объекты полиморфно через интерфейс и уменьшает число фабрик/подклассов, нужных только ради вариаций готового объекта.
Минус — глубокое клонирование сложных графов объектов с циклическими ссылками требует аккуратной реализации __clone(). Объекты, держащие внешние ресурсы (открытый файл, сетевое соединение, файловый дескриптор), в принципе не стоит клонировать бездумно — такие поля в __clone() нужно либо пересоздавать заново, либо явно обнулять.
Итог
Прототип полезен, когда в системе уже есть полностью настроенный экземпляр, а создание нового с нуля стоит дороже, чем его копирование. В PHP это встроенная возможность языка — clone и __clone() — а не решение, которое приходится городить вручную, как в языках без нативной поддержки клонирования.