[PHP] Принцип открытости/закрытости кода и какие трудности могут встать на пути

в 12:40, , рубрики: php, solid, Веб-разработка, ооп, метки: , ,

На хабре не раз поднимался вопрос важности 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); 
	}
}

Таким образом мы могли бы изменить поведение объекта, не модифицируя код класса.

Испытание №1: Неуниверсальность

Код такого класса-обёртки не универсален, и его прийдётся подстраивать под каждый основной класс. Но ведь у нас есть ООП и перегрузка методов и свойств, так почему бы нам не написать обёртку так, чтобы она полностью копировала публичные методы и свойства основного объекта?

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()).

Испытание №2: Массивы

На первый взгляд всё работает нормально. Вот только не получается через объект-обёртку изменить какой-либо элемент массива основного объекта. Здесь в PHP есть хитрость: когда запрашивается изменение элемента(-ов) массива, скрипт пытается получить ссылку на этот массив (перегрузка __get()) и уже через неё вносить изменения. Соответственно, делаем правки в коде:

// public function __get($name) заменяем на:
public function &__get($name)

Испытание №3: А что если нужно изменить поведение всех объектов этого класса?

Действительно? Не уж то прийдётся отслеживать создание объектов этого класса до конца работы скрипта? Конечно, теоретически можно было бы повесить функцию через 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;
	}
	
}

Испытание №4: Конфликт модулей

Весьма забавно будет, если два параллельных модуля попытаются изменить поведение объекта одного и того же класса. Как быть тогда? Заставить разработчиков заблаговременно договариваться «кто какой класс забил»? Здесь никак не помочь, но можно принять меры. Я не думаю, что разработчику нужно перестроить логику работы всего класса. Может просто позволить переопределить функцию? Переопределить — громко сказано, имеется в виду перехватить вызов метода на уровне объекта-обёртки. Давайте обозначим 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);
	}

Испытание №5: А где доступ к private?

В ту самую функцию, имя которой указано в константе типа "[ИМЯ_КЛАССА]_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);
}

Испытание №6: Что? Опять конфликт модулей?!

И опять же вернёмся к старому. Есть ещё вероятность, что два независимых модуля захотят заменить один и тот же метод одного и того же класса. Возможно, эта вероятность снизится, если создать события вызова метода и возврата значения от метода. Давайте обозначим ещё 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js