Вдохновлённый недавней статьёй на Reddit, я решил попробовать симулировать JavaScript-подобное прототипное наследование. Не потому что я думал, что это когда-нибудь будет использоваться в настоящих проектах. Скорее просто мне показалось, что будет интересно это попробовать.
И я был прав! Вот как я это сделал:
Конструирование
Прототипное наследование намного более изменчивое, чем классическое объектно-ориентированное проектирование. То, как это реализовано (в JavaScript), означает, что вы не можете зависеть от каких-либо методов или свойств, находящихся там постоянно. Это приводит к более обширному описанию объектов на этапе конструирования, чем в классическом объектно-ориентированном проектировании.
В классическом объектно-ориентированном проектировании вы скорее всего увидите что-нибудь такое:
class Shape
{
protected $name = "Shape";
public function getType()
{
return $this->name;
}
}
class Square extends Shape
{
protected $name = "Square";
}
$square = new Square();
print $square; // "Square"
А при прототипном наследовании вы вероятнее всего столкнётесь с таким типом конструкций:
$shape = new Prototype([
"name" => "Shape",
"getType" => function () {
return $this->name;
}
]);
$square = new Prototype([
"extends" => $shape,
"name" => "Square"
]);
print $square->getType(); // "Square"
Некоторые из вас могут сравнить эту псевдо-ООП конструкцию (применённую к прототипному языку) с чем-то из разряда MooTools или Prototype (JS). Таким было моё знакомство с построением многократно используемых компонентов JavaScript. Их сегодняшняя актуальность — отдельный разговор!
Это не означает, что языки прототипного наследования предоставляют расширение ключевых слов, или что они реализуют такие конструкции наследования. Я хочу сказать, что объекты (в языках прототипного наследования) гораздо более изменчивые.
Второй пример показывает направление, которое я решил выбрать при построении этого небольшого класса.
Без наследования
Простейший способ начать создавать этот класс — построить его без наследования. Это сводит требования к следующему списку:
- Новые прототипы можно инициализировать с массивом членов.
- Необходимы динамические члены (методы и свойства). После инициализации у вас должна быть возможность добавлять члены и возвращать/вызывать их при последующих вызовах.
- Закрытие свойств (якобы методов) должно быть привязано к правильному контексту (экземпляру прототипа).
Давайте начнём с первого требования:
class Prototype
{
/**
* @var array
*/
protected $members = [];
/**
* @param array $prototype
*/
public function __construct(array $prototype = [])
{
foreach ($prototype as $key => $value) {
$this->$key = $value;
}
}
}
Мы можем содержать члены в защищённом свойстве. Есть расхождение между тем, как мы храним их ($members), и тем как мы задаём их ($this->$key). Эту проблему можно решить созданием метода __set:
/**
* @param string $property
* @param mixed $value
*/
public function __set($property, $value)
{
$this->members[$property] = $value;
}
Отлично! Теперь мы можем определить новые прототипы вот так:
$shape = new Prototype([
"name" => "Shape",
"getType" => function () {
return $this->name;
}
]);
Для того чтобы получать эти члены из прототипа, нам надо создать метод __get:
/**
* @param $property
*
* @return mixed
*/
public function __get($property)
{
if ($this->__isDefined($property)) {
return $this->members[$property];
}
}
/**
* @param string $property
*
* @return bool
*/
public function __isDefined($property)
{
return isset($this->members[$property]);
}
Теперь мы можем задавать и получать члены из прототипа. А что насчёт возможности вызывать закрытие членов?
/**
* @param string $method
* @param mixed $parameters
*
* @return mixed
*/
public function __call($method, $parameters)
{
if ($this->__isDefined($method)
and $this->__isCallable($method)) {
return call_user_func_array(
$this->__getBoundClosure($method),
$parameters
);
}
}
/**
* @param string $method
*
* @return bool
*/
public function __isCallable($method)
{
return $this->members[$method] instanceof Closure;
}
/**
* @param string $method
* @param mixed $context
*
* @return mixed
*/
public function __getBoundClosure($method, $context = null)
{
if ($context === null) {
$context = $this;
}
$closure = $this->members[$method];
return $closure->bindTo($context);
}
Мы повторно использовали метод для того, чтобы убедиться, что член определён, и мы добавили еще один, чтобы проверить, является ли он вызываемым. Если эти условия соблюдены, мы получаем закрытие из массива членов и привязываем его к указанному (по умолчанию: $this) контексту. Наконец мы вызываем его.
Это позволяет нам делать следующее:
$shape = new Prototype([
"getType" => function () {
return "Shape";
}
]);
$shape->getType; // Closure
$shape->name = "I am a Shape";
$shape->getType = function () {
return $this->name;
};
$shape->getType(); // "I am a Shape"
С наследованием
Что же нужно изменить, чтобы обеспечить наследование? Ну (для одиночного наследования) нам надо получать члены из родительского прототипа, если они не определены для дочернего прототипа:
- Свойства, не определённые для дочернего прототипа, должны быть получены из родительского прототипа.
- Методы, не определённые для дочернего прототипа, должны быть вызваны (в контексте дочернего прототипа) из родительского прототипа.
Сначала мы изменим метод __get:
/**
* @param $property
*
* @return mixed
*/
public function __get($property)
{
if ($this->__isDefined($property)) {
return $this->members[$property];
}
if ($this->__isDefined("extends")) {
return $this->extends->$property;
}
}
…а затем мы изменим метод __call:
/**
* @param string $method
* @param mixed $parameters
*
* @return mixed
*/
public function __call($method, $parameters)
{
if ($this->__isDefined($method)
and $this->__isCallable($method)) {
return call_user_func_array(
$this->__getBoundClosure($method),
$parameters
);
}
if ($this->__isDefined("extends")) {
return call_user_func_array(
$this->extends->__getBoundClosure($method, $this),
$parameters
);
}
}
Мы выполнили требования одиночного наследования, и теперь мы можем делать следующее:
$shape = new Prototype([
"name" => "Shape",
"getType" => function () {
return $this->name;
}
]);
print $shape; // "Shape"
$square = new Prototype([
"extends" => $shape,
"name" => "Square"
]);
print $square; // "Square"
$shape->label = "A simple shape";
$square->getLabel = function () {
return $this->label;
};
print $square->getLabel(); // "A simple shape"
На ваш страх и риск
Повторюсь, не используйте это в рабочих проектах. Это эксперимент, а не очередная библиотека/фреймворк/платформа.
Есть моменты, в которых эта реализация отстаёт от реализации JavaScript. У вас должна быть возможность делать такие вещи как $shape() для создания нового экземпляра прототипа Shape, но я не реализовал это. Попробуйте clone($shape), если хотите, но при этом клонам не будут передаваться изменения оригинального прототипа (после клонирования).
Удачи вам в подобных вещах! Не принимайте это слишком серьёзно, чтобы не забыть, для чего это делается (для невнимательных: для обучения).