Основы объектно-ориентированного программирования
Эта статья адресована читателям, не знакомым с базовыми концепциями объектно-ориентированного программирования (ООП). Мы хотим дать краткий обзор ООП с акцентом на эффективное применение в контексте РНР. Обсуждение ограничивается немногими основными идеями ООП - в той мере, в какой они имеют отношение к РНР, - хотя временами полезно бывает взглянуть и на другие объектно-ориентированные языки, например Java или С++.
В этой статье мы обсудим три аспекта объектной ориентированности: класс, модификаторы доступа и наследование. Хотя ООП является иной парадигмой, но во многих отношениях это расширение процедурного программирования, поэтому там, где это будет способствовать лучшему пониманию, я приведу примеры из области процедурного программирования. В последующих статьях мы еще вернемся к затронутым здесь темам и поясним их на конкретных примерах.
Класс
ООП невозможно без объектов, и это именно то, что составляет суть классов. На самом простом уровне класс - это тип данных. Но в отличие от примитивных типов, таких как целое число, число с плавающей точкой и символ, класс представляет собой сложный, определенный пользователем тип. Класс аналогичен записи в базе данных - в том смысле, что он инкапсулирует характеристики объекта. Например, запись типа Person может содержать дату рождения, адрес, фамилию и номер телефона. Класс - это тип, составленный из других типов, которые в совокупности описывают объект.
Классы и записи
Хотя класс во многом подобен записи, между ними существует важное отличие: класс может содержать не только данные, но и функции. А когда частью типа данных оказывается функция, процедурное программирование буквально становится с ног на голову, как видно из следующего примера, демонстрирующего синтаксическую нотацию. Обычный вызов функции:
function_call($somevariable);
$somevariable->function_call();
Важная особенность состоит в том, что в ООП не над переменными производится какая-то операция, а сами переменные выполняют операции. Они являются активными, а не пассивными сущностями, поэтому говорят, что они обладают поведением. Поведение класса - это совокупность его функций.
Связное целое
В процедурном программировании мы часто имеем дело с библиотеками функций. Обычно в одной библиотеке находятся функции, имеющие общее назначение. Например, все функции, относящиеся к работе с базой данных, можно поместить в файл dbfunctions. inc. Функции, составляющие поведение класса, тоже должны быть связаны между собой, но эта связь гораздо сильнее, чем в случае библиотеки. Как разные элементы записи Record описывают конкретного человека, так и поведение класса должно описывать класс. Чтобы некую сущность можно было назвать объектом, она должна быть связным целым, включающим как характеристики, так и поведение.
Объекты - это экземпляры
Сам по себе класс не является объектом, зато дает способ порождения объектов; это в некотором роде чертеж или шаблон, по которому создается объект. В разговорной речи эти два термина иногда взаимозаменяемы, но, строго говоря, объект - это экземпляр класса. Чем-то это напоминает разницу между понятием целого числа и переменной $х, которая в каждый момент времени принимает конкретное значение. Концепция класса как шаблона для порождения объектов становится яснее в контексте наследования, особенно множественного (скоро мы коснемся этой темы).
Объектам нужны модификаторы доступа
ООП можно себе представить, имея только простую концепцию класса как связного агрегата с характеристиками и поведением. Но одна из важнейших особенностей любого объектно-ориентированного языка программирования - использование модификаторов доступа. Модификаторы доступа уточняют модель объекта, контролируя, как именно его можно использовать. Проще говоря, модификаторы доступа определяют, что можно, а чего нельзя делать с объектом. Чтобы разобраться в этом, обратимся к примеру из процедурного программирования.
Назовем подпрограммой функцию, которая никогда не вызывается напрямую, а лишь из других функций. Предположим, что вы пишете библиотеку функций и подпрограмм, используемую несколькими другими программистами. В этом случае была бы полезна возможность помечать подпрограммы как вспомогательные функции, чтобы оградить пользователей от потенциально неправильного применения библиотеки, но добиться этого можно только путем документирования. В ООП же модификаторы доступа не просто обозначают степень важности функций, но и препятствуют их вызову из внешней программы. Тем самым реализуется языковое ограничение, запрещающее вызывать подпрограммы напрямую. Правильно спроектированные классы оказываются самодокументированными и самоконтролируемыми.
В описанной ситуации необходимость документации диктуется наличием коллектива программистов, работающих с библиотекой. И этим же обусловлено появление модификаторов доступа. Одно из допущений, лежащих в основе ООП, состоит в том, что программирование происходит в условиях взаимодействия, а модификаторы доступа определяют способы такого взаимодействия. Это одно из существенных отличий между процедурным и объектно-ориентированным программированием. Модификаторы доступа задают правила работы с классом, этот синтаксически определенный «этикет» часто называют интерфейсом. Предоставив интерфейс, уже не обязательно рассчитывать на то, что «правильное» использование будет описано в документации.
Документировать библиотеки важно потому, что они по сути своей предназначены для повторного использования. Модификаторы доступа существуют по той же причине - чтобы облегчить повторное использование.
Повторное использование и наследование
В биологическом смысле ребенок наследует гены от родителей, и полученный генетический материал определяет его внешность и поведение. В ООП смысл наследования аналогичен и состоит в механизме передачи характеристик и свойств. На первый взгляд такая возможность ООП сродни черной магии, но в действительности наследование - всего лишь техника повторного использования кода, подобно включению библиотек функций в процедурном программировании.
Если вы нашли класс, точно отвечающий вашим потребностям, то можете просто воспользоваться им, получив все необходимое от уже реализованного поведения. Наследование вступает в игру, когда класс делает не совсем то, что вам нужно; Такая ситуация мало чем отличается от добавления новых функций в библиотеку. Наследование позволяет как воспользоваться имеющимся поведением, так и дополнить его новыми возможностями. Например, если вам нужен класс Blue jay (Голубая сойка), то можете унаследовать существующий класс Bird (Птица) и модифицировать его с учетом предъявляемых требований.
Когда один класс оказывается основой для другого, как Bird для Blue jay, говорят, что исходный класс является базовым (или родителем), а построенный на его основе - производным (или потомком).
Множественное наследование
В природе множественное наследований - естественное дело, но в объектно- ориентированном РНР у каждого класса может быть только один родитель. Создатели РНР отказались от идеи множественного наследования классов. Чтобы понять почему, снова воспользуемся классом Bird и на его примере покажем, что такое множественное наследование и к каким проблемам оно может привести. Если вам нужен класс Whooping crane (американский журавль), то естественно произвести его от класса Bird. Предположим, что есть еще класс Endangered species (виды, находящиеся под угрозой исчезновения). Множественное наследование позволило бы унаследовать Whooping crane от обоих классов. Прекрасная идея - но до тех пор, пока вы не осознали, что в том и в другом определено поведение, описывающее потребление пищи. И какое из них предпочесть? Возникновение подобных неоднозначностей и является недостатком множественного наследования. При одиночном наследовании такого не бывает.
Как сесть на ежа и не уколоться?
Одиночное наследование - более простой и естественный подход, но все же бывают ситуации, когда хотелось бы объединить поведения, определенные в разных классах. Американский журавль - одновременно и птица и вид, находящийся под угрозой исчезновения. Не имеет смысла заново создавать один из этих классов каждый раз, когда нужна их комбинация. Есть ли все-таки способ объединить два класса и обойти проблему перекрывающегося поведения?
В РНР эта задача решается путем введения понятия интерфейса. В этом контексте интерфейс означает класс без членов-данных, то есть составленной лишь из функций, не имеющих реализации (то есть прототипы функций заданы, а их тела не определены). Любой класс, наследующий интерфейсу, должен реализовать тела отсутствующих функций. Если бы Endangered species был интерфейсом, а не классом, то наличие нескольких функций eating не имело бы значения. Определение этого метода в классе Bird стало бы реализацией функции интерфейса, и тем самым проблема многократного определения одной и той же функции не возникла бы.
Класс может наследовать только одному классу и любому числу интерфейсов, так как у интерфейсов нет реализации. Не отступаясь от принципов РНР, интерфейсы вносят свой вклад в дело конструирования мощного и гибкого языка программирования.
Интерфейсы можно характеризовать словом абстрактный, так как от программиста требуется их реализовать. Будучи абстрактными, интерфейсы заслуживают называться шаблонами даже в большей степени, чем классы. В отличие от классов, они никогда не бывают «готовыми», а имеют смысл лишь в контексте наследования. Поскольку интерфейс не имеет реализации, то он может лишь выступать в роли модели для создания производного класса.
Что дальше?
Мы коснулись трех тем, основополагающих для ООП: классы, модификаторы доступа и наследование. Классы определяют объекты, модификаторы доступа описывают, как можно пользоваться этими объектами, а наследование упрощает адаптацию классов к конкретным обстоятельствам. Я отметил сходство между процедурным и объектно-ориентированным программированием, в силу чего переход к ООП оказывается легче, но вместе с тем подчеркнул важные различия. Классу как типу данных, включающему функции, нет аналогов в процедурном программировании. Кроме того, в ООП имеются модификаторы доступа, определяющие возможные способы работы с объектом. Чтобы не приходилось полагаться на документацию и дисциплину, в ООП ограничения встроены в сам язык.
Автор: Питер Ловэйн. Переводчик: А. Слинкин