Сайт вебмастера

ООП в PHP

20-05-2019Время чтения ~ 9 мин.PHP/ООП 14195

Следует учитывать, что в PHP несколько упрощенная реализация ООП (объектно-ориентированное программирование). Поэтому, когда речь идёт об ООП как абстрактной парадигме, то следует использовать какой-то более серьёзный язык, вроде Java, С++ или Object Pascal. Потому что на этих языкам можно посмотреть практическую реализацию принципов ООП. В PHP программисты пытаются подражать другим ЯП, что в итоге приводит к излишней сложности и путанице, поскольку язык сам по себе не позволяет сделать «как в теории».

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

Смысл ООП как раз и заключается в том, чтобы упростить разработку. Если программа после использования принципов ООП стала более запутанной, то это говорит либо о том, что ООП никуда не годится (что навряд ли), либо о том, что программист что-то неверно понял в этом самом ООП (скорее всего).

Попробуем разобраться. Кратенько без «воды».

В ООП главное — это объекты, которые в PHP есть не что иное как переменные абстрактного типа данных (который задаёт программист).

Такой тип данных может содержать поля и методы/функции. Типы данных могут быть простыми, например integer, string, boolean. Но могут быть и более сложными, например array. В Паскале есть специальный тип record (запись), которая содержит поля данных произвольного типа.

Я иногда буду приводить примеры из Паскаля, поскольку в нём очень хорошо показывается что к чему, и он прост для понимания, даже тем, кто его не изучал.
type stud = record
	fio: string[20]
	bal: 6..15;
	sum: real;
end;

В PHP нет аналога record, хотя к нему близок массив из-за особенностей типизации. Если запись сделать «активной», то есть снабдить собственными функциями, то получится тип данных, который в ООП называется класс class. В классе можно задавать область видимости.

class Stud {
	// поля данных
	private $fio;
	public $bal;
	
	// методы
	private function in() { }
	public function out() { }
}

Объект — это экземпляр класса (читай типа данных). Но, в отличие от обычного присваивания, объекты создаются через специальную php-конструкцию new. В ней происходит инициализация (выделяется память и т.п.) и возвращается ссылка на готовый объект. Дальше мы получаем доступ к полям и методам класса.

$s = new Stud;
$s->ball = 12; 
$s->out(); 

По сути методы класса — это те же самые функции, но есть одно большое отличие — это область видимости. Если обычные функции всегда (почти) имеют глобальную область видимости, то методы ограничены только классами. Это позволяет создавать разные классы с одними и теми же именами методов (и полей).

class A {
	public function sum() { }
}
 
class B {
	public function sum() { }
}
 
$a = new A;
$b = new B;
 
$a->sum();
$b->sum();

Выделяют специальные статические методы, с помощью которых можно получить доступ к методам класса без инициализацию через new(). Обычно такие классы представляют собой набор функций, которые могут быть выполнены сами по себе. Если делать без static, то вначале пришлось бы выполнить инициализацию объекта.

class Ar {
	public static function sum($a, $b) 
	{ 
		return $a + $b;
	}
}
 
echo Ar::sum(2, 6);

Способность объекта скрывать свою реализацию, называется инкапсуляцией. То есть класс можно рассматривать как черный ящик, о котором мы знаем только о его полях и методах. Это позволяет разделить код на части, что немного напоминает концепцию модулей в других языках: сам класс выносится в отдельный файл, а в другом он уже используются.

В ООП главное не только объекты, но и связи между ними. Основной механизм взаимоотношения между классами — наследование, когда один класс может быть наследником другого. С помощью наследования строится иерархическая цепочка классов. Это имеет большое практическое значение.

Например у нас есть библиотека для работы с базой данных, реализованная в виде класса DB. Пусть это будет даже сторонняя библиотека, которую мы не можем изменить. В процессе работы, нам понадобилось добавить новый метод. Вместо того, чтобы переписывать родительский класс, будет достаточно создать новый в виде потомка с помощью extends.

class DB {
	...
} 
 
class MyDB extends DB {
	public function myselect() { ... }
}
 
$db = new MyDB();
$db->myselect();

С другой стороны, класс DB тоже может измениться, например появятся новые функции, а значит они автоматически станут доступны у всех потомков.

При построении сложных классов не всегда бывает возможность заранее определить конкретную реализацию. Например при разработке несколькими программистами необходимо заранее договориться что в таком-то классе будут такие-то методы. Для решения таких задач используются интерфейсы — это практически те же классы, только не имеющие реализации.

interface IDB {
	public function select();
	public function where();
	public function join();
}
 
class QBuilder implements IDB {
	public function select() { ... }
	public function where() { ... }
	public function join() { ... }
}

Класс QBuilder должен реализовать все методы, описанные в интерфейсе IDB. В «больших» языках программирования, интерфейсы помогают детально проработать не только иерархию классов, но и необходимую функциональность (и приведение типов). В PHP потребность интерфейсов достаточно мала, поскольку как правило один интерфейс используется только одним классом (то есть интерфейс лишняя сущность).

В некоторых случаях интерфейс можно рассматривать как «каркас» класса, который обладает большей читабельностью, чем реализованный класс.

Ещё одной разновидностью классов являются абстрактные классы. Это такие классы, у которых не может быть создан объект. С практической точки зрения абстрактный класс можно рассматривать точно также как и интерфейс. Разница между ними по сути в том, что ваш класс должен реализовывать интерфейс, а абстрактный класс нужно расширять (наследовать). При этом в PHP интерфейсы могут наследовать другие интерфейсы (но не классы).

abstract class ADB {
	public function select() { }
	public function where() { }
	public function join() { }
}
 
class QBuilder extends ADB {
	public function select() { ... }
	public function where() { ... }
	public function join() { ... }
}

Примером использования может быть класс базы данных. Поскольку реализация будет зависеть от конкретной базы (MySQL, PDO, Sqlite и т.п.), то используя в вершине иерархии интерфейс или абстрактный класс, можно сделать несколько потомков, гарантируя, что у всех будут те же методы (и тип), при этом расширив их специфичными методами.

interface IDB {
	public function select();
	public function where();
	public function join();
}
 
class QMySQL implements IDB {
	public function select() { ... }
	public function where() { ... }
	public function join() { ... }
	
	public function get_db() { ... }
}
 
class QSqlite implements IDB {
	public function select() { ... }
	public function where() { ... }
	public function join() { ... }
	
	public function get_file() { ... }
}
 
class QPDO implements IDB {
	public function select() { ... }
	public function where() { ... }
	public function join() { ... }
	
	public function connect() { ... }
}

Еще одним важным принципом ООП является полиморфизм . Вначале рассмотрим что такое «настоящий» полиморфизм (полиморфизм) — это способность функции обрабатывать данные разных типов.

На примере Паскаля это может выглядеть так:

program Adhoc;
 
function Add(x, y : Integer) : Integer;
begin
	Add := x + y;
end;
 
function Add(s, t : String) : String;
begin
	Add := Concat(s, t);
end;
 
begin
	Writeln(Add(1, 2));
	Writeln(Add('Hello, ', 'World!'));
end.

В данном примере функция Add объявлена несколько раз с разными входными параметрами. Компилятор будет смотреть какой входящий тип данных и выполнять подходящую функцию. Реализуется это за счёт того, что компилятор использует «сигнатуру» функции, в которую входит не только название, но и типы принимаемых данных.

В PHP есть два ограничения. Первое — не может быть двух одноименных функций и второе — динамическая типизация, когда компилятор сам решает какой тип данных использовать (в PHP 7/8 идёт работа в сторону строгой типизации).

Нужно понимать, что PHP никогда не создавался как объектно-ориентированный. Это всегда был функциональный скриптовый язык, в задачу которого входило быстро выполнить небольшой код-вставку в HTML. ООП в PHP был добавлен по сути только в 5-й версии (2004 год), да и то в слишком простой форме. Более-менее говорить об ООП в PHP можно лишь с версии 5.3 (2009 год). Поэтому PHP не просто в роли «догоняющего» — в нём есть довольно существенные внутренние ограничения, которые не позволяют вывести поддержку того же полиморфизма на уровень Java или C++.

Поскольку в PHP функции не могут быть перегружены (то есть мы не можем создать две одноименные функции), а значит на этом уровне ad-hoc-полиморфизм просто отсутствует. Точно такая же ситуация и в методах классов — невозможно создать одноименную функцию.

Поэтому в PHP полиморфизм рассматривается как переопределение (или перекрытие), то есть когда потомок переопределяет метод родительского класса.

class A {
   function out()
   {
      echo 'Class A (out)';
   }
   
   function in()
   {
      echo 'Class A (in)';
   }
}
 
class B extends A {
   function out()
   {
      echo 'Class B (out)';
   }
}
 
$b = new B;
 
$b->out(); // "Class B (out)"
$b->in();  // "Class A (in)"

Здесь мы видим то же самое наследование, но при этом есть возможность переопределить класс родителя. Это достигается за счёт того, что в PHP все методы виртуальные. В некоторых других языках для переопределения следует явно указывать «виртуальность».

Часто приходится встречать выражение «Один интерфейс - много реализаций» (сказал Бьёрн Страуструп, автор C++). Выражение на само деле подходит лишь к «настоящему» полиморфизму, то есть не реализуемый в PHP. Часто приходится видеть совершенно бездумное раздувание кода, когда класс разбивается на абстрактный класс и интерфейс (потому что об этом сказал Страуструп...). То есть вместо одной сущности получается сразу несколько. При этом классы получают сложную логику наследования.

В качестве примера посмотрим на переделанный код, построенный якобы «правильно»:

interface IProto {
	function out();
}
 
abstract class Out implements IProto {
	function out() {}
}
 
class A extends Out {
	function out()
	{
		echo 'Class A (out)';
	}
	
	function in()
	{
		echo 'Class A (in)';
	}
}
 
class B extends A {
	function out()
	{
		echo 'Class B (out)';
	}
}
PHP по сути тот же «Algol, только с классами». Сложная иерархия никак не способствует ни читабельности кода, ни его выполнению (компилятору нужно ещё разгрести все эти завалы связей).

Поскольку в PHP «ограниченный» полиморфизм, часто используются разные приёмы, которые призваны нивелировать такие неудобства. В качестве примера приведу код, показывающий как всё-таки можно получать данные разных типов через один.

interface IDraw {
	public function draw($text);
}
 
class Circle implements IDraw {	
	public function draw($text)
	{
		echo $text;
	}
}
 
class Square implements IDraw {
	public function draw($text)
	{
		echo $text;
	}
}
 
class Disp {
	public function get($class)
	{
		if (class_exists(ucfirst($class)))
			return new $class;
	}
}
 
$disp = new Disp();
 
$circle = $disp->get('Circle');
$square = $disp->get('Square');
 
$circle->draw('This is Circle');
$square->draw('This is Square');

Классы Circle и Square содержат конечную реализацию методов. В нашем случае это просто вывод текста. Оба класса реализуют интерфейс IDraw с той целью, чтобы их методы совпадали.

Класс Disp выполняет роль диспетчера и содержит метод get, который по входящему параметру ищет существующий класс и если есть, возвращает на него ссылку. Таким образом объекты $circle и $square можно получить через Disp, при том, что с ним нет никакой связи. Можно даже сделать Disp статическим, чтобы упростить его использование без new.

Данный алгоритм может использоваться например в роутинге, когда можно выделить обработчик запроса в разные классы. Скажем адрес сайт/about будет вызывать класс About, а сайт/contact — класс Contact.

$segment = $GET... ; // данные из URL
$page = $disp->get($segment);

Очевидно, что если необходимо будет «перехватить» новый адрес, например, news, то достаточно будет сделать лишь класс News, без правки существующего кода.

Класс Disp можно усложнить так, чтобы он сразу принимал не только класс реализации, но и выполнял его методы.

В простых случаях эмуляция полиморфизма реализуется на уровне метода/функции, где явно проверяется входящий тип данных. Приведенный выше пример из Паскаля на PHP можно реализовать примерно так:

function add($x, $y)
{
	if (is_string($x) and is_string($y))
	{
		return $x . $y;
	}
	elseif (is_numeric($x) and is_numeric($y))
	{
		return $x + $y;
	}
	else
	{
		return 0;
	}	
}
 
echo add(2, 6); // 8
echo add('Hello ', 'World!'); // Hello, World!

То есть PHP не позволяет создать две функции add(), поэтому входящий тип определяется уже внутри одной функции. На уровне классов используется похожий подход с использование instanceof (особенно для классов с общим интерфейсом).

Итого

Главная проблема использования ООП в PHP только в том, что многие решили, что php-код должен соответствовать принятым стандартам в других ООП-языках. Сам по себе язык PHP очень мощный и покрывает почти все потребности разработчиков. Там где можно спокойно обойтись без сложных классов имитирующих Java, лучше использовать более простой и понятный код в рамках базовых возможностей PHP.

Похожие записи
Комментарии (2) RSS
1 Алекс 2019-07-25 23:30:24

Интересно увидеть эту же статью с поправкой на последнюю версии PHP (на данный момент 7.3)


2 Admin 2019-07-26 08:12:45 admin

Так тоже самое будет. :-)