Может войдёшь?
Черновики Написать статью Профиль

Прототипное наследование в PHP

перевод

Вдохновлённый недавней статьёй на Reddit, я решил попробовать симулировать JavaScript-подобное прототипное наследование. Не потому что я думал, что это когда-нибудь будет использоваться в настоящих проектах. Скорее просто мне показалось, что будет интересно это попробовать.

И я был прав! Вот как я это сделал:

Конструирование

Прототипное наследование намного более изменчивое, чем классическое объектно-ориентированное проектирование. То, как это реализовано (в JavaScript), означает, что вы не можете зависеть от каких-либо методов или свойств, находящихся там постоянно. Это приводит к более обширному описанию объектов на этапе конструирования, чем в классическом объектно-ориентированном проектировании.

В классическом объектно-ориентированном проектировании вы скорее всего увидите что-нибудь такое:

PHP
class Shape
{
  protected 
$name "Shape";

  public function 
getType()
  {
    return 
$this->name;
  }
}

class 
Square extends Shape
{
  protected 
$name "Square";
}

$square = new Square();

print 
$square// "Square"

А при прототипном наследовании вы вероятнее всего столкнётесь с таким типом конструкций:

PHP
$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. Их сегодняшняя актуальность — отдельный разговор!

Это не означает, что языки прототипного наследования предоставляют расширение ключевых слов, или что они реализуют такие конструкции наследования. Я хочу сказать, что объекты (в языках прототипного наследования) гораздо более изменчивые.

Второй пример показывает направление, которое я решил выбрать при построении этого небольшого класса.

Без наследования

Простейший способ начать создавать этот класс — построить его без наследования. Это сводит требования к следующему списку:

  • Новые прототипы можно инициализировать с массивом членов.
  • Необходимы динамические члены (методы и свойства). После инициализации у вас должна быть возможность добавлять члены и возвращать/вызывать их при последующих вызовах.
  • Закрытие свойств (якобы методов) должно быть привязано к правильному контексту (экземпляру прототипа).

Давайте начнём с первого требования:

PHP
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:

PHP
/**
 * @param string $property
 * @param mixed  $value
 */
public function __set($property$value)
{
  
$this->members[$property] = $value;
}

Отлично! Теперь мы можем определить новые прототипы вот так:

PHP
$shape = new Prototype([
  
"name"    => "Shape",
  
"getType" => function () {
    return 
$this->name;
  }
]);

Для того чтобы получать эти члены из прототипа, нам надо создать метод __get:

PHP
/**
 * @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]);
}

Теперь мы можем задавать и получать члены из прототипа. А что насчёт возможности вызывать закрытие членов?

PHP
/**
 * @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) контексту. Наконец мы вызываем его.

Это позволяет нам делать следующее:

PHP
$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:

PHP
/**
 * @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:

PHP
/**
 * @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
    
);
  }
}

Мы выполнили требования одиночного наследования, и теперь мы можем делать следующее:

PHP
$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), если хотите, но при этом клонам не будут передаваться изменения оригинального прототипа (после клонирования).

Удачи вам в подобных вещах! Не принимайте это слишком серьёзно, чтобы не забыть, для чего это делается (для невнимательных: для обучения).

Как вы считаете, полезен ли этот материал? Да Нет

Написать комментарий

Разметка: ? ?

Авторизуйся, чтобы прокомментировать.