Все ленты — последние статьи

Объектно-ориентированное программирование в языке PHP

Автор — Кондраков А.В.
Введение

Данная статья рассчитана на начинающих разработчиков в области ООП. Я работаю с пятой версией РНР, поэтому и статья рассчитана на эту версию.

Первое, что необходимо понимать — класс это не набор функций или удобный контейнер для переменных, а абстрактный тип данных (АТД). Язык РНР не является строго типизированным языком, поэтому для начала необходимо разобраться с "простыми" типами. Целые числа (1, 45, 100, 378 и т.д.) имеют целочисленный тип, integer. Массивы — тоже тип данных. Более подробно с типами данных можно ознакомиться в документации — http://www.php.net/manual/ru/language.types.php. Класс также является типом данных, а объект — своеобразная переменная этого типа.

При создании класса чётко понять задачу, которую мы хотим представить. Часто построение класса является моделированием той сущности, которую необходимо перенести в код. Объект является отражением сущности, которая описана в виде класса. При моделировании класса стоит выявить те необходимые части сущности, над которыми будут производиться необходимые действия, с помощью методов. То есть необходимые части сущности являются полями класса, они как раз отражают данные, которые составляют общий тип данных. Этим тип объекта напоминает данные типа массив.

 

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

Например, класс пачки сигарет состоит из упаковки и массива самих сигарет, следует создавать методы, которые работают только с упаковкой и сигаретами. Нет необходимости использовать методы, с помощью которых мы открываем бутылку газировки или даже прикуриваем сигарету. Каждый класс должен реализовывать только те действия, которые работают с данными самого класса. Не надо пытаться засунуть в один класс всю реализацию целого приложения. Например, есть сайт, который состоит из главной страницы, гостевой книги, страницы новостей, раздела статей и ссылок на дружественные сайты. Опишите каждый раздел сайта своим классом или классами. Классы, отражающие разделы сайта, могут вступать в наследование друг с другом. Т.е. мы представляем раздел как сущность, отдельный тип данных. Снова повторю, что в классе, моделирующем раздел новостей, не надо определять элементы других разделов, если нет необходимости. Если требуется что-то представить из других разделов, используйте включение. Например, пусть на странице новостей требуется вывести заголовки последних статей. Определите в классе новостей поле типа класса статей и используйте методы этого типа. Не надо пытаться писать методы, выводящие заголовки статей в классе, моделирующем новости. Определите необходимый метод в соответствующем классе, классе, представляющем статьи.

Теперь приступим к конкретным примерам. Попробуем создать простейшую гостевую книгу.

Первое, что сделаем, это определим части необходимой нам сущности. Этими частями будут имя посетителя, его email и собственно его сообщение. Приступим.

<?
class GuestBook
{
private $name;
private $email;
private $msg;

public function __construct($name, $email, $msg)
{
$this->name = $name;
$this->email = $email;
$this->msg = $msg;
}
}
?>

Во-первых, мы объявили необходимые части нашего типа данных. Во-вторых, поля объявлены с модификатором доступ private, который означает, что с полями класса можно работать только методам этого же класса, но про модификаторы доступа будет рассказано позже. В-третьих, мы объявили метод __construct($name, $email, $msg). Этот метод является конструктором класса. О конструкторах и деструкторах будет рассказано немного позже, сейчас же запомните, что конструкторы всегда автоматически выполняются первыми, до всех остальных методов класса, непосредственно сразу после выделения памяти под объект. Деструкторы же выполняются последними, непосредственно перед уничтожением объекта.

В-четвёртых, мы использовали ключевое слово $this. $this означает ссылку на объект, для которого реализована та или иная конструкция. В отличие от других языков программирования, при обращении к членам класса, обязательно следует использовать $this: $this->name и $name — это две разные переменные.

Теперь подумаем над тем, как нам обращаться к полям объекта. Реализуем методы, которые вернут соответствующие значения.

<?
class GuestBook
{
private $name;
private $email;
private $msg;

public function __construct($name, $email, $msg)
{
$this->name = $name;
$this->email = $email;
$this->msg = $msg;
}

public function getName()
{
return $this->name;
}
public function getEmail()
{
return $this->email;
}
public function getMsg()
{
return $this->msg;
}
}
?>

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

При разработке гостевой книги неизбежно встаёт вопрос, как хранить данные. Воспользуемся для этого базой данных MySQL. Добавим новое поле типа DataBase — это специальный тип для работы с БД. В конструкторе проведем инициализацию. Создадим методы для получения данных из базы и наполнения базы. Метод чтения (SELECT) реализуем так:

<?php
public function Select()
{
$sql = "SELECT name, email, msg FROM guestbook";
$dbArray = $this->db->Db2Array();
foreach($dbArray as $rows)
{
$outPut[] = new GuestBook($rows['name'], $rows['email'], $rows['msg'], $this->db);
}
return $outPut;
}
?>

Метод добавления значений реализуем следующим образом:

<?php
public function Insert()
{
$sql = "INSERT INTO guestbook (name, email, msg) VALUES('{$this->name}', '{$this->email}', '{$this->msg}')";
if($this->db->Insert($sql) === TRUE)
return TRUE;
return FALSE;
}
?>

Первый метод возвращает массив объектов GuestBook, второй возвращает TRUE, если данные успешно добавлены, и FALSE — в противном случае.

В первой строке метода Select() cоздается SQL-запрос к базе на выборку всех значений.
Во второй строке из базы извлекается результирующая таблица в виде массива $dbArray. Как это происходит будет описано позже. В пятой строке мы создаем массив объектов типа GuestBook, который и возвращается в качестве результата. Напомню, чтобы обратиться к соответствующим полям мы реализовали соответствующие методы.

Для второго метода также создан SQL-запрос, но уже на добавление данных в базу (INSERT). Если запрос выполнен удачно, возвращаем истину (TRUE), иначе — ложь (FALSE).

Разрабатывая Web-приложение с использованием ООП, следует помнить о производительности, которая всегда ниже по сравнению с процедурным подходом. Метод Select, возвращающий значения каждый раз требует обращения к новому объекту типа DataBase. Если в базе хранится тысячи записей, это вызовет значительные накладные расходы — обработка каждой из записей потребует создания дополнительного объекта DataBase. Давайте придумаем выходы из подобной ситуации. Первое что приходит на ум, это не инициализировать поле типа DataBase, а написать соответствующий метод, который инициализирует поле $db.

<?php
public function setDb($db)
{
$this->db = $db;
}
?>

Напомним, что параметр $db — это экземпляр класса DataBase. Можно поступить и так:

<?php
public function setDb($db)
{
$this->db = new DataBase(‘login’,’password’,’host’,’database’); // Параметры — значения подключения к базе данных.
}
?>

Однако снова таки тысячу раз инициализировать объект типа DataBase не оптимально. Надо думать дальше :)
Практика показала, что более оптимально разбить подобный класс на два: в первом реализовать инициализацию сущности, а во второй объявить необходимые методы для работы с базой данных.

<?
class GuestBook
{
private $name;
private $email;
private $msg;

public function __construct($name, $email, $msg)
{
$this->name = $name;
$this->email = $email;
$this->msg = $msg;
}

public function getName()
{
return $this->name;
}
public function getEmail()
{
return $this->email;
}
public function getMsg()
{
return $this->msg;
}
}

class GuestBookDb
{
private $db;

public function __construct($db)
{
$this->db = $db;
}
public function Select()
{
$sql = "SELECT name, email, msg FROM guestbook";
$dbArray = $this->db->Db2Array();
foreach($dbArray as $rows)
{
$outPut[] = new GuestBook($rows['name'], $rows['email'], $rows['msg']);
}
return $outPut;
}
public function Insert($obj)
{
$name = $obj->getName();
$email = $obj->getEmail();
$msg = $obj->getMsg();

$sql = "INSERT INTO guestbook (name, email, msg) VALUES('$name', '$email', '$msg')";
if($this->db->Insert($sql) === TRUE)
return TRUE;
return FALSE;
}
}
?>

Что же изменилось: во-первых, мы получаем также массив объектов с помощью метода Select, но теперь он содержит только необходимые данные. Теперь на тысячу объектов GuestBook, создается только один объект GuestBookDb. В памяти не будет храниться тысячу раз данные типа DataBase. Во-вторых, немного изменился метод Insert(). У него появился параметр — объект типа GuestBook — это необходимо, чтобы занести данные в базу. В результате таких преобразований нагрузка на сервер немного снизится.
Основные понятия

В объектно-ориентированном программировании выделяют три основных элемента: инкапсуляция, наследование, полиморфизм. Статья не ставит своей целью всестороннее рассмотрение всех аспектов ООП. Здесь лишь кратко будет рассмотрена их суть.

Инкапсуляция. Инкапсуляция — это скрытие реализации. Для пользователей класса неважно как реализован класс, важено лишь какие методы доступны, т.е. какой интерфейс представляет класс. Мы уже дважды встречали инкапсуляцию. В первом случае мы объявили поля класса как закрытые (private), т.е. скрыли их от посторонних глаз. Методы также можно делать закрытыми (private), они не будут доступны для внешнего пользователя, однако их можно будет вызывать внути открытых (public) методов этого же класса. Закрытые методы необходимы для того, чтобы упростить открытый методы метод, разбив сложную задачу на несколько более простых. В первую очередь это необходимо для реорганизации кода, для более удобного его чтения и понимания. Во втором случае это также закрытое поле $db. Мы присвоили внутренней переменной соответствующий тип данных, скрыв не только реализацию, но и процесс создание объекта.
Замечание

Объект можно создать и внутри класса, например, в конструкторе.

Управление инкапсуляцие осуществляется при помощи модификаторов доступа — их три:

Public — поля и методы видны везде — в самом классе, в классе потомке, просто во внешнем участке кода, использующем класс.
Private — данные видны только в том классе, в котором они определены.
Protected — данные видны как в том классе, в котором они определены, так и в классе, который наследует первоначальный класс, однако для внешнего кода данные не доступны.

Наследование. В косвенной форме мы уже сталкивались со связью классов друг с другом, когда объявляли переменную типа DataBase. Мы неявно произвели связывание с помощью включения. Но что же такое наследование в более обычном его понимании? Под наследованием понимается расширение старого класса до нового путём расширения функциональности, т.е. новый класс будет содержать те же методы (которые, кстати, могут и изменить свою реализацию) и свойства прежнего класса.
Новый класс мы называем классом-потомком, а прежний, от которого происходит наследование, классом-предком или базовым классом. Наследование используется, в первую очередь, для построения иерархических систем, в котором классы-потомки развивают функциональность базовых классов. Наследование также может использоваться для изменения логики первоначальной реализации — т.е. для модификации уже существующего приложения.

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

<?
class SharedGuestBook extends GuestBook
{
private $url;
private $icq;

public function __construct($name, $email, $msg, $url, $icq)
{
parent :: __construct($name, $email, $msg);
$this->url = $url;
$this->icq = $icq;
}
public function getUrl()
{
return $this->url;
}
public function getIcq()
{
return $this->icq;
}
}
?>

Как видно из листинга, старые методы не нужно реализовывать — именно так достигается одна из важнейших задач объектно-ориентированного подхода — повторное использование кода.

Обратите внимание на реализацию конструктора — parent :: __construct($name, $email, $msg); — обратимся к официальной документации (http://www.php.net/manual/ru/language.oop5.paamayim-nekudotayim.php): Когда дочерний класс перегружает методы, объявленные в классе-родителе, PHP не будет осуществлять автоматический вызов методов, принадлежащих классу-родителю. Этот функционал возлагается на метод, перегружаемый в дочернем классе. Данное правило распространяется на [url = http://www.php.net/manual/ru/language.oop5.decon.php]конструкторы и деструкторы[/url], перегруженные и "магические" методы.

С помощью ключевого слова extends как раз и осуществляется наследование. Теперь настало время изменить класс GuestBookDb для работы с новым типом данных.

<?
class SharedGuestBookDb extends GuestBookDb
{
public function Select()
{
$sql = "SELECT name, email, msg, url, icq FROM new_guestbook";
$dbArray = $this->db->Db2Array();
foreach($dbArray as $rows)
{
$outPut[] = new SharedGuestBook($rows['name'], $rows['email'], $rows['msg'], $rows['url'], $rows['icq']);
}
return $outPut;
}
public function Insert($obj)
{
$name = $obj->getName();
$email = $obj->getEmail();
$msg = $obj->getMsg();
$url = $obj->getUrl();
$icq = $obj->getIcq();

$sql = "INSERT INTO new_guestbook (name, email, msg, url, icq) VALUES('$name', '$email', '$msg', '$url', '$icq')";
if($this->db->Insert($sql) === TRUE)
return TRUE;
return FALSE;
}
}
?>

В данном случае нет необходимости перегружать конструктор, т.к. он остался неизменным. Мы просто переопределили необходимые методы — изменили их функционал, оставив их предыдущие названия, а это уже полиморфизмом пахнет :)
При наследовании мы можем переопределить любой метод, но иногда нам надо запретить переопределение методов, оставив первоначальную реализацию. Для подобного случая необходимо использовать ключевое слово final. Рассмотрим пример.

<?
class SomeClass
{
private $var;

final public function PrintVar()
{
echo $var;
}
public function SetVar()
{
$this->var = "From SomeClass";
}
}
class SomeClassNew extends SomeClass
{
public function SetVar()
{
$this->var = "From SomeClassNew";
}
}
?>

Если мы бы начали переопределять метод PrintVar(), то возникла бы ошибка.
Также мы можем объявлять и классы с ключевым словом final, если будет необходимость запретить наследование от данного класса.

Полиморфизм. Полиморфизмом называется возможность применения методов с одинаковыми именами в группе классов, связанных наследованием.
Конструкторы и деструкторы

Конструктор. Мы уже немного знакомы с конструкторами. Давайте познакомимся с ними поближе. И так, конструкторы, в сущности являющиеся специальными методами, вызываются каждый раз при создании объекта класса, в котором определен конструктор. В классе может и не быть конструктора, а также может быть только один конструктор. Обычно конструктор используют для инициализации какого-либо поля класса, т.е. для задания первоначального состояния объекта, например, с помощью вызова некоторых методов, как правило, приватных. Если конструктор содержит параметры, то и объект должен быть вызван с соответствующими параметрами. Естественно, если мы объявим метод с параметрами, то и вызвать его должны с тем же количеством параметров, а, как уже говорилось выше, конструктор — специальный метод, который вызывается при создании объекта. Поэтому и объект мы должны создавать с параметрами. Давайте создадим объект для нашего класса GuestBook

<?
$guest_book = new GuestBook($name, $email, $msg);
?>

Если бы не было конструктора или конструктор не содержал бы параметров, то мы создавали бы объект так:

<?
$var = new SomeClass();
?>

Нам также известно из предыдущих примеров как вызывать конструкторы, объявленные в родительских классах — parent :: __construct();

Деструктор. Деструктор вызывается при уничтожении объекта. Деструктор не может содержать параметры. Как и конструктор, деструктор тоже специальный метод со специальным именем __destruct(). Обычно деструктор используется для очищения памяти от различного мусора: закрытие соединения с базой данных, закрытие файла, удаление ненужных переменных и т.д. Для вызова деструктора класса-предка, по аналогии с конструктором, — parent :: __destruct();
Моделирование на более высоком уровне абстракции. Абстрактные классы и интерфейсы.

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

Если в классе объявлены абстрактные методы, то и класс тоже должен быть объявлен как абстрактный. Абстрактный класс может содержать и обычные методы и поля. Нельзя создавать объект абстрактного класса, но его нужно переопределять классами потомками. Т.е. абстрактные классы служат прототипами классов, которые наследуют их основу. Мы безоговорочно должны принять правила, диктуемые нам абстрактными классами. Если мы объявили абстрактный метод с двумя параметрами, то в последующих классах мы должны реализовать одноименный метод именно с тем же числом параметров.

Создадим абстрактный класс для класса GuestBookDb, а класс SharedGuestBookDb мы можем унаследовать как от абстрактного класса, так и от класса GuestBookDb.

<?
abstract class GbDb
{
private $db;

public function __construct($db)
{
$this->db = $db;
}

abstract public function Select();
abstract public function Insert($obj);
}

class GuestBookDb extends GbDb
{
public function Select()
{
// реализация
}
public function Insert($obj)
{
// реализация
}
}
class SgbDb extends GuestBookDb
{
public function Select()
{
// реализация
}
public function Insert($obj)
{
// реализация
}
}
?>

Абстрактные классы выгодны тогда, когда в них содержится менее абстрактная реализация, например, как в нашем примере. А если нам необходима только абстракция? И нужно унаследовать класс от нескольких абстрактных сущностей, мы же помним, что наследовать мы можем только от одного класса? И на сцену выходят интерфейсы. В интерфейсе можно объявлять без спецификаторов, в т.ч. и abstract. В интерфейсе можно определять только абстракцию действий, т.е. методы, которые могут быть только публичными. Действительно, мы же абстрагируем видимую реализацию. Нам надо показать не только себе, но и другим разработчикам работу нашего класса. Давайте рассмотрим интерфейс на практике. Немного изменим наш предыдущий код.

<?
interface GbDb
{
function Select();
function Insert($obj);
}

class GuestBookDb implements GbDb
{
public function Select()
{
// реализация
}
public function Insert($obj)
{
// реализация
}
}
class SgbDb extends GuestBookDb
{
public function Select()
{
// реализация
}
public function Insert($obj)
{
// реализация
}
}
?>

Мы можем наследовать сразу несколько интерфейсов. Усовершенствуем нашу гостевую, добавим методы обновления базы данных и выборки определенной записи по конкретному идентификационному номеру.

<?
interface GbDb
{
function Select();
function Insert($obj);
}
interface GbDbNew
{
function Update($id, $obj);
function SelectWithId($id);
}
class GuestBookDb implements GbDb, GbDbNew
{
public function Select()
{
// реализация
}
public function Insert($obj)
{
// реализация
}
public function Update($id, $obj)
{
// реализация
}
public function SelectWithId($id)
{
// реализация
}
}
class SgbDb extends GuestBookDb
{
public function Select()
{
// реализация
}
public function Insert($obj)
{
// реализация
}
public function Update($id, $obj)
{
// реализация
}
public function SelectWithId($id)
{
// реализация
}
}
?>

Мы можем наследовать одновременно как класс, так и интерфейс. При этом класс наследуется первым. Один интерфейс может наследовать другой интерфейс применяя ключевое слово extends.

Вот мы и рассмотрели некоторые особенности объектно-ориентированного подхода написания сценариев на языке РНР. Мы не затронули некоторые темы, например, статические элементы класса, обработку исключений. Когда вы научитесь проектировать классы и писать ОО код, то и эти темы будут вам более понятны.

Вот несколько тем форума, в которых обсуждается объектно-ориентированный подход в PHP