Вдохновлённый недавней ((http://www.reddit.com/r/PHP/comments/276158/want_javascriptlike_behaviour_with_your_php/==статьёй на 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" %% .(alert) Некоторые из вас могут сравнить эту псевдо-ООП конструкцию (применённую к прототипному языку) с чем-то из разряда 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)**, если хотите, но при этом клонам не будут передаваться изменения оригинального прототипа (после клонирования). Удачи вам в подобных вещах! Не принимайте это слишком серьёзно, чтобы не забыть, для чего это делается (для невнимательных: для обучения).