Объектно-ориентированное программирование в языке PHPИсточник: softtime Кондраков А.В.
ВведениеДанная статья рассчитана на начинающих разработчиков в области ООП. Я работаю с пятой версией РНР, поэтому и статья рассчитана на эту версию. Первое, что необходимо понимать - класс это не набор функций или удобный контейнер для переменных, а абстрактный тип данных (АТД). Язык РНР не является строго типизированным языком, поэтому для начала необходимо разобраться с "простыми" типами. Целые числа (1, 45, 100, 378 и т.д.) имеют целочисленный тип, integer. Массивы - тоже тип данных. Более подробно с типами данных можно ознакомиться в документации - http://www.php.net/manual/ru/language.types.php. Класс также является типом данных, а объект - своеобразная переменная этого типа. При создании класса чётко понять задачу, которую мы хотим представить. Часто построение класса является моделированием той сущности, которую необходимо перенести в код. Объект является отражением сущности, которая описана в виде класса. При моделировании класса стоит выявить те необходимые части сущности, над которыми будут производиться необходимые действия, с помощью методов. То есть необходимые части сущности являются полями класса, они как раз отражают данные, которые составляют общий тип данных. Этим тип объекта напоминает данные типа массив. Методы выполняют разнообразные действия над данными. Методы следует проектировать так, чтобы они работали только с теми данными, которые определены в классе. Не рекомендуется определять методы, которые как-либо влияют на внешние данные. Так называемая стратегия слабого связывания - чем меньше связей между классами и внешними данными - тем проще извлечь класс из системы и повторно использовать его вновь. Например, класс пачки сигарет состоит из упаковки и массива самих сигарет, следует создавать методы, которые работают только с упаковкой и сигаретами. Нет необходимости использовать методы, с помощью которых мы открываем бутылку газировки или даже прикуриваем сигарету. Каждый класс должен реализовывать только те действия, которые работают с данными самого класса. Не надо пытаться засунуть в один класс всю реализацию целого приложения. Например, есть сайт, который состоит из главной страницы, гостевой книги, страницы новостей, раздела статей и ссылок на дружественные сайты. Опишите каждый раздел сайта своим классом или классами. Классы, отражающие разделы сайта, могут вступать в наследование друг с другом. Т.е. мы представляем раздел как сущность, отдельный тип данных. Снова повторю, что в классе, моделирующем раздел новостей, не надо определять элементы других разделов, если нет необходимости. Если требуется что-то представить из других разделов, используйте включение. Например, пусть на странице новостей требуется вывести заголовки последних статей. Определите в классе новостей поле типа класса статей и используйте методы этого типа. Не надо пытаться писать методы, выводящие заголовки статей в классе, моделирующем новости. Определите необходимый метод в соответствующем классе, классе, представляющем статьи. Теперь приступим к конкретным примерам. Попробуем создать простейшую гостевую книгу. Первое, что сделаем, это определим части необходимой нам сущности. Этими частями будут имя посетителя, его email и собственно его сообщение. Приступим.
Во-первых, мы объявили необходимые части нашего типа данных. Во-вторых, поля объявлены с модификатором доступ private, который означает, что с полями класса можно работать только методам этого же класса, но про модификаторы доступа будет рассказано позже. В-третьих, мы объявили метод __construct($name, $email, $msg). Этот метод является конструктором класса. О конструкторах и деструкторах будет рассказано немного позже, сейчас же запомните, что конструкторы всегда автоматически выполняются первыми, до всех остальных методов класса, непосредственно сразу после выделения памяти под объект. Деструкторы же выполняются последними, непосредственно перед уничтожением объекта. В-четвёртых, мы использовали ключевое слово $this. $this означает ссылку на объект, для которого реализована та или иная конструкция. В отличие от других языков программирования, при обращении к членам класса, обязательно следует использовать $this: $this->name и $name - это две разные переменные. Теперь подумаем над тем, как нам обращаться к полям объекта. Реализуем методы, которые вернут соответствующие значения.
Теперь данные класса можно читать из вне. При разработке гостевой книги неизбежно встаёт вопрос, как хранить данные. Воспользуемся для этого базой данных MySQL. Добавим новое поле типа DataBase - это специальный тип для работы с БД. В конструкторе проведем инициализацию. Создадим методы для получения данных из базы и наполнения базы. Метод чтения (SELECT) реализуем так:
Метод добавления значений реализуем следующим образом:
Первый метод возвращает массив объектов GuestBook, второй возвращает TRUE, если данные успешно добавлены, и FALSE - в противном случае. В первой строке метода Select() cоздается SQL-запрос к базе на выборку всех значений. Для второго метода также создан SQL-запрос, но уже на добавление данных в базу (INSERT). Если запрос выполнен удачно, возвращаем истину (TRUE), иначе - ложь (FALSE). Разрабатывая Web-приложение с использованием ООП, следует помнить о производительности, которая всегда ниже по сравнению с процедурным подходом. Метод Select, возвращающий значения каждый раз требует обращения к новому объекту типа DataBase. Если в базе хранится тысячи записей, это вызовет значительные накладные расходы - обработка каждой из записей потребует создания дополнительного объекта DataBase. Давайте придумаем выходы из подобной ситуации. Первое что приходит на ум, это не инициализировать поле типа DataBase, а написать соответствующий метод, который инициализирует поле $db.
Напомним, что параметр $db - это экземпляр класса DataBase. Можно поступить и так:
Однако снова таки тысячу раз инициализировать объект типа DataBase не оптимально. Надо думать дальше :) class GuestBookDb Что же изменилось: во-первых, мы получаем также массив объектов с помощью метода Select, но теперь он содержит только необходимые данные. Теперь на тысячу объектов GuestBook, создается только один объект GuestBookDb. В памяти не будет храниться тысячу раз данные типа DataBase. Во-вторых, немного изменился метод Insert(). У него появился параметр - объект типа GuestBook - это необходимо, чтобы занести данные в базу. В результате таких преобразований нагрузка на сервер немного снизится. Основные понятияВ объектно-ориентированном программировании выделяют три основных элемента: инкапсуляция, наследование, полиморфизм. Статья не ставит своей целью всестороннее рассмотрение всех аспектов ООП. Здесь лишь кратко будет рассмотрена их суть. Инкапсуляция. Инкапсуляция - это скрытие реализации. Для пользователей класса неважно как реализован класс, важено лишь какие методы доступны, т.е. какой интерфейс представляет класс. Мы уже дважды встречали инкапсуляцию. В первом случае мы объявили поля класса как закрытые (private), т.е. скрыли их от посторонних глаз. Методы также можно делать закрытыми (private), они не будут доступны для внешнего пользователя, однако их можно будет вызывать внути открытых (public) методов этого же класса. Закрытые методы необходимы для того, чтобы упростить открытый методы метод, разбив сложную задачу на несколько более простых. В первую очередь это необходимо для реорганизации кода, для более удобного его чтения и понимания. Во втором случае это также закрытое поле $db. Мы присвоили внутренней переменной соответствующий тип данных, скрыв не только реализацию, но и процесс создание объекта. ЗамечаниеОбъект можно создать и внутри класса, например, в конструкторе. Управление инкапсуляцие осуществляется при помощи модификаторов доступа - их три:
Наследование. В косвенной форме мы уже сталкивались со связью классов друг с другом, когда объявляли переменную типа DataBase. Мы неявно произвели связывание с помощью включения. Но что же такое наследование в более обычном его понимании? Под наследованием понимается расширение старого класса до нового путём расширения функциональности, т.е. новый класс будет содержать те же методы (которые, кстати, могут и изменить свою реализацию) и свойства прежнего класса. Рассмотрим пример. Пусть необходимо расширить перечень параметров, которые может ввести пользователь в гостевой книге. Пусть помимо имени, email и сообщения пользователи получают возможность вводить адрес Web-сайта и номер ICQ. Для этого не требуется создавать новый класс, мы просто наследуем уже готовую реализацию от класса GuestBook.
Как видно из листинга, старые методы не нужно реализовывать - именно так достигается одна из важнейших задач объектно-ориентированного подхода - повторное использование кода. Обратите внимание на реализацию конструктора - 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 для работы с новым типом данных.
В данном случае нет необходимости перегружать конструктор, т.к. он остался неизменным. Мы просто переопределили необходимые методы - изменили их функционал, оставив их предыдущие названия, а это уже полиморфизмом пахнет :)
Если мы бы начали переопределять метод PrintVar(), то возникла бы ошибка. Полиморфизм. Полиморфизмом называется возможность применения методов с одинаковыми именами в группе классов, связанных наследованием. Конструкторы и деструкторыКонструктор. Мы уже немного знакомы с конструкторами. Давайте познакомимся с ними поближе. И так, конструкторы, в сущности являющиеся специальными методами, вызываются каждый раз при создании объекта класса, в котором определен конструктор. В классе может и не быть конструктора, а также может быть только один конструктор. Обычно конструктор используют для инициализации какого-либо поля класса, т.е. для задания первоначального состояния объекта, например, с помощью вызова некоторых методов, как правило, приватных. Если конструктор содержит параметры, то и объект должен быть вызван с соответствующими параметрами. Естественно, если мы объявим метод с параметрами, то и вызвать его должны с тем же количеством параметров, а, как уже говорилось выше, конструктор - специальный метод, который вызывается при создании объекта. Поэтому и объект мы должны создавать с параметрами. Давайте создадим объект для нашего класса GuestBook
Если бы не было конструктора или конструктор не содержал бы параметров, то мы создавали бы объект так:
Нам также известно из предыдущих примеров как вызывать конструкторы, объявленные в родительских классах - parent :: __construct(); Деструктор. Деструктор вызывается при уничтожении объекта. Деструктор не может содержать параметры. Как и конструктор, деструктор тоже специальный метод со специальным именем __destruct(). Обычно деструктор используется для очищения памяти от различного мусора: закрытие соединения с базой данных, закрытие файла, удаление ненужных переменных и т.д. Для вызова деструктора класса-предка, по аналогии с конструктором, - parent :: __destruct(); Моделирование на более высоком уровне абстракции. Абстрактные классы и интерфейсы.Архитектор начинает свой проект с макета здания, с чертежа. Программист же начинает проект с абстрактной модели. Мы уже моделировали гостевую книгу, но на наименьшем уровне абстракции. Вспомним, что класс является абстрактным типом данных, поэтому абстракция имеет достаточно важное значение. Мы проектируем сущность, но мы еще не знаем или пока не хотим затрагивать реализацию, поэтому мы только делаем макеты методов, которые будут работать с данными. На первоначальном этапе нам надо только описать функциональность класса, оставляя детали реализации. Нам просто надо понять то, что мы хотим сделать над теми данными, которые мы включили в наш тип. Также мы заботимся о тех программистах, которые будут или разрабатывать логику методов, или использовать наши будущие методы, или как-то иначе использовать наш тип данных. Для организации вышесказанного на арену выходят абстрактные классы, их абстрактные методы, а также интерфейсы. Если в классе объявлены абстрактные методы, то и класс тоже должен быть объявлен как абстрактный. Абстрактный класс может содержать и обычные методы и поля. Нельзя создавать объект абстрактного класса, но его нужно переопределять классами потомками. Т.е. абстрактные классы служат прототипами классов, которые наследуют их основу. Мы безоговорочно должны принять правила, диктуемые нам абстрактными классами. Если мы объявили абстрактный метод с двумя параметрами, то в последующих классах мы должны реализовать одноименный метод именно с тем же числом параметров. Создадим абстрактный класс для класса GuestBookDb, а класс SharedGuestBookDb мы можем унаследовать как от абстрактного класса, так и от класса GuestBookDb. class GuestBookDb extends GbDb Абстрактные классы выгодны тогда, когда в них содержится менее абстрактная реализация, например, как в нашем примере. А если нам необходима только абстракция? И нужно унаследовать класс от нескольких абстрактных сущностей, мы же помним, что наследовать мы можем только от одного класса? И на сцену выходят интерфейсы. В интерфейсе можно объявлять без спецификаторов, в т.ч. и abstract. В интерфейсе можно определять только абстракцию действий, т.е. методы, которые могут быть только публичными. Действительно, мы же абстрагируем видимую реализацию. Нам надо показать не только себе, но и другим разработчикам работу нашего класса. Давайте рассмотрим интерфейс на практике. Немного изменим наш предыдущий код. class GuestBookDb implements GbDb Мы можем наследовать сразу несколько интерфейсов. Усовершенствуем нашу гостевую, добавим методы обновления базы данных и выборки определенной записи по конкретному идентификационному номеру.
Мы можем наследовать одновременно как класс, так и интерфейс. При этом класс наследуется первым. Один интерфейс может наследовать другой интерфейс применяя ключевое слово extends. Вот мы и рассмотрели некоторые особенности объектно-ориентированного подхода написания сценариев на языке РНР. Мы не затронули некоторые темы, например, статические элементы класса, обработку исключений. Когда вы научитесь проектировать классы и писать ОО код, то и эти темы будут вам более понятны. |