- PVSM.RU - https://www.pvsm.ru -
На хабре не раз поднимался вопрос важности SOLID, в частности принцип открытости/закрытости кода. В данном посте я расскажу как реализовать его на PHP и через какие испытания прийдётся пройти.
SOLID — это совокупность пяти основных принципов дизайна классов в ООП. Одни из них и является принцип открытости/закрытости кода. Он гласит, что программные сущности должны быть открыты для расширения, но закрыты для изменения.
Речь идёт о создании двух классов: основного и обёртки. Обёртка, будучи объектом, ссылался бы на основной, копируя его публичные методы и свойства, а ссылку на последнего можно было бы легко изменить. Вот примерная реализация:
interface iNote {
public function add($text);
public function edit($id);
public function delete($id);
}
class _note implements iNote {
public function add($text) { /* ... */ }
public function edit($id) { /* ... */ }
public function delete($id) { /* ... */ }
}
class Note implements iNote {
private $source;
public function setSource(iNote $source)
{
$this->source = $source;
}
public function add($text)
{
return $this->source->add($text);
}
public function edit($id)
{
return $this->source->edit($id);
}
public function delete($id)
{
return $this->source->delete($id);
}
}
Таким образом мы могли бы изменить поведение объекта, не модифицируя код класса.
Код такого класса-обёртки не универсален, и его прийдётся подстраивать под каждый основной класс. Но ведь у нас есть ООП и перегрузка методов и свойств, так почему бы нам не написать обёртку так, чтобы она полностью копировала публичные методы и свойства основного объекта?
class Note {
private $source;
public function __construct()
{
$class = '_' . __CLASS__;
$class = new $class();
$interface = 'i' . __CLASS__;
if (!$class instanceof $interface) die("Default class for '" . __CLASS__ . "' is diened");
else $this->source = new $class();
}
public function setSource($source)
{
$interface = 'i' . __CLASS__;
if (!$source instanceof $interface) die("Changed class for '" . __CLASS__ . "' is diened");
$this->source = $source;
}
public function __get($name)
{
return $this->source->$name;
}
public function __set($name, $value)
{
$this->source->$name = $value;
}
public function __call($func, $args)
{
return call_user_func_array(array($this->source, $func), $args);
}
}
Здесь переписанный класс-обёртка полностью универсален. При создании объекта, он попытается найти основной класс, имя которого должны составлять символ подчёркивания и имя класса-обёртки, и инициализировать его. В дальнейшем объект класса Note будет полностью копировать открытые методы и свойства объекта, лежащего в переменной $source (по дефолту — объект класса _Note, можно изменить, обратившись к методу Note::setSource()).
На первый взгляд всё работает нормально. Вот только не получается через объект-обёртку изменить какой-либо элемент массива основного объекта. Здесь в PHP есть хитрость: когда запрашивается изменение элемента(-ов) массива, скрипт пытается получить ссылку на этот массив (перегрузка __get()) и уже через неё вносить изменения. Соответственно, делаем правки в коде:
// public function __get($name) заменяем на:
public function &__get($name)
Действительно? Не уж то прийдётся отслеживать создание объектов этого класса до конца работы скрипта? Конечно, теоретически можно было бы повесить функцию через register_tick_function(), которая регулярно обновляла бы список всех зарегистрированных объектов и весила бы в объекты-обёртки свой основной класс, но это по-индусски. А можно было бы создать ещё один объект, который хранил бы стандартные объекты, но это торт. Я предлагаю создать систему констант. Например, пусть константы типа "[ИМЯ_КЛАССА]_DEFAULT" хранили бы имена дефолтных основных классов, которых бы запрашивали объекты-обёртки, чьих имя класса совпадает с описанным в имени константы, а константы типа "[ИМЯ_КЛАССА]_CHANGED" соответственно хранили бы имена изменённых основных классов. Вот так теперь будет выглядеть Note::__construct():
public function __construct() {
$interface = 'i' . __CLASS__;
if (!defined(strtoupper(__CLASS__) . '_DEFAULT')) die('Default class not set (' . __LINE__ . ' line in ' . __FILE__ . ')');
if (!defined(strtoupper(__CLASS__) . '_CHANGED') or constant(strtoupper(__CLASS__) . '_CHANGED') == null) {
$class = constant(strtoupper(__CLASS__) . '_DEFAULT');
$class = new $class();
if (!$class instanceof $interface) die("Default class for '" . __CLASS__ . "' is diened (" . __LINE__ . " line in " . __FILE__ . ")");
else $this->source = new $class();
}
else {
$class = constant(strtoupper(__CLASS__) . '_CHANGED');
$class = new $class();
if (!$class instanceof $interface) {
die("Changed class for '" . __CLASS__ . "' is diened (" . __LINE__ . " line in " . __FILE__ . ")");
}
else $this->source = new $class;
}
}
Весьма забавно будет, если два параллельных модуля попытаются изменить поведение объекта одного и того же класса. Как быть тогда? Заставить разработчиков заблаговременно договариваться «кто какой класс забил»? Здесь никак не помочь, но можно принять меры. Я не думаю, что разработчику нужно перестроить логику работы всего класса. Может просто позволить переопределить функцию? Переопределить — громко сказано, имеется в виду перехватить вызов метода на уровне объекта-обёртки. Давайте обозначим 2-й тип констант: "[ИМЯ_КЛАССА]_FUNCTION_[ИМЯ_МЕТОДА]", в которой будет лежать имя пользовательской функции, которую будет вызывать объект-обёртка при перехвате обращения к методу, имя которого указано в имени константы, класса, имя которого указано в имени константы, которой будут переданы ссылка на объект-обёртку первым аргументом и аргументы, с которыми вызывался метод, начиная со второго. Весьма замысловато получилось, возможно код немного прояснит прочитанное:
public function __call($func, $args) {
if (defined(strtoupper(__CLASS__) . '_CHANGED')) return call_user_func_array(array(&$this->source, $func), $args); # Если основной класс изменён, то обращение идёт к нему в любом случае
if (defined(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func))) {
return call_user_func_array(constant(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func)), array(&$this) + $args);
}
else
return call_user_func_array(array(&$this->source, $func), $args);
}
В ту самую функцию, имя которой указано в константе типа "[ИМЯ_КЛАССА]_FUNCTION_[ИМЯ_МЕТОДА]" первым аргументом передаётся ссылка на объект-обёртку, но через него не получить доступа к private- и protected- методам и свойствам, что весьма тривиально. Давайте создадим ещё один класс-обёртку, у которого будет доступ к защищённым методам и свойствам основного объекта. Пусть его имя составляют два символа подчёркивания и имя первого класса-обёртки. Сама же задача решается через ReflectionProperty и ReflectionMethod:
class __Note extends Note {
private $source;
private function &reader ($object, $property) {
$sweetsThief = new ReflectionProperty(&$object, $property);
return $sweetsThief;
}
public function __construct(&$source) {
$interface = 'i' . substr(__CLASS__, 2);
if (!$source instanceof $interface) die('Die construct ' . __CLASS__ . ' on ' . __LINE__ . ' line in ' . __FILE__ . '.');
$this->source = $source;
}
public function &__get($name) {
try {
$sweetsThief = new ReflectionProperty(get_class($this->source), $name);
$sweetsThief->setAccessible(true);
$return = $sweetsThief->getValue(&$this->source);
return $return;
}
catch (ReflectionException $e) {
return '';
}
}
public function __set($name, $value) {
try {
$sweetsThief = new ReflectionProperty(get_class($this->source), $name);
$sweetsThief->setAccessible(true);
$sweetsThief->setValue(&$this->source, $value);
}
catch (ReflectionException $e) { }
}
public function __isset($name) {
try {
$sweetsThief = new ReflectionProperty(get_class($this->source), $name);
return true;
}
catch (ReflectionException $e) {
return false;
}
}
public function __call($func, $args) {
$sweetsThief = new ReflectionMethod(get_class($this->source), $func);
$sweetsThief->setAccessible(true);
return $sweetsThief->invokeArgs(&$this->source, $args);
}
}
И модифицируем функцию __call():
public function __call($func, $args) {
if (defined(strtoupper(__CLASS__) . '_CHANGED')) return call_user_func_array(array(&$this->source, $func), $args);
if (defined(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func))) {
$obj = '__' . __CLASS__;
$obj = new $obj(&$this->source);
return call_user_func_array(constant(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func)), array(&$obj) + $args);
}
else
return call_user_func_array(array(&$this->source, $func), $args);
}
И опять же вернёмся к старому. Есть ещё вероятность, что два независимых модуля захотят заменить один и тот же метод одного и того же класса. Возможно, эта вероятность снизится, если создать события вызова метода и возврата значения от метода. Давайте обозначим ещё 2 типа констант: "[ИМЯ_КЛАССА]_[ИМЯ_ФУНКЦИИ]_EVENT_CALL_[ID]" и "[ИМЯ_КЛАССА]_[ИМЯ_ФУНКЦИИ]_EVENT_RETURN_[ID]". Я думаю, что и так понятно, как всё устроенно, а если я возьмусь объяснять, то Вам станет только непонятнее. Я лишь скажу, что в функцию, чьё имя указано в первой константе, передаются два аргумента: ссылка на объект-обёртку с доступом к защищённым элементам и аргументы вызова метода в виде массива, а вернуть она должна аргументы в виде массива, которые потом передадутся методу. В функцию, чьё имя указано во второй константе, будет передано 3 аргумента: ссылка на объект-обёртку с доступом к защищённым элементам, аргументы вызова метода в виде массива и return метода, на который установлено событие, а должна она будет вернуть отформатированную версию return метода, которая будет передана скрипту.
public function __call($func, $args) {
if (defined(strtoupper(__CLASS__) . '_CHANGED')) return call_user_func_array(array(&$this->source, $func), $args);
if (defined(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func))) {
$obj = '__' . __CLASS__;
$obj = new $obj(&$this->source);
return call_user_func_array(constant(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func)), array(&$obj) + $args);
}
else {
$obj = '__' . __CLASS__;
$obj = new $obj(&$this->source);
for ($i = 0; defined(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func) . "_EVENT_CALL_" . $i); $i++)
$args = call_user_func(constant(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func) . "_EVENT_CALL_" . $i), $obj, $args);
$return = call_user_func_array(array(&$this->source, $func), $args);
for ($i = 0; defined(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func) . "_EVENT_RETURN_" . $i); $i++)
$return = call_user_func(constant(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func) . "_EVENT_RETURN_" . $i), $obj, $args, $return);
return $return;
}
}
Таким образом, мы получили два универсальных класса-обёртки для реализации принципа открытости/закрытости кода. Полноценный код под споилером.
interface iAbc {
public function test1();
public function test3($echo);
}
define("ABC_DEFAULT", "_abc");
class _abc implements iAbc {
public function test1() {
echo 'test1';
}
private function test2() {
echo 'test2';
}
public function test3($echo) {
return ':' . $echo . ':';
}
private $_test = '_test';
}
class Abc {
private $source;
public function __construct() {
$interface = 'i' . __CLASS__;
if (!defined(strtoupper(__CLASS__) . '_DEFAULT')) die('Default class not set (' . __LINE__ . ' line in ' . __FILE__ . ')');
if (!defined(strtoupper(__CLASS__) . '_CHANGED') or constant(strtoupper(__CLASS__) . '_CHANGED') == null) {
$class = constant(strtoupper(__CLASS__) . '_DEFAULT');
$class = new $class();
if (!$class instanceof $interface) die("Default class for '" . __CLASS__ . "' is diened (" . __LINE__ . " line in " . __FILE__ . ")");
else $this->source = new $class();
}
else {
$class = constant(strtoupper(__CLASS__) . '_CHANGED');
$class = new $class();
if (!$class instanceof $interface) {
die("Changed class for '" . __CLASS__ . "' is diened (" . __LINE__ . " line in " . __FILE__ . ")");
}
else $this->source = new $class;
}
}
public function setSource($source) {
$interface = 'i' . __CLASS__;
if (!$source instanceof $interface) die('Die argument for ' . __CLASS__ . '::setSource() on ' . __LINE__ . ' line in ' . __FILE__ . '.');
$this->source = $source;
}
public function &__get($name) {
return $this->source->$name;
}
public function __set($name, $value) {
$this->source->$name = $value;
}
public function __isset($name) {
return isset($this->source->$name);
}
public function __call($func, $args) {
if (defined(strtoupper(__CLASS__) . '_CHANGED')) return call_user_func_array(array(&$this->source, $func), $args);
if (defined(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func))) {
$obj = '__' . __CLASS__;
$obj = new $obj(&$this->source);
return call_user_func_array(constant(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func)), array(&$obj) + $args);
}
else {
$obj = '__' . __CLASS__;
$obj = new $obj(&$this->source);
for ($i = 0; defined(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func) . "_EVENT_CALL_" . $i); $i++)
$args = call_user_func(constant(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func) . "_EVENT_CALL_" . $i), $obj, $args);
$return = call_user_func_array(array(&$this->source, $func), $args);
for ($i = 0; defined(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func) . "_EVENT_RETURN_" . $i); $i++)
$return = call_user_func(constant(strtoupper(__CLASS__) . '_FUNCTION_' . strtoupper($func) . "_EVENT_RETURN_" . $i), $obj, $args, $return);
return $return;
}
}
}
class __Abc extends Abc {
private $source;
private function &reader ($object, $property) {
$sweetsThief = new ReflectionProperty(&$object, $property);
return $sweetsThief;
}
public function __construct(&$source) {
$interface = 'i' . substr(__CLASS__, 2);
if (!$source instanceof $interface) die('Die construct ' . __CLASS__ . ' on ' . __LINE__ . ' line in ' . __FILE__ . '.');
$this->source = $source;
}
public function &__get($name) {
try {
$sweetsThief = new ReflectionProperty(get_class($this->source), $name);
$sweetsThief->setAccessible(true);
$return = $sweetsThief->getValue(&$this->source);
return $return;
}
catch (ReflectionException $e) {
return '';
}
}
public function __set($name, $value) {
try {
$sweetsThief = new ReflectionProperty(get_class($this->source), $name);
$sweetsThief->setAccessible(true);
$sweetsThief->setValue(&$this->source, $value);
}
catch (ReflectionException $e) { }
}
public function __isset($name) {
try {
$sweetsThief = new ReflectionProperty(get_class($this->source), $name);
return true;
}
catch (ReflectionException $e) {
return false;
}
}
public function __call($func, $args) {
$sweetsThief = new ReflectionMethod(get_class($this->source), $func);
$sweetsThief->setAccessible(true);
return $sweetsThief->invokeArgs(&$this->source, $args);
}
}
function rewrite_test1($obj) {
$obj->test2();
//echo (isset($obj->_test)) ? '1' : '0';
}
function event_call_1($obj, $args) {
$args[0] = 'it not argument, lol';
return $args;
}
function event_call_2($obj, $args) {
$args[0] = 'nice try, it's argument.';
return $args;
}
function event_return_1($obj, $args, $return) {
$return .= '1';
return $return;
}
function event_return_2($obj, $args, $return) {
$return .= '2';
return $return;
}
$abc = new Abc();
$abc->test1(); echo '<br />';
//Перезапишем функцию Abc::test1()
define("ABC_FUNCTION_TEST1", "rewrite_test1");
$abc->test1(); echo '<br />';
//Поставим event на вызов функции Abc::test3()
define("ABC_FUNCTION_TEST3_EVENT_CALL_0", "event_call_1");
echo $abc->test3('ZzZz'); echo '<br />';
//Поставим второй event на вызов функции Abc::test3()
define("ABC_FUNCTION_TEST3_EVENT_CALL_1", "event_call_2");
echo $abc->test3('ZzZz'); echo '<br />';
//Поставим второй event на возврат значения от функции Abc::test3()
define("ABC_FUNCTION_TEST3_EVENT_RETURN_0", "event_return_1");
echo $abc->test3('ZzZz'); echo '<br />';
//Поставим второй event на возврат значения от функции Abc::test3()
define("ABC_FUNCTION_TEST3_EVENT_RETURN_1", "event_return_2");
echo $abc->test3('ZzZz'); echo '<br />';
/**
* Вывод:
* test1
* test2
* :it not argument, lol:
* :nice try, it's argument.:
* :nice try, it's argument.:1
* :nice try, it's argument.:12
*/
?>
Автор: survive
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/56903
Ссылки в тексте:
[1] Источник: http://habrahabr.ru/post/215515/
Нажмите здесь для печати.