Технология объектно ориентированного программирования
Объектно-ориентированное программирование: на пальцах
Настало время серьёзных тем: сегодня расскажем про объектно-ориентированное программирование, или ООП. Это тема для продвинутого уровня разработки, и мы хотим, чтобы вы его постигли.
Из этого термина можно сделать вывод, что ООП — это такой подход к программированию, где на первом месте стоят объекты. На самом деле там всё немного сложнее, но мы до этого ещё доберёмся. Для начала поговорим про ООП вообще и разберём, с чего оно начинается.
Обычное программирование (процедурное)
Чаще всего под обычным понимают процедурное программирование, в основе которого — процедуры и функции. Функция — это мини-программа, которая получает на вход какие-то данные, что-то делает внутри себя и может отдавать какие-то данные в результате вычислений. Представьте, что это такой конвейер, который упакован в коробочку.
Например, в интернет-магазине может быть функция «Проверить email». Она получает на вход какой-то текст, сопоставляет со своими правилами и выдаёт ответ: это правильный электронный адрес или нет. Если правильный, то true, если нет — то false.
Функции полезны, когда нужно упаковать много команд в одну. Например, проверка электронного адреса может состоять из одной проверки на регулярные выражения, а может содержать множество команд: запросы в словари, проверку по базам спамеров и даже сопоставление с уже известными электронными адресами. В функцию можно упаковать любой комбайн из действий и потом просто вызывать их все одним движением.
Что не так с процедурным программированием
Процедурное программирование идеально работает в простых программах, где все задачи можно решить, грубо говоря, десятком функций. Функции аккуратно вложены друг в друга, взаимодействуют друг с другом, можно передать данные из одной функции в другую.
Например, вы пишете функцию «Зарегистрировать пользователя интернет-магазина». Внутри неё вам нужно проверить его электронный адрес. Вы вызываете функцию «Проверить email» внутри функции «Зарегистрировать пользователя», и в зависимости от ответа функции вы либо регистрируете пользователя, либо выводите ошибку. И у вас эта функция встречается ещё в десяти местах. Функции как бы переплетены.
Тут приходит продакт-менеджер и говорит: «Хочу, чтобы пользователь точно знал, в чём ошибка при вводе электронного адреса». Теперь вам нужно научить функцию выдавать не просто true — false, а ещё и код ошибки: например, если в адресе опечатка, то код 01, если адрес спамерский — код 02 и так далее. Это несложно реализовать.
Вы залезаете внутрь этой функции и меняете её поведение: теперь она вместо true — false выдаёт код ошибки, а если ошибки нет — пишет «ОК».
И тут ваш код ломается: все десять мест, которые ожидали от проверяльщика true или false, теперь получают «ОК» и из-за этого ломаются.
Теперь вам нужно:
- либо переписывать все функции, чтобы научить их понимать новые ответы проверяльщика адресов;
- либо переделать сам проверяльщик адресов, чтобы он остался совместимым со старыми местами, но в нужном вам месте как-то ещё выдавал коды ошибок;
- либо написать новый проверяльщик, который выдаёт коды ошибок, а в старых местах использовать старый проверяльщик.
Задача, конечно, решаемая за час-другой.
Но теперь представьте, что у вас этих функций — сотни. И изменений в них нужно делать десятки в день. И каждое изменение, как правило, заставляет функции вести себя более сложным образом и выдавать более сложный результат. И каждое изменение в одном месте ломает три других места. В итоге у вас будут нарождаться десятки клонированных функций, в которых вы сначала будете разбираться, а потом уже нет.
Это называется спагетти-код, и для борьбы с ним как раз придумали объектно-ориентированное программирование.
Объектно-ориентированное программирование
Основная задача ООП — сделать сложный код проще. Для этого программу разбивают на независимые блоки, которые мы называем объектами.
Объект — это не какая-то космическая сущность. Это всего лишь набор данных и функций — таких же, как в традиционном функциональном программировании. Можно представить, что просто взяли кусок программы и положили его в коробку и закрыли крышку. Вот эта коробка с крышками — это объект.
Программисты договорились, что данные внутри объекта будут называться свойствами, а функции — методами. Но это просто слова, по сути это те же переменные и функции.
Объект можно представить как независимый электроприбор у вас на кухне. Чайник кипятит воду, плита греет, блендер взбивает, мясорубка делает фарш. Внутри каждого устройства куча всего: моторы, контроллеры, кнопки, пружины, предохранители — но вы о них не думаете. Вы нажимаете кнопки на панели каждого прибора, и он делает то, что от него ожидается. И благодаря совместной работе этих приборов у вас получается ужин.
Объекты характеризуются четырьмя словами: инкапсуляция, абстракция, наследование и полиморфизм. Если интересно, что это такое, приглашаем в кат:
Инкапсуляция — объект независим: каждый объект устроен так, что нужные для него данные живут внутри этого объекта, а не где-то снаружи в программе. Например, если у меня есть объект «Пользователь», то у меня в нём будут все данные о пользователе: и имя, и адрес, и всё остальное. И в нём же будут методы «Проверить адрес» или «Подписать на рассылку».
Абстракция — у объекта есть «интерфейс»: у объекта есть методы и свойства, к которым мы можем обратиться извне этого объекта. Так же, как мы можем нажать кнопку на блендере. У блендера есть много всего внутри, что заставляет его работать, но на главной панели есть только кнопка. Вот эта кнопка и есть абстрактный интерфейс.
В программе мы можем сказать: «Удалить пользователя». На языке ООП это будет «пользователь.удалить()» — то есть мы обращаемся к объекту «пользователь» и вызываем метод «удалить». Кайф в том, что нам не так важно, как именно будет происходить удаление: ООП позволяет нам не думать об этом в момент обращения.
Например, над магазином работают два программиста: один пишет модуль заказа, а второй — модуль доставки. У первого в объекте «заказ» есть метод «отменить». И вот второму нужно из-за доставки отменить заказ. И он спокойно пишет: «заказ.отменить()». Ему неважно, как другой программист будет реализовывать отмену: какие он отправит письма, что запишет в базу данных, какие выведет предупреждения.
Наследование — способность к копированию. ООП позволяет создавать много объектов по образу и подобию другого объекта. Это позволяет не копипастить код по двести раз, а один раз нормально написать и потом много раз использовать.
Например, у вас может быть некий идеальный объект «Пользователь»: в нём вы прописываете всё, что может происходить с пользователем. У вас могут быть свойства: имя, возраст, адрес, номер карты. И могут быть методы «Дать скидку», «Проверить заказ», «Найти заказы», «Позвонить».
На основе этого идеального пользователя вы можете создать реального «Покупателя Ивана». У него при создании будут все свойства и методы, которые вы задали у идеального покупателя, плюс могут быть какие-то свои, если захотите.
Идеальные объекты программисты называют классами.
Полиморфизм — единый язык общения. В ООП важно, чтобы все объекты общались друг с другом на понятном им языке. И если у разных объектов есть метод «Удалить», то он должен делать именно это и писаться везде одинаково. Нельзя, чтобы у одного объекта это было «Удалить», а у другого «Стереть».
При этом внутри объекта методы могут быть реализованы по-разному. Например, удалить товар — это выдать предупреждение, а потом пометить товар в базе данных как удалённый. А удалить пользователя — это отменить его покупки, отписать от рассылки и заархивировать историю его покупок. События разные, но для программиста это неважно. У него просто есть метод «Удалить()», и он ему доверяет.
Такой подход позволяет программировать каждый модуль независимо от остальных. Главное — заранее продумать, как модули будут общаться друг с другом и по каким правилам. При таком подходе вы можете улучшить работу одного модуля, не затрагивая остальные — для всей программы неважно, что внутри каждого блока, если правила работы с ним остались прежними.
Плюсы и минусы ООП
У объектно-ориентированного программирования много плюсов, и именно поэтому этот подход использует большинство современных программистов.
- Визуально код становится проще, и его легче читать. Когда всё разбито на объекты и у них есть понятный набор правил, можно сразу понять, за что отвечает каждый объект и из чего он состоит.
- Меньше одинакового кода. Если в обычном программировании одна функция считает повторяющиеся символы в одномерном массиве, а другая — в двумерном, то у них большая часть кода будет одинаковой. В ООП это решается наследованием.
- Сложные программы пишутся проще. Каждую большую программу можно разложить на несколько блоков, сделать им минимальное наполнение, а потом раз за разом подробно наполнить каждый блок.
- Увеличивается скорость написания. На старте можно быстро создать нужные компоненты внутри программы, чтобы получить минимально работающий прототип.
А теперь про минусы:
- Сложно понять и начать работать. Подход ООП намного сложнее обычного процедурного программирования — нужно знать много теории, прежде чем будет написана хоть одна строчка кода.
- Требует больше памяти. Объекты в ООП состоят из данных, интерфейсов, методов и много другого, а это занимает намного больше памяти, чем простая переменная.
- Иногда производительность кода будет ниже. Из-за особенностей подхода часть вещей может быть реализована сложнее, чем могла бы быть. Поэтому бывает такое, что ООП-программа работает медленнее, чем процедурная (хотя с современными мощностями процессоров это мало кого волнует).
Что дальше
Впереди нас ждёт разговор о классах, объектах и всём остальном важном в ООП. Крепитесь, будет интересно!
Основные понятия в объектно-ориентированном программировании ИЛИ
моя шпаргалка по ООП
С целью освежения базовых знаний по ООП, я решила перечитать потрясающую книгу «Объектно-ориентированный анализ и проектирование с примерами приложений», Гради Буч
Я обожаю эту книгу, потому что она написана простым языком со знанием дела и такой любовью к программированию, что вы ее с упоением прочтете в метро. Вы будете с нетерпением ждать того момента, когда вы сможете усесться с книжечкой в поезде и взахлеб читать и пропускать свои станции.
А теперь для ленивых и для себя любимой я составила краткий конспект-шпаргалку по этой книги.
ШПАРГАЛКА ПО ООП
Объектно-ориентированное программирование или ООП — это способ создания программных компонентов, базирующихся на объектах.
Основные принципы ООП
- абстрагирование
- инкапсуляция
- модульность
- иерархия
Абстрагирование — это процесс выделения наиболее существенных характеристик некоторого объекта, отличающих его от всех других видов объектов, важных с точки зрения дальнейшего рассмотрения и анализа, и игнорирование менее важных или незначительных деталей.
Объекты и классы — основные абстракции предметной области.
Инкапсуляция — это процесс отделения друг от друга элементов объекта, определяющих его устройство и поведение; инкапсуляция служит для того, чтобы изолировать контрактные обязательства абстракции от их реализации.
Модульность — это свойство системы, связанное с возможностью ее декомпозиции на ряд внутренне сильно сцепленных, но слабо связанных между собой подсистем (частей).
Модульность снижает сложность системы, позволяя выполнять независимую разработку ее отдельных частей.
Иерархия — это упорядочение абстракций, расположение их по уровням.
Типизация — способ защититься от использования объектов одного класса вместо другого, или, по крайней мере, управлять таким использованием.
Тип — точная характеристика некоторой совокупности однородных объектов, включающая структуру и поведение.
При строгой типизации (например, в языке Оберон) запрещается использование объектов неверного типа, требуется явное преобразование к нужному типу. При менее строгой типизации такого рода запреты ослаблены. В частности, допускается полиморфизм — многозначность имен. Одно из проявлений полиморфизма, использование объект подтипа (наследника) в роли объекта супертипа (предка).
Параллелизм — это свойство, отличающее активные объекты от пассивных.
Параллелизм — наличие в системе нескольких потоков управления одновременно. Объект может быть активен, т. е. может порождать отдельный поток управления. Различные объекты могут быть активны одновременно.
Сохраняемость (устойчивость) — способность объекта существовать во времени, переживая породивший его процесс, и (или) в пространстве, перемещаясь из своего первоначального адресного пространства.
Устойчивость — способность объекта сохранять свое существование во времени и/или пространстве (адресном, в частности при перемещении между узлами вычислительной системы). В частности, устойчивость объектов может быть обеспечена за счет их хранения в базе данных.
Основные понятия объектно-ориентированного подхода или элементы объектной модели
“ Объект в ООП — это сущность, способная сохранять свое состояние (информацию) и обеспечивающая набор операций (поведение) для проверки и изменения этого состояния. ”
Объект — осязаемая сущность (tangible entity) — предмет или явление (процесс), имеющие четко выраженные границы, индивидуальность и поведение.
Любой объект обладает состоянием, поведением и индивидуальностью.
Состояние объекта определяется значениями его свойств (атрибутов) и связями с другими объектами, оно может меняться со временем.
Поведение определяет действия объекта и его реакцию на запросы от других объектов. Поведение представляется с помощью набора сообщений, воспринимаемых объектом (операций, которые может выполнять объект).
Индивидуальность — это свойства объекта, отличающие его от всех других объектов.
Структура и поведение схожих объектов определяют общий для них класс.
Объект в JavaScript создаётся с помощью функции Object.create. Эта функция из родителя и опционального набора свойств создаёт новую сущность. Пока что мы не будем беспокоиться о параметрах.
Прототип — это объект-образец, по образу и подобию которого создаются другие объекты. Объекты-копии могут сохранять связь с родительским объектом, автоматически наследуя изменения в прототипе; эта особенность определяется в рамках конкретного языка.
Класс — это множество объектов, связанных общностью свойств, поведения, связей и семантики. Любой объект является экземпляром класса. Определение классов и объектов — одна из самых сложных задач объектно-ориентированного проектирования.
Класс (class) — это группа данных и методов(функций) для работы с этими данными. Это шаблон. Объекты с одинаковыми свойствами, то есть с одинаковыми наборами переменных состояния и методов, образуют класс.
Конструктор класса — специальный блок инструкций, вызываемый при создании объекта.
var s = new String();
Деструктор — специальный метод класса, служащий для деинициализации объекта (например освобождения памяти).
Атрибут — поименованное свойство класса, определяющее диапазон допустимых значений, которые могут принимать экземпляры данного свойства. Атрибуты могут быть скрыты от других классов, это определяет видимость атрибута: рublic (общий, открытый); private (закрытый, секретный); protected (защищенный).
Требуемое поведение системы реализуется через взаимодействие объектов. Взаимодействие объектов обеспечивается механизмом пересылки сообщений. Определенное воздействие одного объекта на другой с целью вызвать соответствующую реакцию называется операцией или посылкой сообщения. Сообщение может быть послано только вдоль соединения между объектами. В терминах программирования соединение между объектами существует, если один объект имеет ссылку на другой.
Дескриптор — это атрибут объекта со связанным поведением (англ. binding behavior), т.е. такой, чьё поведение при доступе переопределяется методами протокола дескриптора.
Операция — это услуга, которую можно запросить у любого объекта данного класса. Операции реализуют поведение экземпляров класса. Описание операции включает четыре части: имя; список параметров; тип возвращаемого значения; видимость.
Реализация операции называется методом.
Метод — это функция или процедура, принадлежащая какому-то классу или объекту.
Различают простые методы и статические методы (методы класса):
- простые методы имеют доступ к данным объекта (конкретного экземпляра данного класса),
- статические методы не имеют доступа к данным объекта и для их использования не нужно создавать экземпляры (данного класса).
Методы предоставляют интерфейс, при помощи которого осуществляется доступ к данным объекта некоторого класса, тем самым, обеспечивая инкапсуляцию данных.
В зависимости от того, какой уровень доступа предоставляет тот или иной метод, выделяют:
- открытый (public) интерфейс — общий интерфейс для всех пользователей данного класса;
- защищённый (protected) интерфейс — внутренний интерфейс для всех наследников данного класса;
- закрытый (private) интерфейс — интерфейс, доступный только изнутри данного класса.
Такое разделение интерфейсов позволяет сохранять неизменным открытый интерфейс, но изменять внутреннюю реализацию.
Полиморфизм — способность скрывать множество различных реализаций под единственным общим именем или интерфейсом.
Понятие полиморфизма может быть интерпретировано, как способность объекта принадлежать более чем одному типу.
Интерфейс — это совокупность операций, определяющих набор услуг класса или компонента. Интерфейс не определяет внутреннюю структуру, все его операции открыты.
Компонент — это относительно независимая и замещаемая часть системы, выполняющая четко определенную функцию в контексте заданной архитектуры.
Компонент представляет собой физическую реализацию проектной абстракции и может быть: компонентом исходного кода (cpp-шник); компонентом времени выполнения (dll, ActiveX и т. п.); исполняемый компонентом (exe-шник). Компонент обеспечивает физическую реализацию набора интерфейсов. Компонентная разработка (component-based development) представляет собой создание программных систем, состоящих из компонентов (не путать с объектно-ориентированным программированием (ООП).
Компонентная разработка — технология, позволяющая объединять объектные компоненты в систему.
Пакет — это общий механизм для организации элементов в группы. Это элемент модели, который может включать другие элементы. Каждый элемент модели может входить только в один пакет.
-средством организации модели в процессе разработки, повышения ее управляемости и читаемости;
-единицей управления конфигурацией.
Подсистема — это комбинация пакета (может включать другие элементы модели) и класса (обладает поведением). Подсистема реализует один или более интерфейсов, определяющих ее поведение. Она используется для представления компонента в процессе проектирования.
10 принципов ООП, о которых стоит знать каждому программисту
- Переводы, 21 мая 2019 в 10:17
- Klara Oswald
Многим опытным разработчикам, вероятно, знакома методология объектно-ориентированного программирования (ООП). Кроме известных её принципов (абстракция, инкапсуляция, полиморфизм, наследование и т. д.) существуют и другие — менее известные, но не менее важные и полезные для реализации. Некоторые из них собраны в специальный блок и известны по акрониму SOLID. Эта статья расскажет об этих и других существующих принципах объектно-ориентированной разработки и о том, какие преимущества они предлагают.
Принцип единственной ответственности (SRP)
Соответствует букве S акронима SOLID. Согласно этому принципу, не должно быть более одной причины для изменения класса, или класс должен всегда обрабатывать одну функциональность.
Основное преимущество состоит в том, что такой подход уменьшает связь между отдельным компонентом программного обеспечения и кодом. Если вы добавляете более одной функциональности в один класс, это вводит связь между двумя функциями, и даже если вы меняете только одну из них, есть шанс сломать другую, связанную с ней. Что в свою очередь требует больше раундов тестирования для избежания каких-либо неожиданностей в продакшене.
Принцип открытости/закрытости (OCP)
Соответствует букве O акронима SOLID. Принцип можно выразить так: «Классы, методы или функции должны быть открыты для расширения (добавления новой функциональности) и закрыты для модификации». Такой подход запрещает кому-либо изменять уже опробованный и протестированный код, а значит, он не ломается. В этом и состоит основное преимущество такого подхода.
Ниже приведён пример кода на Java, который нарушает этот принцип:
А вот пример после рефакторинга. Теперь соблюдается принцип открытости/закрытости: при добавлении новой реализации Shape не нужно менять код GraphicEditor .
Принцип подстановки Барбары Лисков (LSP)
Соответствует букве L акронима SOLID. Согласно этому принципу подтипы должны быть заменяемыми для супертипа. Другими словами, методы или функции, работающие с суперклассом, должны иметь возможность без проблем работать также и с его подклассами.
Ивент переехал в онлайн, есть новые даты ( 14 – 15 июля ) , Москва и онлайн, 10 750–138 000 ₽
LSP тесно связан с принципом единственной ответственности и принципом разделения интерфейса.
Если класс реализует больше функциональности, чем подкласс, то последний может не поддерживать некоторые функции и тем самым нарушает данный принцип.
Ниже приведён пример такого кода на Java:
Функция resize() провоцирует неявную ошибку при работе с экземпляром класса Square , потому что позволяет устанавливать отличные друг от друга значения ширины и высоты. Согласно принципу LSP, функции, использующие ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом. Поэтому для корректной работы функция resize() должна проверять, является ли передаваемый объект экземпляром класса Square, и в этом случае не позволять установить разные значения ширины и высоты. Отсюда идёт нарушение принципа.
Принцип разделения интерфейса (ISP)
Соответствует букве I акронима SOLID. Этот принцип подразумевает, что интерфейс, который не используется, не должен быть реализован.
В основном это происходит, когда один интерфейс содержит несколько функциональностей, и клиенту нужна только одна из них, а другие — нет.
Написание интерфейса — сложная задача. Когда он готов, вы не сможете изменить его, не нарушив всю реализацию.
Ещё одно преимущество этого принципа в Java заключается в том, что интерфейс имеет недостаток. Необходимо сначала реализовать все методы, прежде чем какой-либо класс сможет их использовать. Поэтому наличие единственной функциональности означает меньшее количество методов для реализации.
Принцип инверсии зависимостей (DIP)
Соответствует букве D акронима SOLID. Прелесть этого принципа проектирования в том, что любой класс легко тестируется с помощью фиктивного объекта и проще в обслуживании, потому что код создания объекта централизован, а клиентский код не перегружен им.
Ниже приведён пример кода Java, который нарушает принцип инверсии зависимости:
Пример демонстрирует, что AppManager зависит от EventLogWriter . Если вам нужно использовать другой способ уведомления клиента (например push-уведомления, SMS или электронную почту), необходимо изменить класс AppManager .
Эту проблему можно решить с помощью принципа инверсии зависимостей. Вместо того, чтобы AppManager запрашивал EventLogWriter , последний следует внедрить в AppManager явно. Плюсом реализации общего интерфейса позволить внедрять любую реализацию для других способов уведомления.
Теперь перейдём к принципам, которые не входят в пятёрку SOLID, но не менее важны.
DRY (Don’t Repeat Yourself)
Переводится как «не повторяйся» и буквально означает, что нужно уходить от дублирующего кода и по возможности использовать абстракцию для общих вещей.
Если есть одинаковый блок кода в более чем двух местах, вынесите его в отдельный метод. Если вы используете жёстко запрограммированное значение более одного раза, сделайте его общедоступной константой. Преимущество этого принципа заключается в упрощении поддержки вашего кода.
Но важно не злоупотреблять этим принципом. Например, один и тот же код не подойдёт для проверки OrderId и SSN. Их форматы могут не совпадать, и на выходе функция выдаст некорректный результат. В качестве решения можно предусмотреть в методе проверку форматов для подобных наборов чисел.
Инкапсуляция изменяющегося кода
Сервисы стремительно развиваются. Продакшн подразумевает постоянные изменения кода и его поддержку. Отсюда следует второй принцип ООП — инкапсуляция кода, который с большой вероятностью будет изменён в будущем.
Преимущество этого принципа ООП заключается в том, что инкапсулированный код легко тестировать и поддерживать.
Воспользуйтесь алгоритмом, по которому переменные и методы по умолчанию имеют спецификатор private. Затем шаг за шагом увеличиваете доступ при необходимости (с private на protected, с protected на public).
Одним из вариантов инкапсуляции является Фабричный метод. Он инкапсулирует код создания объекта и обеспечивает гибкость для последующего создания новых объектов без влияния на существующий код.
Композиция вместо наследования
Существует два основных способа повторного использования кода: наследование и композиция. Оба они имеют свои преимущества и недостатки, но, как правило, предпочтение рекомендуется отдавать последнему, если это возможно. Обусловлено это тем, что композиция гибче наследования.
Композиция позволяет изменять поведение класса прямо во время выполнения через установку его свойств. Реализуя интерфейсы, вы, таким образом, используете полиморфизм, который обеспечивает более гибкую реализацию.
«Effective Java» Джошуа Блоха также советует отдавать предпочтение композиции вместо наследования. Если вы всё ещё не уверены, вы также можете посмотреть здесь, чтобы узнать, почему композиция лучше, чем наследование для повторного использования кода и его функциональности.
Программирование для интерфейса
Этот принцип подразумевает, что следует по возможности программировать для интерфейса, а не для его реализации. Это даст вам гибкий код, который может работать с любой новой реализацией интерфейса.
Другими словами, нужно использовать тип интерфейса для переменных, возвращаемых типов или типа аргумента метода. Например, использовать для хранения объекта суперкласс, а не подкласс.
Это также рекомендовано во многих книгах по Java, в том числе в Effective Java и Head First design pattern.
Ниже приведён пример для интерфейса в Java:
Принцип делегирования
Не делайте всё самостоятельно, делегируйте это в соответствующий класс. Классическим примером этого принципа являются методы equals() и hashCode() в Java. Если нужно сравнить два объекта, это действие поручается соответствующему классу вместо клиентского.
Основным преимуществом этого принципа является отсутствие дублирования кода и довольно простое изменение поведения. Этот принцип относится также к делегированию событий (событие делегируется соответствующему обработчику).
Заключение
Эти принципы разработки помогают писать гибкий код, стремящийся к высокой связности и низкому зацеплению. Как только вы это освоите, следующим шагом будет изучение шаблонов проектирования для решения общих проблем разработки приложений и программного обеспечения.
Что такое ООП на примерах. Для чайников
Наверное, в половине вакансий(если не больше), требуется знание и понимание ООП. Да, эта методология, однозначно, покорила многих программистов! Обычно понимание ООП приходит с опытом, поскольку годных и доступно изложенных материалов на данный счет практически нет. А если даже и есть, то далеко не факт, что на них наткнутся читатели. Надеюсь, у меня получится объяснить принципы этой замечательной методологии, как говорится, на пальцах.
Итак, уже в начале статьи я уже упомянул такой термин «методология». Применительно к программированию этот термин подразумевает наличие какого-либо набора способов организации кода, методов его написания, придерживаясь которых, программист сможет писать вполне годные программы.
ООП (или объектно-ориентированное программирование) представляет собой способ организации кода программы, когда основными строительными блоками программы являются объекты и классы, а логика работы программы построена на их взаимодействии.
Об объектах и классах
Класс — это такая структура данных, которую может формировать сам программист. В терминах ООП, класс состоит из полей (по-простому — переменных) и методов (по-простому — функций). И, как выяснилось, сочетание данных и функций работы над ними в одной структуре дает невообразимую мощь. Объект — это конкретный экземпляр класса. Придерживаясь аналогии класса со структурой данных, объект — это конкретная структура данных, у которой полям присвоены какие-то значения. Поясню на примере:
Допустим, нам нужно написать программу, рассчитывающую периметр и площадь треугольника, который задан двумя сторонами и углом между ними. Для написания такой программы используя ООП, нам необходимо будет создать класс (то есть структуру) Треугольник. Класс Треугольник будет хранить три поля (три переменные): сторона А, сторона Б, угол между ними; и два метода (две функции): посчитать периметр, посчитать площадь. Данным классом мы можем описать любой треугольник и вычислить периметр и площадь. Так вот, конкретный треугольник с конкретными сторонами и углом между ними будет называться экземпляром класса Треугольник. Таким образом класс — это шаблон, а экземпляр — конкретная реализация шаблона. А вот уже экземпляры являются объектами, то есть конкретными элементами, хранящими конкретные значения.
Одним из самых распространенных объектно-ориентированных языков программирования является язык java. Там без использования объектов просто не обойтись. Вот как будет выглядеть код класса, описывающего треугольник на этом языке:
Если мы внутрь класса добавим следующий код:
то программу уже можно будет запускать на выполнение. Это особенность языка java. Если в классе есть такой метод
то этот класс можно выполнять. Разберем код подробнее. Начнем со строки
Здесь мы создаем экземпляр triangle1 класса Triangle и тут же задаем ему параметры сторон и угла между ними. При этом, вызывается специальный метод, называемый конструктор и заполняет поля объекта переданными значениями в конструктор. Ну, а строки
выводят рассчитанные площадь треугольника и его периметр в консоль.
Аналогично все происходит и для второго экземпляра класса Triangle .
Понимание сути классов и конструирования конкретных объектов — это уверенный первый шаг к пониманию методологии ООП.
Еще раз, самое важное:
ООП — это способ организации кода программы;
Класс — это пользовательская структура данных, которая воедино объединяет данные и функции для работы с ними(поля класса и методы класса);
Объект — это конкретный экземпляр класса, полям которого заданы конкретные значения.
Три волшебных слова
ООП включает три ключевых подхода: наследование, инкапсуляцию и полиморфизм. Для начала, приведу определения из wikipedia:
Инкапсуляция — свойство системы, позволяющее объединить данные и методы, работающие с ними, в классе. Некоторые языки (например, С++) отождествляют инкапсуляцию с сокрытием, но большинство (Smalltalk, Eiffel, OCaml) различают эти понятия.
Наследование — свойство системы, позволяющее описать новый класс на основе уже существующего с частично или полностью заимствующейся функциональностью. Класс, от которого производится наследование, называется базовым, родительским или суперклассом. Новый класс — потомком, наследником, дочерним или производным классом.
Полиморфизм — свойство системы, позволяющее использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта.
Понять, что же все эти определения означают на деле достаточно сложно. В специализированных книгах, раскрывающих данную тему на каждое определение, зачастую, отводится целая глава, но, как минимум, абзац. Хотя, сути того, что нужно понять и отпечатать навсегда в своем мозге программиста совсем немного.
А примером для разбора нам будут служить фигуры на плоскости. Из школьной геометрии мы знаем, что у всех фигур, описанных на плоскости, можно рассчитать периметр и площадь. Например, для точки оба параметра равны нулю. Для отрезка мы можем вычислить лишь периметр. А для квадрата, прямоугольника или треугольника — и то, и другое. Сейчас же мы опишем эту задачу в терминах ООП. Также не лишним будет уловить цепь рассуждений, которые выливаются в иерархию классов, которая , в свою очередь, воплощается в работающий код. Поехали:
Итак, точка — это самая малая геометрическая фигура, которая является основой всех прочих построений (фигур). Поэтому именно точка выбрана в качестве базового родительского класса. Напишем класс точки на java:
У получившегося класса Point пустой конструктор, поскольку в данном примере мы работаем без конкретных координат, а оперируем только параметрами значениями сторон. Так как у точки нет никаких сторон, то и передавать ей никаких параметров не надо. Также заметим, что класс имеет методы Point::getSquare() и Point::getPerimeter() для расчета площади и периметра, оба возвращают 0. Для точки оно и логично.
Поскольку у нас точка является основой всех прочих фигур, то и классы этих прочих фигур мы наследуем от класса Point . Опишем класс отрезка, наследуемого от класса точки:
означает, что класс LineSegment наследуется от класса Point . Методы LineSegment::getSquare() и LineSegment::getPerimeter() переопределяют соответствующие методы базового класса. Площадь отрезка всегда равняется нулю, а площадь периметра равняется длине этого отрезка.
Теперь, подобно классу отрезка, опишем класс треугольника(который также наследуется от класса точки):
Тут нет ничего нового. Также, методы Triangle::getSquare() и Triangle::getPerimeter() переопределяют соответствующие методы базового класса.
Ну а теперь, собственно, тот самый код, который показывает волшебство полиморифзма и раскрывает мощь ООП:
Мы создали массив объектов класса Point , а поскольку классы LineSegment и Triangle наследуются от класса Point , то и их мы можем помещать в этот массив. Получается, каждую фигуру, которая есть в массиве figures мы можем рассматривать как объект класса Point . В этом и заключается полиморфизм: неизвестно, к какому именно классу принадлежат находящиеся в массиве figures объекты, но поскольку все объекты внутри этого массива принадлежат одному базовому классу Point , то все методы, которые применимы к классу Point также и применимы к его классам-наследникам.
Теперь о инкапсуляции. То, что мы поместили в одном классе параметры фигуры и методы расчета площади и периметра — это и есть инкапсуляция, мы инкапсулировали фигуры в отдельные классы. То, что у нас для расчета периметра используется специальный метод в классе — это и есть инкапсуляцию, мы инкапсулировали расчет периметра в метод getPerimiter() . Иначе говоря, инкапсуляция — это сокрытие реализции (пожалуй, самое короткое, и в то же время емкое определением инкапсуляции).
Основы применения объектно-ориентированного программирования
В настоящее время объектно-ориентированный подход к программированию является стандартом де-факто. Мы попытаемся проанализировать те свойства объектной модели, которые позволили ей занять лидирующую позицию в разработке ПО, и способы ее применения на практике. В данной статье приведены основные понятия объектной модели и кратко рассмотрены ее применение на разных этапах разработки ПО. В дальнейшем мы более подробно расскажем о специфике объектно-ориентированных методов спецификации, анализа и дизайна программных систем.
Что такое объектно- ориентированный подход к программированию
В сознании значительной части людей объектно-ориентированный подход к программированию ассоциируется прежде всего с языком программирования. Если проект написан на Си++, Java или Smalltalk — значит, используется объектный подход. Я бы сказал иначе: если проект пишется на Си++ или Smalltalk — значит, возможно использование объектного подхода.
Что же такое объектно-ориентированный подход к программированию? Это последовательное использование объектной модели предметной области на всех этапах разработки программного обеспечения. Ниже мы опишем основные понятия объектной модели. Эти понятия не зависят ни от языка программирования, ни от технологии разработки программных систем.
В центре объектной модели, естественно, стоит понятие объекта. Объект — это настолько общее понятие, что довольно сложно дать ему четкое и приемлемое для всех определение. Ивар Якобсон дает следующее определение: объект — это сущность, способная сохранять свое состояние (информацию) и обеспечивающая набор операций (поведение) для проверки и изменения этого состояния. Объект характеризуется набором операций и состоянием, которое сохраняет результат этих операций.
Предположим, мы разрабатываем библиотечную систему. Одним из объектов в этой системе будет читатель. Читатель характеризуется своим именем и списком книг, взятых в библиотеке. Читатель может сдать книгу или взять новую. В такой модели объект «читатель» имеет состояние описываемое, двумя атрибутами — имя (неизменяемое) и список книг (изменяемый), и совершает две операции — «сдать» и «взять». Операции соответственно изменяют список книг у данного читателя.
Целиком система будет состоять из множества объектов: читатели, книги, хранилище, библиотекарь и т. д. Эти объекты взаимодействуют между собой, посылая друг другу сообщения. Например, читатель может получить сообщение о необходимости сдать книгу (поскольку подходит к концу срок, на который она выдана) и в ответ выполнить операцию «сдать». Другим примером сообщения может быть вопрос библиотекаря к читателю: «Как Вас зовут?». Чтобы правильно отреагировать на такое сообщение, объекту «читатель» необходимо иметь операцию «назвать свое имя».
В системе обычно функционирует множество объектов одного класса. Читатели Иванов, Петров и Сидоров — экземпляры класса «читатель». Все экземпляры одного и того же класса имеют один и тот же набор операций и могут реагировать на одни и те же сообщения. Класс иногда называют типом объектов. Класс в определенном смысле соответствует понятию абстрактного типа данных, введенному в программирование довольно давно, еще в языке Модула-2.
Классы могут наследовать свойства других классов. В нашем примере два класса — «библиотекарь» и «читатель» — имеют общее. Они являются людьми, т. е. они принадлежат к классу «человек». Атрибут «имя» и операция «сказать свое имя» — свойства человека. Эти свойства унаследованы классами «библиотекарь» и «читатель» от класса «человек».
Предположим, что нашей библиотекой пользуются студенты и преподаватели университета, при этом у них немного разные права. Например, преподаватели могут одновременно брать больше книг, чем студенты. В рамках объектной модели этот факт можно отобразить следующим образом: введем два класса — «читатель-преподаватель» и «читатель-студент». Они наследуют (являются подклассами, или выведены из) класса «читатель». У них общий интерфейс — набор операций, однако действия при выполнении этих операций могут быть различны. Кроме того, при дальнейшей разработке системы нам понадобятся различные операции или атрибуты для студентов и преподавателей.
Таким образом, классы образуют иерархию, показанную на рисунке.
Различие операций «взять» и «сдать» в классах «преподаватель» и «студент» иллюстрирует еще одно важное свойство объектной модели — полиморфизм. Полиморфизм позволяет работать с неким объектом, не зная конкретного класса, к которому этот объект относится. При посылке сообщения «сдать» нам неважно, кем является данный читатель — студентом или преподавателем. Однако сама операция сдачи книг будет происходить по-разному в зависимости от того, кто этот читатель.
Объективные и субъективные проблемы большого проекта. Объектно-ориентированная модель как адекватное отображение решаемой задачи
Рассмотрим ряд типичных проблем больших проектов, которые объектно-ориентированный подход существенно упрощает.
Большая и сложная задача. Это естественно, иначе проект не был бы большим. Когда мы разрабатываем программную систему, мы разрабатываем программную модель предметной области — части реального мира. Самая большая сложность состоит в семантическом разрыве между реальностью и программой. Объектная модель уменьшает этот разрыв. Реальный мир естественно описывать как набор взаимодействующих объектов. Такое описание, с одной стороны, понятно конечному потребителю и эксперту, с другой — легко ложится на объектную модель, а следовательно, его легко реализовать.
Постоянные изменения задания. Чаще всего изменяются не сами объекты, участвующие в системе, а протоколы их взаимодействия. В этом случае основа системы остается без изменения и реализация изменений оказывается существенно проще. Если же изменения касаются какого-либо объекта, они будут локальны для данного класса.
Слишком большая длительность разработки. Объектно-ориентированный подход стимулирует многократное использование программного обеспечения. Количество существенно различных объектов не так уж и велико. Иногда можно применить имеющиеся классы в готовом виде или объекты в качестве компонентов других объектов. Механизм наследования классов позволяет использовать имеющиеся классы в качестве базовых, из которых выводятся новые, специализированные на конкретное применение. Кроме того, широкое распространение объектно-ориентированного подхода привело к огромному числу предлагаемых готовых библиотек классов, как универсального характера, так и ориентированных на различные сферы бизнеса.
Сложность сопровождения. Использование готовых, многократно проверенных классов уменьшает количество ошибок. Структура программ, основанная на объектах, отображающих объекты реального мира, уменьшает вероятность появления побочных неожиданных эффектов от изменений. Также здесь можно повторить аргументы о сравнительной простоте изменения системы.
Разумеется, все это не дается даром. При разработке системы необходимо приложить определенные усилия, особенно направленные на многократное использование кода.
И здесь опять объектная ориентация показывает свое преимущество тем, что существуют объектно-ориентированные методы для всех этапов разработки, начиная от анализа требований заказчика и кончая тестированием и поддержкой исходных текстов.
Этапы разработки и их реализация в рамках объектной модели
До появления объектной модели все шаги разработки программной системы были резко отделены друг от друга. Переход от анализа к дизайну, от дизайна к кодированию — это переход от одной модели к другой, своего рода прыжок. Такой переход требует дополнительных сил и, естественно, сопряжен с дополнительной опасностью искажения информации, внесения ошибок.
Как мы уже говорили, одним из важнейших преимуществ объектной ориентации является возможность использования одной и той же модели на всех этапах разработки. Наличие хорошо стыкующихся, а тем более одних и тех же методов анализа, дизайна и программирования упрощает весь процесс, уменьшает риск непонимания между разработчиками и тем самым ускоряет разработку и повышает качество продукта.
Анализ. Объектно-ориентированный анализ (ООА) начинается с документирования (записи) требований заказчика к системе. Важно, чтобы это описание однозначно понималось как заказчиками, так и разработчиками. Одним из наиболее широко распространенных является метод use case — метод сценариев. В нем записываются все сценарии использования будущей системы с точки зрения пользователя. Затем, на этапе анализа, эти сценарии переводят в сценарии взаимодействия объектов. Благодаря тому, что основные объекты реального мира и программной системы одни и те же, перевод будет простым и однозначным.
Существует множество методов объектно-ориентированного анализа. Пожалуй, самой интересной разработкой последнего времени является попытка создания так называемого Объединенного метода (Unitied Method). Три законодателя мод в объектно-ориентированном анализе — Grady Booch, James Rumbaugh и Ivar Jacobson — объединились под крышей компании Rational Software для того, чтобы соединить лучшее из трех ранее самостоятельных методов в один. Следует отметить, что большинство методов ООА не зависят от конкретного языка программирования. Результирующая модель одна и та же для Си++ и Smalltalk.
Дизайн. Разница между анализом и дизайном заключается в том, что в результате анализа мы описываем внешнее поведение системы, тогда как результат дизайна — спецификация того, как это поведение будет реализовано.
В объединенном методе анализ и дизайн — просто две стадии одной и той же модели с одним и тем же языком описания и одними и теми же CASE-средствами.
Уже на этапе дизайна возможно многократное использование имеющихся решений. Настоящую революцию в программировании произвело проектирование «по образцам» (Design Patterns). Образцы — это хорошо документированные, аргументированные проектные решения. Например, если в системе может существовать не более одного объекта определенного класса, можно воспользоваться образцом Singleton; если нужен объект, контролирующий доступ к другому объекту, — образцом Proxy, и т. п. Фактически образцы — это развитие понятия алгоритма, но с упором на структуру для решения определенной задачи, а не на последовательность действий.
Программирование. Наличие объектно-ориентированного дизайна существенно упрощает этап написания кода. Как мы уже отмечали, важнейшую роль играет возможность использования готовых библиотек классов. Относительная изолированность классов существенно повышает надежность тестирования.
Объектно-ориентированное программирование — не панацея
Использование самой передовой методики не гарантирует от неудач. Один из самых больших провалов в истории разработки программных комплексов — система обработки багажа в новом аэропорту города Денвера, которая задумывалась как полностью объектно-ориентированная система.
Основные проблемы объектно-ориентированной разработки — трудоемкость и сложность, особенно на начальном этапе перехода от, например, структурного программирования.
Объектно-ориентированные языки программирования сложнее процедурных. Для освоения языка Си++ требуется по крайней мере в два-три раза больше времени, чем для освоения языка Си. Библиотеки классов экономят время разработки, но опять-таки требуют времени на изучение.
Большая часть времени разработки уходит на анализ и дизайн. По многочисленным оценкам, эти стадии отнимают не менее 60% общего времени. Это часто усложняет взаимодействие с заказчиком, который хочет быть уверен, что разрабатывается именно то, что ему требуется. (На помощь приходят средства прототипирования — за недостатком места мы не можем остановиться на них подробнее.) Использование образцов дизайна требует квалификации, изучения и, следовательно, времени. То же самое относится и к методам ООА/ООД и CASE-средствам.
Тем не менее опыт показывает, что преодоление всех этих трудностей себя полностью оправдывает. Компании, переходящие на объектно-ориентированную разработку, вначале вкладывают дополнительные средства в обучение. По мере освоения новых и накопления собственного багажа проектных решений и программного обеспечения процесс разработки ускоряется и удешевляется и выгоды намного перекрывают начальные затраты.
Booch G. Object-Oriented Analisys and Design with Applications. 2nd ed. 1994.
Jacobson I. e. a. Object-Oriented Software Engineering. Addison-Wesley, 1992.
Gamma E. e. a. Design Patterns. Addison-Wesley, 1995.