Ремонт РЭА

Объектно-ориентированное программирование для начинающих. ООП - это что такое? Основные принципы объектно-ориентированного программирования Основные свойства объектно ориентированного программирования

Концепция объектно-ориентированного программирования (ООП) появилась более сорока лет назад, как развитие идей процедурного программирования. Идеология процедурного программирования, ничего особенного собой не представляет: все программы представлены набором процедур и функций, в то время как сами процедуры и функции – это последовательности операторов, выполняя которые компьютер модифицирует значения переменных в памяти. Основная программа в процедурном программировании также является процедурой (функцией), в теле которой могут быть вызовы других процедур и функций – подпрограмм. Суть процедурного программирования проста: данные отдельно, поведение отдельно. То из чего состоит процедурный язык программирования (какие конструкции в него входят), я постарался собрать в отдельном разделе. Разделение кода на подпрограммы, во-первых, позволяет выделить повторно используемые фрагменты кода, а во-вторых, делает код программы структурированным.

Идеология объектно-ориентированного программирования, как следует из самого названия, строится вокруг понятия объект. Объект объединяет в себе и данные и поведение. Объект – это любая сущность, с которой имеет дело программа, а именно: объекты предметной области, моделируемые программой; ресурсы операционной системы; сетевые протоколы и многое другое. По сути, объект – это та же структура (составной тип), но дополненная процедурами и функциями, управляющими элементами этой структуры. К примеру, для работы с файлом в процедурном языке программирования отдельно была бы создана переменная для хранения имени файла и отдельно – для хранения его дескриптора (уникальный идентификатор ресурса в операционной системе), а также ряд процедур работы с файлом: открыть файл, прочитать данные из файла и закрыть файл. Все бы эти процедуры, помимо обычных параметров и переменных для хранения результата, обязаны были бы принимать тот самый дескриптор, чтобы понять, о каком именно файле идет речь. В объектно-ориентированном языке для этих же целей был бы описан объект-файл, который также бы хранил внутри себя имя и дескриптор и предоставлял бы пользователю процедуры для открытия, чтения и закрытия себя самого (файла, ассоциированного с конкретным объектом). Разница была бы в том, что дескриптор был бы скрыт от остальной части программы, создавался бы в коде процедуры открытия файла и использовался бы неявно только самим объектом. Таким образом, пользователю объекта (программному коду внешней по отношению к объекту программы) не нужно было бы передавать дескриптор каждый раз в параметрах процедур. Объект – это комплект данных и методов работы с этими данными, часть из которых может быть скрыта от окружающего его мира, к которой и относятся детали реализации. Более подробно о терминологии объектно-ориентированного программирования будет рассказано далее.

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

Терминология объектно-ориентированного программирования

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

  • Класс – тип данных, описывающий структуру и поведение объектов.
  • Объект – экземпляр класса.
  • Поле – элемент данных класса: переменная элементарного типа, структура или другой класс, являющийся частью класса.
  • Состояние объекта – набор текущих значений полей объекта.
  • Метод – процедура или функция, выполняющаяся в контексте объекта, для которого она вызывается. Методы могут изменять состояние текущего объекта или состояния объектов, передаваемых им в качества параметров.
  • Свойство – специальный вид методов, предназначенный для модификации отдельных полей объекта. Имена свойств обычно совпадают с именами соответствующих полей. Внешне работа со свойствами выглядит точно так же, как работа с полями структуры или класса, но на самом деле перед тем, как вернуть или присвоить новое значение полю может быть выполнен программный код, осуществляющий разного рода проверки, к примеру, проверку на допустимость нового значения.
  • Член класса – поля, методы и свойства класса.
  • Модификатор доступа – дополнительная характеристика членов класса, определяющая, имеется ли к ним доступ из внешней программы, или же они используются исключительно в границах класса и скрыты от окружающего мира. Модификаторы доступа разделяют все элементы класса на детали реализации и открытый или частично открытый интерфейс.
  • Конструктор – специальный метод, выполняемый сразу же после создания экземпляра класса. Конструктор инициализирует поля объекта – приводит объект в начальное состояние. Конструкторы могут быть как с параметрами, так и без. Конструктор без параметров называют конструктором по умолчанию, который может быть только один. Имя метода конструктора, чаще всего, совпадает с именем самого класса.
  • Деструктор – специальный метод, вызываемый средой исполнения программы в момент, когда объект удаляется из оперативной памяти. Деструктор используется в тех случаях, когда в состав класса входят ресурсы, требующие явного освобождения (файлы, соединения с базами данных, сетевые соединения и т.п.)
  • Интерфейс – набор методов и свойств объекта, находящихся в открытом доступе и призванных решать определенный круг задач, к примеру, интерфейс формирования графического представления объекта на экране или интерфейс сохранения состояния объекта в файле или базе данных.
  • Статический член – любой элемент класса, который может быть использован без создания соответствующего объекта. К примеру, если метод класса не использует ни одного поля, а работает исключительно с переданными ему параметрами, то ничто не мешает его использовать в контексте всего класса, не создавая отдельных его экземпляров. Константы в контексте класса обычно всегда являются статическими его членами.

Преимущества объектно-ориентированного программирования

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

  • Инкапсуляция обозначает сокрытие деталей реализации классов средствами награждения отдельных его членов соответствующими модификаторами доступа. Таким образом, вся функциональность объекта, нацеленная на взаимодействие с другими объектами программы группируется в открытый интерфейс, а детали тщательно скрываются внутри, что избавляет основной код бизнес-логики информационной системы от ненужных ему вещей. Инкапсуляция повышает надежность работы программного кода, поскольку гарантирует, что определенные данные не могут быть изменены за пределами содержащего их класса.
  • Наследование . Краеугольный камень ООП. В объектно-ориентированном программировании есть возможность наследовать структуру и поведение класса от другого класса. Класс, от которого наследуют, называется базовым или суперклассом, а класс, который получается вследствие наследования – производным или просто потомком. Любой класс может выступать как в роли суперкласса, так и в роли потомка. Связи наследования классов образуют иерархию классов. Множественным наследованием называют определение производного класса сразу от нескольких суперклассов. Не все объектно-ориентированные языки программирования поддерживают множественное наследование. Наследование – это эффективный способ выделения многократно используемых фрагментов кода, но у него есть и минусы, о которых будет рассказано далее.
  • Абстрагирование . Возможность объединять классы в отдельные группы, выделяя общие, значимые для них всех характеристики (общие поля и общее поведение). Собственно, абстрагирование и есть следствие наследования: базовые классы не всегда имеют свою проекцию на объекты реального мира, а создаются исключительно с целью выделить общие черты целой группы объектов. К примеру, объект мебель – это базовое понятие для стола, стула и дивана, всех их объединяет то, что это движимое имущество, часть интерьера помещений, и они могут быть выполнены для дома или офиса, а также относиться к “эконом” или “премиум” классу. В ООП есть для этого отдельное понятие абстрактный класс – класс, объекты которого создавать запрещено, но можно использовать в качестве базового класса. Наследование и абстрагирование позволяют описывать структуры данных программы и связи между ними точно так же, как выглядят соответствующие им объекты в рассматриваемой модели предметной области.

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

Диаграмма классов или иерархия наследования "Транспортные средства". Белые квадраты обозначают абстрактные классы.

  • Полиморфизм . Еще одно свойство, которое является следствием наследования. Дело в том, что объектно-ориентированные языки программирования позволяют работать с набором объектов из одной иерархии точно так же, как если бы все они были объектами их базового класса. Если вернуться к примеру про мебель, то можно предположить, что в контексте создания информационной системы для мебельного магазина в базовый класс для всех видов мебели разумно добавить общий для всех метод “показать характеристики”. При распечатке характеристик всех видов товара программа бы без разбору для всех объектов вызывала бы этот метод, а каждый конкретный объект уже сам бы решал, какую информацию ему предоставлять. Как это реализуется: Во-первых, в базовом классе определяют общий для всех метод с общим для всех поведением. В случае с нашим примером это будет метод, печатающий общие для любых типов мебели параметры. Во-вторых, в каждом производном классе, где это необходимо, переопределяют базовый метод (добавляют метод с тем же именем), где расширяют базовое поведение своим, например, выводят характеристики, свойственные только конкретному виду мебельной продукции. Метод в базовом классе иногда вообще не обязан содержать какой-либо код, а необходим только для того, чтобы определить имя и набор параметров – сигнатуру метода. Такие методы называют абстрактными методами, а классы, их содержащие, автоматически становятся абстрактными классами. Итак, полиморфизм – это возможность единообразного общения с объектами разных классов через определенный интерфейс. Идеология полиморфизма гласит, что для общения с объектом вам не нужно знать его тип, а нужно знать, какой интерфейс он поддерживает.
  • Интерфейс . В некоторых языках программирования (C#, Java) понятие интерфейса выделено явно - это не только открытые методы и свойства самого класса. Такие языки, как правило, не поддерживают множественного наследования и компенсируют это тем, что любой объект может иметь один базовый объект и реализовывать любое количество интерфейсов. Интерфейс в их интерпретации – это подобие абстрактного класса, содержащего только описание (сигнатуру) открытых методов и свойств. Реализация интерфейса ложится на плечи каждого класса, который собирается его поддерживать. Один и тот же интерфейс могут реализовывать классы абсолютно разных иерархий, что расширяет возможности полиморфизма. К примеру, интерфейс “сохранение/восстановление информации в базе данных” могли бы реализовывать как классы иерархии “мебель”, так и классы, связанные с оформлением заказов на изготовление мебели, а при нажатии на кнопку “сохранить” программа бы прошлась по всем объектами, запросила бы у них этот интерфейс и вызвала бы соответствующий метод.

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

Некоторые элементы современного объектно-ориентированного программирования

Время не стоит на месте, да и времени с момента появления ООП уже прошло довольно много, поэтому не стоит удивляться, что сегодня словарь по объектно-ориентированному программированию серьезно разросся. Итак, вот некоторые новые термины и понятия, связанные с ООП.

  • События . Специальный вид объектов, создаваемый для оповещения одних объектов о событиях, происходящих с другими объектами. В разных языках программирования механизм событий реализуется по-разному: где-то с помощью специальных синтаксических конструкции, а где-то силами базовых средств ООП.
  • Универсальный тип . Концепция универсальных типов не связана непосредственно с концепцией ООП, но она является причиной появление таких элементов, как универсальный класс, универсальный метод, универсальное событие и т.д. Универсальный тип – это тип, параметризованный другим типом (набором типов). Кем является этот тип-параметр в контексте проектирования универсального типа неизвестно, хотя есть возможность ограничить значения типов-параметров, заставив их быть производными от конкретного класса или реализовывать определенные интерфейсы. В качестве примера можно привести универсальный класс сортировки последовательности элементов, где тип элемента в последовательности заранее неизвестен. При проектировании такого класса важно указать, что тип-параметр должен поддерживать операцию сравнения. При создании объектов универсальных типов параметр указывается явно, например целочисленный или строковый тип, а сам объект начинает себя вести так, как если бы это был экземпляр класса, созданный специально для сортировки целых чисел или строк.
  • Исключения . Еще один специальный вид объектов, поддерживаемый встроенным в конкретный язык программирования механизмом обработки ошибок и исключительных ситуаций. Исключения, помимо кода ошибки, содержат ее описание, возможные причины возникновения и стек вызовов методов, имевший место до момента возникновения исключения в программе.

Недостатки объектно-ориентированного программирования

Про то, что популярность объектно-ориентированного подхода к созданию программных продуктов огромна я уже сказал. Про то, что тех, кто стремится расширить эту парадигму довольно много, я тоже уже отметил. Но есть еще один способ выделиться среди огромного сообщества специалистов в информационных технологиях – это заявить, что ООП себя не оправдало, что это не панацея, а, скорее, плацебо. Есть среди этих людей действительно специалисты очень высокого класса, такие как Кристофер Дэйт, Александр Степанов, Эдсгер Дейкстра и другие, и их мнение заслуживает внимания, но есть и те, про которых говорят, что “плохому танцору всегда что-то мешает”. Вот они, наиболее очевидные недостатки ООП, на которые указывают специалисты:

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

2. В некоторых языках все данные являются объектами, в том числе и элементарные типы, а это не может не приводить к дополнительным расходам памяти и процессорного времени.

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

4. Психологический аспект. Многие считают, что ООП это круто и начинают использовать его подходы всегда и везде и без разбору. Все это приводит к снижению производительности программ в частности и дискредитации ООП в целом.

Класс (classes ) является типом данных, определяемых пользователем. В классе задаются свойства и поведение какого-либо предмета или процесса в виде полей данных и функций для работы с ними.

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

Идея классов является основной объектно-ориентированного программирования (ООП). Основные принципы ООП были разработаны еще в языках Simula-67 иSmallTalk, но в то время не получили широкого распространения из-за трудностей освоения и низкой эффективности реализации.

Конкретные величины типа данных «класс» называют экземплярами класса илиобъектами (objects ) .

Подпрограммы, определяющие операции над объектами класса, называются методами (methods ). Вызовы методов называютсясообщениями (messages ). Весь набор методов объекта называется протоколом сообщений (messageprotocol), илиинтерфейсом сообщений (message interface ) объекта. Сообщение должно иметь, по крайней мере, две части: конкретный объект, которому оно должно быть послано, и имя метода, определяющего необходимое действие над объектом. Таким образом, вычисления в объектно-ориентированной программе определяются сообщениями, передаваемыми от одного объекта к другому.

Объекты взаимодействуют между собой, посылая и получая сообщения. Сообщение – это запрос на выполнение действия, содержащий набор необходимых параметров. Механизм сообщения реализуется с помощью вызова соответствующих функций. С помощью ООП легко реализуется так называемая событийно-управляемая модель, когда данные активны и управляют вызовом того или иного фрагмента программного кода.

ООП - это метод программирования, развивающий принципы структурного программирования и основанный на следующих абстракциях данных:

I. Инкапсуляция : объединение данных с процедурами и функциями в единый блок программного кода (данные и методы работы с ними рассматриваются как поля объекта).

II. Наследование – передача методов и свойств от предка к потомку, без необходимости написания дополнительного программного кода (наличие экземпляров класса; потомки, прародители, иерархия).

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

Инкапсуляция

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

Наследование

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

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

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

Класс, который определяется через наследование от другого класса, называется производным классом (derived class ) , илиподклассом (subclass ) . Класс, от которого производится новый класс, называетсяродительским классом (parent class ) , илисуперклассом (superclass ) .

В простейшем случае класс наследует все сущности (переменные и методы) родительского класса. Это наследование можно усложнить, введя управление доступом к сущностям родительского класса.

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

В дополнение к наследуемым сущностям производный класс может добавлять новые сущности и модифицировать методы. Модифицированный метод имеет то же самое имя и часто тот же самый протокол, что и метод, модификацией которого он является. Говорят, что новый метод замещает (override) наследуемую версию метода, который поэтому называется замещаемым (overriden) методом. Наиболее общее предназначение замещающего метода - выполнение операции, специфической для объектов производного класса и не свойственной для объектов родительского класса.

Разработка программы для объектно-ориентированной системы начинается с определения иерархии классов, описывающей отношения между объектами, которые войдут в программу, решающую поставленную задачу. Чем лучше эта иерархия классов соответствует проблемной части, тем более естественным будет полное решение.

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

Полиморфизм

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

Вычисления в объектно-ориентированных языках

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

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

Библиотека визуальных компонентов (Visual Component Library, VCL)

Delphi содержит большое количество классов, предназначенных для быстрой разработки приложений. Библиотека написана на Object Pascal и имеет непосредственную связь с интегрированной средой разработки приложений Delphi.

Все классы VCL расположены на определенном уровне иерархии и образуют дерево (иерархию) классов .

Знание происхождения объекта оказывает значительную помощь при его изучении, так как потомок наследует все элементы объекта-родителя. Так, если свойство Caption принадлежит классу TControl, то это свойство будет и у его потомков, например, у классов TButton и TCheckBox и у компонентов - кнопки Button и независимого переключателя CheckBox соответственно. Фрагмент иерархии классов с важнейшими классами показан на рис.

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

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

Что такое ООП

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

  • инкапсуляция;
  • наследование;
  • полиморфизм.

Некоторые принципы, которые были изначально заложены в первые ООЯ, подверглись существенному изменению.

Примеры объектно-ориентированных языков:

  1. Pascal. С выходом Delphi 7 на официальном уровне стал называться Delphi. Основная область использования Object Pascal - написание прикладного ПО.
  2. C++ широко используется для разработки программного обеспечения, является одним из самых популярных языков. Применяется для создания ОС, прикладных программ, драйверов устройств, приложений, серверов, игр.
  3. Java - транслируется в байт-код, обрабатывается виртуальной машиной Java. Преимуществом такого способа выполнения является независимость от операционной системой и оборудования. Существующие семейства: Standard Edition, Enterprise Edition, Micro Edition, Card.
  4. JavaScript применяется в качестве языка сценариев для web-страниц. Синтаксис во многом напоминает Си и Java. Является реализацией Ecmascript. Сам Ecmascript используется в качестве основы для построения других таких как JScript, ActionScript.
  5. Objective-C построен на основе языка Си, а сам код Си понятен компилятору Objective-C.
  6. Perl - высокоуровневый интерпретируемый динамический язык общего назначения. Имеет богатые возможности для работы с текстом, изначально разработан именно для манипуляций с текстом. Сейчас используется в системном администрировании, разработке, сетевом программировании, биоинформатике и т. д.
  7. PHP. Аббревиатура переводится как препроцессор гипертекста. Применяется для разработки веб-приложений, в частности серверной части. С его помощью можно создавать gui-приложения с помощью пакетов WinBinder.
  8. Python - язык общего назначения, ориентирован на повышение производительности разработчика и читаемость кода. Был разработан проект Cython, с помощью которого осуществляется трансляция программ, написанных на Python в код на языке Си.

Абстракция

Любая книга из рода “Объектно-ориентированное программирование для чайников” выделяет один из главных принципов - абстракцию. Идея состоит в разделении деталей или характеристик реализации программы на важные и неважные. Необходима для крупных проектов, позволяет работать на разных уровнях представления системы, не уточняя детали.

Абстрактный тип данных представляется как интерфейс или структура. Позволяет не задумываться над уровнем детализации реализации. АТД не зависит от других участков кода.

Известный афоризм Дэвида Уилера гласит: Все проблемы в информатике можно решить на другом уровне абстракции.

Наследование

Объектно-ориентированные языки являются наследуемыми - это один из важнейших принципов.

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

Существует несколько типов наследования:

  • простое;
  • множественное.

При множественном наследовании может быть несколько детей от одного предка, когда при простом - только один. Это является основным различием между типами.

Наследование выглядит так:

function draw() {

return "just animal";

function eat() {

return "the animal is eating";

class Cow extends Animal {

function draw() {

Return "something that looks like a cow";

Видим, что class Cow унаследовал все методы от class Animal. Теперь, если выполнить Cow.eat(), получаем "the animal is eating", соответственно, метод draw() изменен. Cow.draw() вернет “something that looks like a cow”, а Animal.draw() вернет “just animal”.

Инкапсуляция

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

Обычно понятия инкапсуляция и сокрытие отождествляются, но некоторые языки различают эти понятия. Другими словами, критичные для работы свойства защищаются, а их изменение становится невозможным.

function __construct($name) {

$this->name = $name;

function getName() {

return $this->name;

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

Полиморфизм

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

В примере выше находится таблица. Мы видим class CardDesk и class GraphicalObject. У обоих есть функция под названием draw(). Она выполняет разные действия, хотя имеет одно имя.

Ad hoc полиморфизм или специальный полиморфизм использует:

  • перегрузку функций, методов;
  • приведение типов.

Перегрузка подразумевает использование нескольких функций с одним именем, когда выбор подходящих происходит на этапе компиляции.

Приведение типов означает преобразование значения одного типа в значение другого типа. Существует явное преобразование - применяется функция, которая принимает один тип, а возвращает другой, неявное - выполняется компилятором или интерпретатором.

«Один интерфейс — много реализаций» Бьерн Страуструп.

Класс

Класс - это такой тип данных, который состоит из единого набора полей и методов.

Имеет внутренние и внешние интерфейсы для управления содержимым. При копировании через присваивание копируется интерфейс, но не данные. Разные виды взаимодействуют между собой посредством:

  • наследования;
  • ассоциации;
  • агрегации.

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

Одной из главных характеристик является область видимости. Понятие по-разному определяется разными ЯП.

В Object Pascal описывается следующим образом:

ClassName = class(SuperClass)

{ использование элементов ограничивается только пределами модуля }

{ здесь указываются поля }

{ спецификатор доступа стал доступным с выходом Delphi 2007, обозначает то же, что и private }

{ элементы могут использоваться внутри ClassName или при наследовании }

{ элементы доступны всем, они отображаются в Object Inspector"e }

Здесь SuperClass - предок, от которого происходит наследование.

Для C++ создание выглядит так:

class MyClass: public Parent

MyClass(); // конструктор

~MyClass(); // деструктор

В этом примере Parent является предком, если таковой имеется. Спецификаторы private, public, protected обозначают то же самое, что в предыдущем примере на Паскале. Также мы видим конструктор, деструктор, доступные для любой части программы. У C++ все элементы по умолчанию являются private, соответственно, это можно не указывать.

Особенности реализации

В центре объектно-ориентированных языков - объект, он является частью класса. Он состоит из:

  • поля;
  • метода.

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

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

ООП-методологии

Существуют такие методологии:

  • Компонентно-ориентированное программирование;
  • Прототипное программирование;
  • Классоориентированное программирование.

Компонентно-ориентированное программирование опирается на понятие компонента - такого составляющего программы, которое предназначено для повторного использования. Реализуется как множество конструкций с общим признаком, правилами, ограничениями. Подход используется в объектно-ориентированном языке Java, где компонентная ориентация реализуется посредством “JavaBeans”, написанных по одним правилам.

В прототипном программировании нет понятия класса - наследование производится за счет клонирования существующего прототипа. Это основа объектно-ориентированных языков javascript и других диалектов ecmascript, а также lua или lo. Главные особенности:

  • потомки не должны сохранять структурное подобие прототипа (в отношении класс - экземпляр это происходит именно так);
  • при копировании прототипа все методы наследуются один в один.

Классоориентированное программирование фокусируется на и экземпляр. Класс определяет общую структуру, поведение для экземпляров, которые их перенимают.

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

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

ООЯ обязательно содержит набор следующих элементов:

  • объявление классов с полями, методами;
  • расширение за счет наследования функций;
  • полиморфное поведение.

Кроме вышеперечисленного списка, могут быть добавлены дополнительные средства:

  • конструктор, деструктор, финализаторы;
  • свойства;
  • индексаторы;
  • модификаторы доступа.

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

ЯП поддерживают больше, чем одну парадигму. Например, PHP или JavaScript поддерживают функциональное, процедурное, объектно-ориентированное программирование. Java работает с пятью парадигмами: объектно-ориентированной, обобщенной, процедурной, аспектно-ориентированной, конкурентной. C# считается одним из самых успешных примеров мультипарадигмальности. Он поддерживает те же подходы, что Java, к этому списку добавляется еще рефлексивная парадигма. Такой ЯП, как Oz, разработан для того, чтобы объединить все понятия, традиционно связанные с различными программными парадигмами.

Я не умею программировать на объектно-ориентированных языках. Не научился. После 5 лет промышленного программирования на Java я всё ещё не знаю, как создать хорошую систему в объектно-ориентированном стиле. Просто не понимаю.

Я пытался научиться, честно. Я изучал паттерны, читал код open source проектов, пытался строить в голове стройные концепции, но так и не понял принципы создания качественных объектно-ориентированных программ. Возможно кто-то другой их понял, но не я.

И вот несколько вещей, которые вызывают у меня непонимание.

Я не знаю, что такое ООП

Серьёзно. Мне сложно сформулировать основные идеи ООП. В функциональном программировании одной из основных идей является отсутствие состояния. В структурном - декомпозиция. В модульном - разделение функционала в законченные блоки. В любой из этих парадигм доминирующие принципы распространяются на 95% кода, а язык спроектирован так, чтобы поощрять их использование. Для ООП я таких правил не знаю.
  • Абстракция
  • Инкапсуляция
  • Наследование
  • Полиморфизм
Смахивает на свод правил, не так ли? Значит вот оно, те самые правила, которым нужно следовать в 95% случаев? Хмм, давайте посмотрим поближе.

Абстракция

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

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

Во-вторых, абстракции в программировании были всегда, начиная с записей Ады Лавлейс, которую принято считать первым в истории программистом. С тех пор люди бесперерывно создавали в своих программах абстракции, зачастую имея для этого лишь простейшие средства. Так, Абельсон и Сассман в своей небезызвестной книге описывают, как создать систему решения уравнений с поддержкой комплексных чисел и даже полиномов, имея на вооружении только процедуры и связные списки. Так какие же дополнительные средства абстрагирования несёт в себе ООП? Понятия не имею. Выделение кода в подпрограммы? Это умеет любой высокоуровневый язык. Объединение подпрограмм в одном месте? Для этого достаточно модулей. Типизация? Она была задолго до ООП. Пример с системой решения уравнений хорошо показывает, что построение уровней абстракции не столько зависит от средств языка, сколько от способностей программиста.

Инкапсуляция

Главный козырь инкапсуляции в сокрытии реализации. Клиентский код видит только интерфейс, и только на него может рассчитывать. Это развязывает руки разработчикам, которые могут решить изменить реализацию. И это действительно круто. Но вопрос опять же в том, причём тут ООП? Все вышеперечисленные парадигмы подразумевают сокрытие реализации. Программируя на C вы выделяете интерфейс в header-файлы, Oberon позволяет делать поля и методы локальными для модуля, наконец, абстракция во многих языках строится просто посредствам подпрограмм, которые также инкапсулируют реализацию. Более того, объектно-ориентированные языки сами зачастую нарушают правило инкапсуляции , предоставляя доступ к данным через специальные методы - getters и setters в Java, properties в C# и т.д. (В комментариях выяснили, что некоторые объекты в языках программирования не являются объектами с точки зрения ООП: data transfer objects отвечают исключительно за перенос данных, и поэтому не являются полноценными сущностями ООП, и, следовательно, для них нет необходимости сохранять инкапсуляцию. С другой стороны, методы доступа лучше сохранять для поддержания гибкости архитектуры. Вот так всё непросто.) Более того, некоторые объектно-ориентированные языки, такие как Python, вообще не пытаются что-то скрыть, а расчитывают исключительно на разумность разработчиков, использующих этот код.

Наследование

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

Значит, вот оно - то, что выделяет ООП как парадигму среди других? Хмм… если так, то почему мы так редко используем его в реальном коде? Помните, я говорил про 95% кода, подчиняющихся правилам доминирующей парадигмы? Я ведь не шутил. В функцинальном программировании не меньше 95% кода использует неизменяемые данные и функции без side-эффектов. В модульном практически весь код логично расфасован по модулям. Преверженцы структурного программирования, следуя заветам Дейкстры, стараются разбивать все части программы на небольшие части. Наследование используется гораздо реже. Может быть в 10% кода, может быть в 50%, в отдельных случаях (например, при наследовании от классов фреймворка) - в 70%, но не больше. Потому что в большинстве ситуаций это просто не нужно .

Более того, наследование опасно для хорошего дизайна. Настолько опасно, что Банда Четырех (казалось бы, проповедники ООП) в своей книге рекомендуют при возможности заменять его на делегирование. Наследование в том виде, в котором оно существует в популярных ныне языках ведёт к хрупкому дизайну. Унаследовавшись от одного предка, класс уже не может наследоваться от других. Изменение предка так же становится опасным. Существуют, конечно, модификаторы private/protected, но и они требуют неслабых экстрасенсорных способностей для угадывания, как класс может измениться и как его может использовать клиентский код. Наследование настолько опасно и неудобно, что крупные фреймворки (такие как Spring и EJB в Java) отказываются от них, переходя на другие, не объектно-ориентированные средства (например, метапрограммирование). Последствия настолько непредсказуемы, что некоторые библиотеки (такие как Guava) прописывает своим классам модификаторы, запрещающие наследование, а в новом языке Go было решено вообще отказаться от иерархии наследования.

Полиморфизм

Пожалуй, полиморфизм - это лучшее, что есть в объектно-ориентированном программировании. Благодаря полиморфизму объект типа Person при выводе выглядит как «Шандоркин Адам Имполитович», а объект типа Point - как "". Именно он позволяет написать «Mat1 * Mat2» и получить произведение матриц, аналогично произведению обычных чисел. Без него не получилось бы и считывать данные из входного потока, не заботясь о том, приходят они из сети, файла или строки в памяти. Везде, где есть интерфейсы, подразумевается и полиморфизм.

Мне правда нравится полиморфизм. Поэтому я даже не стану говорить о его проблемах в мейнстримовых языках. Я также промолчу про узость подхода диспетчеризации только по типу, и про то, как это могло бы быть сделано . В большинстве случаев он работает как надо, а это уже неплохо. Вопрос в другом: является ли полиморфизм тем самым принципом, отличающим ООП от других парадигм? Если бы вы спросили меня (а раз уж вы читаете этот текст, значит, можно считать, что спросили), я бы ответил «нет». И причина всё в тех же процентах использования в коде. Возможно, интерфейсы и полиморфные методы встречаются немного чаще наследования. Но сравните количество строк кода, занимаемое ими, с количеством строк, написанных в обычном процедурном стиле - последних всегда больше. Глядя на языки, поощряющие такой стиль программирования, я не могу назвать их полиморфными. Языки с поддержкой полиморфизма - да, так нормально. Но не полиморфные языки.

(Впрочем, это моё мнение. Вы всегда можете не согласиться.)

Итак, абстракция, инкапсуляция, наследование и полиморфизм - всё это есть в ООП, но ничто из этого не является его неотъемлемым атрибутом. Тогда что такое ООП? Есть мнение, что суть объектно-ориентированного программирования лежит в, собственно, объектах (звучит вполне логично) и классах. Именно идея объединения кода и данных, а также мысль о том, что объекты в программе отражают сущности реального мира. К этому мнению мы ещё вернёмся, но для начала расставим некоторые точки над i.

Чьё ООП круче?

Из предыдущей части видно, что языки программирования могут сильно отличаться по способу реализации объектно-ориентированного программирования. Если взять совокупность всех реализаций ООП во всех языках, то вероятнее всего вы не найдёте вообще ни одной общей для всех черты. Чтобы как-то ограничить этот зоопарк и внести ясность в рассуждения, я остановлюсь только одной группе - чисто объекто-ориентированные языки, а именно Java и C#. Термин «чисто объектно-ориентированный» в данном случае означает, что язык не поддерживает другие парадигмы или реализует их через всё то же ООП. Python или Ruby, например, не буду являться чистыми, т.к. вы вполне можете написать полноценную программу на них без единого объявления класса.

Чтобы лучше понять суть ООП в Java и C#, пробежимся по примерам реализации этой парадигмы в других языках.

Smalltalk. В отличие от своих современных коллег, этот язык имел динамическую типизацию и использовал message-passing style для реализации ООП. Вместо вызовов методов объекты посылали друг другу сообщения, а если получатель не мог обработать то, что пришло, он просто пересылал сообщение кому-то ещё.

Common Lisp. Изначально CL придерживался такой же парадигмы. Затем разработчики решили, что писать `(send obj "some-message)` - это слишком долго, и преобразовали нотацию в вызов метода - `(some-method obj)`. На сегодняшний день Common Lisp имеет развитую систему объектно-ориентированного программирования (CLOS) с поддержкой множественного наследования, мультиметодов и метаклассов. Отличительной чертой является то, что ООП в CL крутится не вокруг объектов, а вокруг обобщённых функций.

Clojure. Clojure имеет целых 2 системы объектно-ориентированного программирования - одну, унаследованную от Java, и вторую, основанную на мультиметодах и более похожую на CLOS.

R. Этот язык для статистического анализа данных также имеет 2 системы объектно-ориентированного программирования - S3 и S4. Обе унаследованы от языка S (что не удивительно, учитывая, что R - это open source реализация коммерческого S). S4 по большей части соотвествует реализациям ООП в современных мейнстримовых языках. S3 является более легковесным вариантом, элементарно реализуемым средствами самого языка: создаётся одна общая функция, диспетчеризирующая запросы по атрибуту «class» полученного объекта.

JavaScript. По идеологии похож на Smalltalk, хотя и использует другой синтаксис. Вместо наследования использует прототипирование: если искомого свойства или вызванного метода в самом объекте нет, то запрос передаётся объекту-прототипу (свойство prototype всех объектов JavaScript). Интересным является факт, что поведение всех объектов класса можно поменять, заменив один из методов прототипа (очень красиво, например, выглядит добавление метода `.toBASE64` для класса строки).

Python. В целом придерживается той же концепции, что и мейнcтримовые языки, но кроме этого поддерживает передачу поиска атрибута другому объекту, как в JavaScript или Smalltalk.

Haskell. В Haskell вообще нет состояния, а значит и объектов в обычном понимании. Тем не менее, своеобразное ООП там всё-таки есть: типы данных (types) могут принадлежать одному или более классам типов (type classes). Например, практически все типы в Haskell состоят в классе Eq (отвечает за операции сравнения 2-х объектов), а все числа дополнительно в классах Num (операции над числами) и Ord (операции <, <=, >=, >). В менстримовых языках типам соответствуют классы (данных), а классам типов - интерфейсы.

Stateful или Stateless?

Но вернёмся к более распространённым системам объектно-ориентированного программирования. Чего я никогда не мог понять, так это отношения объектов с внутренним состоянием. До изучения ООП всё было просто и прозрачно: есть структуры, хранящие несколько связанных данных, есть процедуры (функции), их обрабатывающие. выгулять(собаку), снятьс(аккаунт, сумма). Потом пришли объекты, и это было тоже ничего (хотя читать программы стало гораздо сложней - моя собака выгуливала [кого?], а аккаунт снимал деньги [откуда?]). Затем я узнал про сокрытие данных. Я всё ещё мог выгулять собаку, но вот посмотреть состав её пищи уже не мог. Пища не выполняла никаких действий (наверное, можно было написать, что пища.съесть(собака), но я всё-таки предпочитаю, чтобы моя собака ела пищу, а не наоборот). Пища - это просто данные, а мне (и моей собаке) нужно было просто получить к ним доступ. Всё просто . Но в рамки парадигмы влезть было уже невозможно, как в старые джинсы конца 90-х.

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

Не успел я насладиться просветлением, как увидил в интернетах слово stateless (готов поклясться, оно было окружено сиянием, а над буквами t и l висел нимб). Короткое изучение литературы открыло чудесный мир прозрачного потока управления и простой многопоточности без необходимости отслеживать согласованность объекта. Конечно, мне сразу захотелось прикоснуться к этому чудесному миру. Однако это означало полный отказ от любых правил - теперь было непонятно, следует ли собаке самой себя выгуливать, или для этого нужен специальный ВыгулМенеджер; нужен ли аккаунт, или со всей работой справится Банк, а если так, то должен он списывать деньги статически или динамически и т.д. Количество вариантов использования возрасло экспоненциально, и все варианты в будущем могли привести к необходимости серьёзного рефакторинга.

Я до сих пор не знаю, когда объект следует сделать stateless, когда stateful, а когда просто контейнером данных. Иногда это очевидно, но чаще всего нет.

Типизация: статическая или динамическая?

Еща одна вещь, с которой я не могу определиться относительно таких языков, как C# и Java, это являются они статически или динамически типизированными. Наверное большинство людей воскликнет «Что за глупость! Конечно статически типизированными! Типы проверяются во время компиляции!». Но действительно ли всё так просто? Правда ли, что программист, прописывая в параметрах метода тип X может быть уверен, что в него всегда будут передаваться объекты именно типа X? Верно - не может, т.к. в метод X можно будет передать параметр типа X или его наследника . Казалось бы, ну и что? Наследники класса X всё равно будут иметь те же методы, что и X. Методы методами, а вот логика работы может оказаться совершенно другой. Самый распространённый случай, это когда дочерний класс оказывается соптимизированным под другие нужды, чем X, а наш метод может рассчитывать именно на ту оптимизацию (если вам такой сценарий кажется нереалистичным, попробуйте написать плагин к какой-нибудь развитой open source библиотеке - либо вы потратите несколько недель на разбор архитектуры и алгоритмов библиотеки, либо будете просто наугад вызывать методы с подходящей сигнатурой). В итоге программа работает, однако скорость работы падает на порядок. Хотя с точки зрения компилятора всё корректно. Показательно, что Scala, которую называют наследницей Java, во многих местах по умолчанию разрешает передавать только аргументы именно указанного типа, хотя это поведение и можно изменить.

Другая проблема - это значение null, которое может быть передано практически вместо любого объекта в Java и вместо любого Nullable объекта в C#. null принадлежит сразу всем типам, и в то же время не принадлежит ни одному. null не имеет ни полей, ни методов, поэтому любое обращение к нему (кроме проверки на null) приводит к ошибке. Вроде бы все к этому привыкли, но для сравнения Haskell (да и та же Scala) заставлют использовать специальные типы (Maybe в Haskell, Option в Scala) для обёртки функций, которые в других языках могли бы вернуть null. В итоге про Haskell часто говорят «скомпилировать программу на нём сложно, но если всё-таки получилось, значит скорее всего она работает корректно».

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

Какая разница, как называется такая типизация, если все правила всё равно известны? Разница в том, с какой стороны подходить к проектированию архитектуры. Существует давний спор, как строить систему: делать много типов и мало функций, или мало типов и много функций? Первый подход активно используется в Haskell, второй в Lisp. В современных объектно-ориентированных языках используется что-то среднее. Я не хочу сказать, что это плохо - наверное у него есть свои плюсы (в конце концов не стоит забывать, что за Java и C# стоят мультиязыковые платформы), но каждый раз приступая к новому проекту я задумываюсь, с чего начать проектирования - с типов или с функционала.

И ещё...

Я не знаю, как моделировать задачу. Считается, что ООП позволяет отображать в программе объекты реального мира. Однако в реальности у меня есть собака (с двумя ушами, четырмя лапами и ошейником) и счёт в банке (с менеджером, клерками и обеденным перерывом), а в программе - ВыгулМенеджер, СчётФабрика… ну, вы поняли. И дело не в том, что в программе есть вспомогательные классы, не отражающие объекты реального мира. Дело в том, что поток управления изменяется . ВыгулМенеджер лишает меня удовольствия от прогулки с собакой, а деньги я получаю от бездушного БанкСчёта (эй, где та милая девушка, у которой я менял деньги на прошлой неделе?).

Может быть я сноб, но мне было гораздо приятней, когда данные в компьютере были просто данными, даже если описывали мою собаку или счёт в банке. С данными я мог сделать то, что удобно, без оглядки на реальный мир.

Я также не знаю, как правильно декомпозировать функционал. В Python или C++, если мне нужна была маленькая функция для преобразования строки в число, я просто писал её в конце файла. В Java или C# я вынужден выносить её в отдельный класс StringUtils. В недо-ОО-языках я мог объявить ad hoc обёртку для возврата двух значений из функции (снятую сумму и остаток на счету). В ООП языках мне придётся создать полноценный класс РезультатТранзакции. И для нового человека на проекте (или даже меня самого через неделю) этот класс будет выглядеть точно таким же важным и фундаментальным в архитектуре системы. 150 файлов, и все одинаково важные и фундаментальные - о да, прозрачная архитектура, прекрасные уровни абстракции.

Я не умею писать эффективные программы. Эффективные программы используют мало памяти - иначе сборщик мусора будет постоянно тормозить выполнение. Но чтобы совершить простейшую операцию в объектно-ориентированных языках приходится создавать дюжину объектов. Чтобы сделать один HTTP запрос мне нужно создать объект типа URL, затем объект типа HttpConnection, затем объект типа Request… ну, вы поняли. В процедурном программировании я бы просто вызвал несколько процедур, передав им созданную на стеке структуру. Скорее всего, в памяти был бы создан всего один объект - для хранения результата. В ООП мне приходится засорять память постоянно.

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

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

Что такое объектно ориентированное программирование

Объектно-ориентированное программирование — это стиль кодирования, который позволяет разработчику группировать схожие задачи в классы . Таким образом код соответствует принципу DRY (don"t repeat yourself - не повторяй самого себя) и становится лёгким для сопровождения.

Одним из преимуществ DRY программирования является то, что если некоторая информация требует изменения вашей программы, то нужно изменять код лишь в одном месте, чтобы обновить алгоритм . Одним из самых страшных ночных кошмаров разработчика является сопровождение кода, в котором данные объявляются снова и снова, что превращает любые изменения в программе в бесконечную игру «прятки», в ходе которой приходится охотиться на повторяющиеся данные и куски алгоритма.

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

Что такое объекты и классы

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

В чем заключается разница между классами и объектами

Разработчики, начиная разговаривать о классах и объектах, начинают подменять понятия. К сожалению, такое очень часто происходит.

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

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

Классы формируют структуру данных и действий и используют эту информацию для строительства объектов. Из одного класса может быть построено более одного объекта в одно и тоже время, каждый из них будет независим от других. Продолжая аналогию со строительством, целый район может быть построен по одному проекту: 150 различных домов, которые выглядят одинаково, но в каждом из них живут разные семьи и внутренняя отделка строений разная.

Структура класса

Синтаксис для создания класса очень прост: для объявления класса используется ключевое слово class , за которым следует имя класса и набор фигурных скобок ({}):

После создания класса, новый объект может быть реализован и сохранен в переменной с использованием ключевого слова new:

$obj = new MyClass;

Чтобы увидеть содержимое объекта, используйте var_dump() :

Var_dump($obj);

Можно протестировать весь процесс, скопировав весь код в файл test.php:

Загрузите страницу в ваш браузер и на экране должна появиться следующая строка:

Object(MyClass)#1 (0) { }

Вы только что создали свой первый скрипт ООП.

Определение свойств класса

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

Чтобы добавить свойства классу MyClass, используйте такой код в вашем скрипте:

Ключевое слово public определяет видимость свойства, мы расскажем о нем чуть позже в данной главе. Затем свойство именуется с использованием обычного синтаксиса для переменных, и ему присваивается значение (хотя свойство класса не нуждается в инициализации).

Echo $obj->prop1;

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

Модифицируйте скрипт в test.php, чтобы прочитать значение свойства, а не выводить информацию обо всем классе:

prop1; // Выводим свойство?>

Обновите страницу в вашем браузере, чтобы увидеть результат работы скрипта:

Свойство класса

Определение методов класса

Метод — это функция класса. Индивидуальное действие, которое может выполнить объект, определяется в классе как метод.

Например, создадим методы, которые устанавливают и читают значение свойства $prop1:

prop1 = $newval; } public function getProperty() { return $this->prop1 . "
"; } } $obj = new MyClass; echo $obj->prop1; ?>

Примечание — ООП позволяет объекту ссылаться на самого себя, используя $this . При работе внутри метода, использование $this позволяет использовать имя объекта вне класса.

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

prop1 = $newval; } public function getProperty() { return $this->prop1 . "
"; } } $obj = new MyClass; echo $obj->getProperty(); // получаем значение свойства $obj->setProperty("Новое свойство."); // Устанавливаем новое значение echo $obj->getProperty(); // Снова читаем значение, чтобы увидеть изменения?>

Обновляем страницу в браузере и видим следующее:

Свойство класса Новое свойство

Преимущества ООП проявляются при использовании множества объектов одного класса.

prop1 = $newval; } public function getProperty() { return $this->prop1 . "
"; } } // Создаем два объекта $obj = new MyClass; $obj2 = new MyClass; // Получаем значения $prop1 из обоих объектов echo $obj->getProperty(); echo $obj2->getProperty(); // Устанавливаем новые значения свойств для обоих объектов $obj->setProperty("Новое значение свойства"); $obj2->setProperty("Свойство принадлежит второму объекту"); // Выводим значения $prop1 для обоих echo $obj->getProperty(); echo $obj2->getProperty(); ?>

Когда вы загрузите страницу в браузер, то увидите следующее:

Свойство класса Свойство класса Новое значение свойства Свойство принадлежит второму объекту

Обратите внимание, ООП сохраняет объекты как различные сущности , что позволяет легко разделять код на различные маленькие и взаимосвязанные части.

Магические методы в ООП

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

Использование конструкторов и деструкторов

Когда создаётся объект, очень часто нужно, чтобы при этом сразу производились установки некоторых свойств. Для выполнения таких задач PHP имеет магический метод __construct() , который вызывается автоматически при создании нового объекта.

Для иллюстрации концепции, добавим конструктор к классу MyClass. Он будет выводить сообщение при создании нового объекта класса:

"; } public function setProperty($newval) { $this->prop1 = $newval; } public function getProperty() { return $this->prop1 . "
"; } } // Создаем новый объект $obj = new MyClass; // Получаем значение свойства $prop1 echo $obj->
"; ?>

Примечание — константа __CLASS__ возвращает имя класса, в котором она вызывается; это одна из магических констант PHP.

Создан объект класса "MyClass"! Свойство класса Конец файла.

Для вызова функции в процессе удаления объекта используется магический метод __destruct() . Это очень полезный метод для корректной очистки свойств класса (например, для правильного закрытия соединения с базой данных).

Выведем сообщение, когда будет удаляться объект класса, с помощью магического метода:
__destruct() in MyClass:

"; } public function __destruct() { echo "Объект класса "", __CLASS__, "" удален.
"; } public function setProperty($newval) { $this->prop1 = $newval; } public function getProperty() { return $this->prop1 . "
"; } } // Создаём новый объект $obj = new MyClass; // Получаем значение свойства $prop1 echo $obj->getProperty(); // Выводим сообщение о достижении конца файла echo "Конец файла.
"; ?>

Обновляем страницу в браузере и получаем результат:

Создан объект класса "MyClass"! Свойство класса Конец файла. Объект класса "MyClass" удален.

При достижении конца файла PHP автоматически освобождает все ресурсы.

Для явного вызова деструктора и удаления объекта Вы можете использовать функцию unset() :


"; } public function setProperty($newval) { $this->prop1 = $newval; } public function getProperty() { return $this->prop1 . "
"; } } // Создаем новый объект $obj = new MyClass; // Получаем значение свойства $prop1 echo $obj->getProperty(); // Удаляем объект unset($obj); // Выводим сообщение о достижении конца файла echo "Конец файла.
"; ?>

Теперь результат работы кода будет выглядеть после загрузки в браузер следующим образом:

Создан объект класса "MyClass"! Свойство класса Объект класса "MyClass" удален. Конец файла.

Преобразование в строку

Чтобы избежать ошибки, если скрипт попытается вывести MyClass как строку, используется другой магический метод __toString() .

Без использования __toString() попытка вывести объект как строку приведёт к фатальной ошибке . Попробуйте использовать функцию echo, чтобы вывести объект без применения магического метода:

"; } public function __destruct() { echo "Объект классса "", __CLASS__, "" удален.
"; } public function setProperty($newval) { $this->prop1 = $newval; } public function getProperty() { return $this->prop1 . "

"; ?>

Результат работы будет выглядеть следующим образом:

Создан объект класса "MyClass"! Catchable fatal error: Object of class MyClass could not be converted to string in /Applications/XAMPP/xamppfiles/htdocs/testing/test.php on line 40

Чтобы избежать ошибки, используйте метод __toString() :

"; } public function __destruct() { echo "Объект классса "", __CLASS__, "" удален.
"; } public function __toString() { echo "Используем метод toString: "; return $this->getProperty(); } public function setProperty($newval) { $this->prop1 = $newval; } public function getProperty() { return $this->prop1 . "
"; } } // Создаем новый объект $obj = new MyClass; // Выводим объект как строку echo $obj; // Удаляем объект unset($obj); // Выводим сообщение о достижении конца файла echo "Конец файла.
"; ?>

В этом случае попытка конвертировать объект в строку приведёт к вызову метода getProperty() . Загружаем скрипт в браузер и смотрим на результат работы:

Создан объект класса "MyClass"! Используем метод toString: Свойство класса Объект класса "MyClass" удален. Конец файла.

Использование наследования классов

Классы могут наследовать методы и свойства от других классов с помощью ключевого слова extends . Например, создадим второй класс, который расширяет возможности MyClass и добавляет метод:

"; } public function __destruct() { echo "Объект классса "", __CLASS__, "" удален.
"; } public function __toString() { echo "Используем метод toString: "; return $this->getProperty(); } public function setProperty($newval) { $this->prop1 = $newval; } public function getProperty() { return $this->prop1 . "
"; } } class MyOtherClass extends MyClass { public function newMethod() { echo "Из нового метода класса " . __CLASS__ . ".
"; } } // Создаем новый объект $newobj = new MyOtherClass; // Используем новый метод echo $newobj->newMethod(); // Используем метод из родительского класса echo $newobj->getProperty(); ?>

После загрузки скрипта в браузер получим результат:

Создан объект класса "MyClass"! Из нового метода класса "MyOtherClass". Свойство класса Объект класса "MyClass" удален.

Перегрузка унаследованных свойств и методов

Чтобы изменить поведение существующих свойств или методов в новом классе, вы можете просто перегрузить их с помощью повторного объявления в новом классе:

"; } public function __destruct() { echo "Объект классса "", __CLASS__, "" удален.
"; } public function __toString() { echo "Используем метод toString: "; return $this->getProperty(); } public function setProperty($newval) { $this->prop1 = $newval; } public function getProperty() { return $this->prop1 . "
"; } } class MyOtherClass extends MyClass { public function __construct() { echo "Новый конструктор в классе " . __CLASS__ . ".

"; } } // Создаем новый объект $newobj = new MyOtherClass; // Выводим объект как строку echo $newobj->newMethod(); // Используем метод из родительского класса echo $newobj->getProperty(); ?>

Изменения приведут к следующему выводу при выполнении кода:

Новый конструктор в классе "MyOtherClass". Из нового метода класса "MyOtherClass". Свойство класса Объект класса "MyClass" удален.

Сохранение оригинальной функциональности при перегрузке методов

Чтобы добавить новую функциональность к унаследованному методу при сохранении функций оригинального метода, используйте ключевое слово parent с оператором разрешения видимости ( :: ) :

"; } public function __destruct() { echo "Объект классса "", __CLASS__, "" удален.
"; } public function __toString() { echo "Используем метод toString: "; return $this->getProperty(); } public function setProperty($newval) { $this->prop1 = $newval; } public function getProperty() { return $this->prop1 . "
"; } } class MyOtherClass extends MyClass { public function __construct() { parent::__construct(); // Вызываем конструктор родительского класса echo "Новый конструктор в классе " . __CLASS__ . ".
"; } public function newMethod() { echo "Из нового метода класса " . __CLASS__ . ".
"; } } // Создаем новый объект $newobj = new MyOtherClass; // Выводим объект как строку echo $newobj->newMethod(); // Используем метод из родительского класса echo $newobj->getProperty(); ?>

Выше приведённый код при выполнении выведет на экран сообщения из обеих конструкторов нового и родительского класса:

Создан объект класса "MyClass"! Новый конструктор в классе "MyOtherClass". Из нового метода класса "MyOtherClass". Свойство класса Объект класса "MyClass" удален.

Определение области видимости свойств и методов

Для дополнительного контроля над объектами, методами и свойствами устанавливается область видимости. Таким образом контролируется как и откуда могут быть доступны свойства и методы. Существует три ключевых слова для установки области видимости: public , protected , и private . В дополнение к установке области видимости, методы и свойства могут быть объявлены как static , что позволяет получать к ним доступ без реализации класса.

Примечание — Область видимости — это новое свойство, которое было введено в PHP 5. чтобы узнать о совместимости ООП с PHP 4 , смотрите руководство по использованию PHP.

Свойства и методы public (Общие)

Все свойства и методы, которые вы использовали ранее в данной статье были public (общими). Это значит, что доступ к ним можно было получить где угодно, как внутри класса, так и вне класса..

Методы и свойства protected (Защищённые)

Когда свойство или метод объявляется с директивой protected , оно может быть доступно только внутри самого класса или внутри производных классов (классов, которые расширяют базовый класс, содержащий метод с директивой protected ).

Объявим метод getProperty()как protected в MyClass и попробуем получить доступ к нему вне класса:

"; } public function __destruct() { echo "Объект классса "", __CLASS__, "" удален.
"; } public function __toString() { echo "Используем метод toString: "; return $this->getProperty(); } public function setProperty($newval) { $this->prop1 = $newval; } protected function getProperty() { return $this->prop1 . "

"; } public function newMethod() { echo "Из нового метода класса " . __CLASS__ . ".
"; } } // Создаем новый объект $newobj = new MyOtherClass; // Пробуем вызвать защищенный метод echo $newobj->getProperty(); ?>

При попытке выполнить скрипт, будет сгенерирована следующая ошибка:

Создан объект класса "MyClass"! Новый конструктор в классе "MyOtherClass". Fatal error: Call to protected method MyClass::getProperty() from context "" in /Applications/XAMPP/xamppfiles/htdocs/testing/test.php on line 55

Теперь, создадим новый метод в MyOtherClass для вызова метода getProperty() :

"; } public function __destruct() { echo "Объект классса "", __CLASS__, "" удален.
"; } public function __toString() { echo "Используем метод toString: "; return $this->getProperty(); } public function setProperty($newval) { $this->prop1 = $newval; } protected function getProperty() { return $this->prop1 . "
"; } } class MyOtherClass extends MyClass { public function __construct() { parent::__construct(); echo "Новый конструктор в классе " . __CLASS__ . ".
"; } public function newMethod() { echo "Из нового метода класса " . __CLASS__ . ".
"; } public function callProtected() { return $this->getProperty(); } } // Создаём новый объект $newobj = new MyOtherClass; // Вызываем защищённый метод из общего метода echo $newobj->callProtected(); ?>

При выполнении скрипта результат будет таким:

Создан объект класса "MyClass"! Новый конструктор в классе "MyOtherClass". Свойство класса Объект класса "MyClass" удален.

Методы и свойства private (Частные)

Свойства и методы, объявленные с директивой private, доступны только внутри класса, в котором они определены . Это означает, что даже если новый класс будет производным от класса, в котором определены частные свойства и методы, они не будут доступны в производном классе.

Для демонстрации объявим метод getProperty() как private в MyClass , и попробуем вызвать метод callProtected() из
MyOtherClass :

"; } public function __destruct() { echo "Объект классса "", __CLASS__, "" удален.
"; } public function __toString() { echo "Используем метод toString: "; return $this->getProperty(); } public function setProperty($newval) { $this->prop1 = $newval; } private function getProperty() { return $this->prop1 . "
"; } } class MyOtherClass extends MyClass { public function __construct() { parent::__construct(); echo "Новый конструктор в классе " . __CLASS__ . ".
"; } public function newMethod() { echo "Из нового метода класса " . __CLASS__ . ".
"; } public function callProtected() { return $this->getProperty(); } } // Создаём новый объект $newobj = new MyOtherClass; // Используем метод из родительского класса echo $newobj->callProtected(); ?>

Сохраняем скрипт, обновляем страницу в браузере и получаем следующее:

Создан объект класса "MyClass"! Новый конструктор в классе "MyOtherClass". Fatal error: Call to private method MyClass::getProperty() from context "MyOtherClass" in /Applications/XAMPP/xamppfiles/htdocs/testing/test.php on line 49

Методы и свойства Static (статические)

Методы и свойства, объявленные с директивой static могут быть доступны без инициации класса. Вы просто используете имя класса, оператор разрешения видимости и имя свойства или метода.

Одним из основных преимуществ статических свойств является то, что они сохраняют свои значения на протяжении работы всего скрипта.

Для демонстрации добавим статическое свойство $count и статический метод plusOne() к классу MyClass . Затем установим цикл do...while для вывода увеличивающего значения $count, до тех пор пока оно не станет больше 10:

"; } public function __destruct() { echo "Объект классса "", __CLASS__, "" удален.
"; } public function __toString() { echo "Используем метод toString: "; return $this->getProperty(); } public function setProperty($newval) { $this->prop1 = $newval; } private function getProperty() { return $this->prop1 . "
"; } public static function plusOne() { return "count = " . ++self::$count . ".
"; } } class MyOtherClass extends MyClass { public function __construct() { parent::__construct(); echo "Новый конструктор в классе " . __CLASS__ . ".
"; } public function newMethod() { echo "Из нового метода класса " . __CLASS__ . ".
"; } public function callProtected() { return $this->getProperty(); } } do { // Вызываем plusOne без инициации класса MyClass echo MyClass::plusOne(); } while (MyClass::$count < 10); ?>

Примечание — Для доступа к статическим свойствам знак доллара ($) должен стоять после оператора разрешения видимости.

Выполнение скрипта в браузере выдаст следующий результат:

Count = 1. count = 2. count = 3. count = 4. count = 5. count = 6. count = 7. count = 8. count = 9. count = 10.