Зачем нам ооп и что это такое. Объектно-ориентированные языки. Основы объектно-ориентированного программирования

Я не умею программировать на объектно-ориентированных языках. Не научился. После 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… ну, вы поняли. В процедурном программировании я бы просто вызвал несколько процедур, передав им созданную на стеке структуру. Скорее всего, в памяти был бы создан всего один объект - для хранения результата. В ООП мне приходится засорять память постоянно.

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

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

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

Процедурные языки

C, Pascal, FORTRAN и подобные языки являются процедурными. То есть каждый их оператор приказывает компьютеру что-то сделать: получить данные, сложить числа, разделить на шесть, отобразить результат. Приложение на процедурном языке представляет собой список инструкций. Если он небольшой, никакого другого организационного принципа (часто называемого парадигмой) не требуется. Программист создает список инструкций, и компьютер выполняет их.

Разделение на функции

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

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

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

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

Проблемы структурного программирования

Поскольку приложения становились все более крупными, структурное программирование начало испытывать трудности. Проекты становились слишком сложными. Графики сдвигались. Задействовалось большее число программистов. Сложность росла. Затраты взлетали, график сдвигался дальше, и наступал крах.

Анализ причин этих неудач показал недостатки процедурной парадигмы. Независимо от того, насколько хорошо реализован структурированный подход к программированию, крупные приложения становятся чрезмерно сложными.

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

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

Неограниченный доступ

В программе, написанной, например, на C, есть два вида данных. Локальные скрыты внутри функции и другими процедурами не используются.

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

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

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

Например, в программе учета кто-то решит, что код учитываемого предмета должен состоять не из 5 цифр, а из 12. Это потребует изменить с short на long. Теперь связанные с кодом функции должны быть изменены для работы с новым форматом.

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

Моделирование реального мира

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

Атрибуты

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

Поведение

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

Решение проблемы

Объект в ООП представляется как совокупность данных и функций. Только процедуры, которые называются функциями-членами в C ++, позволяют получить его значения. Данные скрыты и защищены от изменения. Значения и функции инкапсулированы в одно целое. Инкапсуляция и упрятывание - основные термины в описании ОО-языков.

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

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

Сегодня наиболее широко используемый программирование) - C++ (плюс-плюс). В Java отсутствуют некоторые функции, такие как указатели, шаблоны и множественное наследование, что делает его менее мощным и универсальным, чем C++. C# еще не достиг популярности C++.

Следует отметить, что так называемые функции-члены в C++ называются методами в некоторых других ОО-языках, таких как Smalltalk. Элементы данных называются атрибутами. Вызов метода объекта является посылкой ему сообщения.

Аналогия

Можно представить объекты отделами компании. В большинстве организаций сотрудники не работают один день с кадрами, на следующий начисляя зарплату, а затем неделю занимаясь розничной торговлей. У каждого отдела есть свой персонал с четко возложенными на него обязанностями. Есть и собственные данные: показатели заработной платы, продаж, учет сотрудников и т. д. Люди в отделах работают со своей информацией. Разделение компании, таким образом, облегчает контроль за ее деятельностью и поддерживает целостность данных. Бухгалтерия отвечает за Если необходимо знать общую сумму заработной платы, выплачиваемой в южном филиале в июле, не нужно рыться в архиве. Достаточно направить записку ответственному лицу, подождать, пока этот человек получит доступ к данным и отправит ответ с требуемой информацией. Это гарантирует соответствие регламенту и отсутствие постороннего вмешательства. Таким же образом объект в ООП обеспечивает организацию приложения.

Следует помнить, что ориентация на объекты не касается подробностей работы программы. Большинство инструкций C++ соответствует операторам процедурных языков, таких как С. Действительно, функции-члены в C++ очень похожи на функции в С. Только более широкий контекст позволит установить, является ли инструкция процедурной или объектно-ориентированной.

Объект в ООП: определение

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

Какие вещи становятся объектами в ООП? Ниже представлены типичные категории.

Физический объект в ООП - это:

  • транспорт в моделях движения потока;
  • электрические элементы в программах схемотехники;
  • страны в модели экономики;
  • самолет в системе управления воздушным движением.

Элементы среды компьютера пользователя:

  • меню;
  • окна;
  • графика (линия, прямоугольник, круг);
  • клавиатура, мышь, принтер, дисковые накопители.
  • работники;
  • студенты;
  • клиенты;
  • продавцы.
  • книга учета;
  • личное дело;
  • словарь;
  • таблица широт и долгот населенных пунктов.

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

Класс

Объекты в ООП - это члены классов. Что это значит? Языки программирования имеют встроенные типы данных. Тип int, т. е. целое число, предопределен в C++. Можно объявлять сколько угодно переменных int.

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

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

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

В жизни классы разделены на подклассы. Например, животные делятся на земноводных, млекопитающих, птиц, насекомых и т. д.

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

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

Повторное использование

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

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

Создание новых типов данных

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

position1 = position + origin,

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

Полиморфизм, перегрузка

Операторы = (равно) и + (плюс), используемые в позиционной арифметике выше, не действуют так же, как с встроенными типами, такими как int. Объекты position и др. не предопределены, а заданы программным путем. Каким образом эти операторы знают, как с ними обращаться? Ответ заключается в том, что для них можно задать новые модели поведения. Эти операции будут функциями-членами класса Position.

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

Книга об ООП «Объектно-ориентированное программирование для чайников» позволит всем желающим ознакомиться с данной темой подробнее.

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

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

История

Основатели ООП - выдающиеся норвежские ученые Кристен Нигаард (Cristen Nygaard) и Оле-Йохан Даль (Ole-Johan Dahl). Работая над моделированием судовождения, Нигаард понял, что существующие программные средства малоэффективны в создании столь сложных программ, и тогда Нигаард начал разрабатывать концепции нового программирования, позволяющего преодолеть барьеры сложности, и которое впоследствии было названо объектно-ориентированным (сам термин был придуман Аланом Кеем, автором языка Java). Вместе с Оле-Йоханом Далем Нигаард разработал основные положения ООП и практические механизмы их реализации, которые затем были воплощены в первом ООЯ Симула (Simula). Заслуги этих ученых были по достоинству оценены мировым научным сообществом, и в 2001 году Нигаард и Даль стали лауреатами премии имени Алана Тьюринга - своеобразного аналога Нобелевской премии в области computer science.

Язык Симула пользовался известностью в академических кругах, однако по ряду причин не завоевал популярности среди разработчиков коммерческого ПО. Тем не менее основные идеи и возможности ООП были очень привлекательны для программистов. Впоследствии были созданы другие ООЯ: SmallTalk (1980), C++ (1985), Eiffel (1986), Object Pascal (1986) и Delphi (1995), Oberon-2 (1991), Java (1991), Visual Basic (1991) и многие другие. Некоторые из этих языков стали промышленными стандартами в программировании.

Особенности ООП

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

В обыденной жизни люди используют (пусть даже неосознанно) различные приемы “экономии мышления”, позволяющие осмысливать и выражать сложные явления в простых понятиях. Типичными приемами “экономии мышления” являются:

· абстрагирование (отбрасывание несущественных деталей);

· обобщение (выделение общих существенных признаков у разных явлений или предметов);

· классификация (осознание связи между явлениями и степени их схожести).

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

· объекты - особые программные структуры, объединяющие данные и алгоритмы их обработки;

· инкапсуляция - сокрытие подробностей функционирования объектов;

· наследование - “сокращенный” способ создания новых классов;

· полиморфизм - возможность применения нескольких реализаций одной функции.

Объекты и классы

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

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

В некоторых языках (C++, Java) объекты называются экземплярами класса (instances).

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

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

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

С позиций “борьбы со сложностью” инкапсуляция позволяет переложить часть контроля за правильностью работы с объектами на компилятор (компьютер).

Различные ООЯ предлагают разные возможности по инкапсуляции полей и методов (от полного отсутствия и до автоматического сокрытия всех полей). В промышленных ООЯ, таких, как C++, Java, Delphi, Eiffel и т.д., предусмотрены три уровня инкапсуляции полей и методов:

· public - на обращение к публичным полям и методам объектов нет никаких ограничений;

· protected - прямое обращение к защищенным полям и методам возможно только из методов данного класса и методов дочерних классов;

· private - прямое обращение к приватным полям и методам возможно исключительно из методов данного класса.

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

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

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

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

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

Совокупность всех классов-предков и классов-потомков называется иерархией классов .

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

Полиморфизм

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

Если провести аналогию с реальной жизнью, то полиморфизм соответствует обобщенным действиям. Например, глагол “музицировать” означает “играть на музыкальном инструменте”. Но на разных музыкальных инструментах играют по-разному. Термин один, а вариантов действия - много. Значит, “музицировать” - полиморфное действие . В ООП действию “музицировать” соответствовал бы полиморфный метод , имеющий свои реализации для каждого класса музыкальных инструментов.

В ООП есть два вида полиморфных методов - перегруженные и виртуальные .

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

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

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

Достоинства виртуальных методов проявляются только при использовании иерархии классов. Типичная схема использования виртуальных методов такова:

· В классе-предке иерархии объявляется полиморфный метод, который описывает некое полезное действие. При этом либо он использует виртуальный метод, либо сам является виртуальным.

· В классах-потомках соответствующий виртуальный метод переопределяется - для каждого класса-потомка это полезное действие выполняется по-своему.

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

Яркий пример подобного использования виртуальных методов - система графического оконного интерфейса Delphi или Visual Basic: каждый видимый элемент графического интерфейса - кнопка, ползунок, окно и т.п. - должен быть потомком класса TControl. В классе TControl вводятся общие полиморфные методы отрисовки элементов графического интерфейса, а любой его потомок может нарисовать себя на экране своим собственным способом.

Объектно-ориентированное программирование явно не упоминается в Стандарте 2004 года, хотя в Обязательном минимуме содержания образования по информатике для учащихся профильных заведений (Уровень Б) такая тема присутствовала: Объектно-ориентированное программирование: объект, свойства объекта, операции над объектом . Там же упоминалась и объектно-ориентированная технология программирования.

Тем не менее ООП не просто вошло в практику преподавания информатики (программирования) многих школ, но и присутствует на страницах школьных учебников (Угринович Н.Д. Информатика и информационные технологии. Учебник для 10–11-х классов, 2005. М.: БИНОМ. Лаборатория Знаний). Кроме того, в пропедевтическом курсе информатики для начальной школы (рабочие тетради авторского коллектива под руководством А.Горячева. 1–4-е классы) также вводятся понятия объекта и его свойств .

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

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

Фактически же ООП в школе рассматривается лишь как неотъемлемая часть визуального и компонентного программирования в современных профессиональных системах программирования, а в качестве объектов используются готовые объектные библиотеки различного уровня - это и библиотеки для построения графического интерфейса Windows-приложений, и многоцелевые универсальные библиотеки типов данных (например, STL в С++). Для примитивного использования этих библиотек достаточно знать и уметь применять несколько простейших правил синтаксиса языка программирования. Однако такие “знания” никоим образом не приближают учащихся ни к профессиональному овладению языком программирования, ни даже к пониманию ООП. Но, видимо, ничего страшного в этом нет. Школьная информатика и в профильной школе не ставит своей целью подготовку профессиональных программистов. Преподавание ООП - это специальная тема, даже на соответствующих специальностях вузов ее часто не изучают в достаточном объеме.

Не отрицая полностью предложение некоторых преподавателей информатики поставить объектно-ориентированный подход во главу угла изучения программирования, в том числе в школе, отметим, что ООП невозможно без таких базовых понятий, как программа, исполнитель, переменная, условие, цикл и т.д. Концепция ООП также включает в себя классическое процедурное программирование (см. “Подпрограммы ”), как механика Эйнштейна - механику Ньютона: достаточно представить себе процедурную программу как единственный объект с опущенным для простоты именем. Поэтому в первую очередь задача курса программирования в школе - научить базовым вещам. И лишь при возможности работы с современными визуальными средами программирования (Delphi, Visual Basic, Visual C++
и т.п.) познакомить с понятием объектов и их использованием в основном с помощью методики обучения программированию “по образцу”.

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

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

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

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

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

К преимуществам ООП относятся:

  • ? использование при программировании понятий, более близких к предметной области;
  • ? более простая структура программы в результате инкапсуляции, т.е. объединения свойств и поведения объекта и скрытия деталей его реализации;
  • ? возможность повторного использования кода за счет наследования;
  • ? сравнительно простая возможность модификации программы;
  • ? возможность создания библиотек объектов.

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

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

Объекты В ПАСКАЛЕ объект - это тип данных,

Определяемый программистом. Объект

похож на тип «запись» (record), но кроме полей данных в нем можно описывать методы. Методами называются подпрограммы, предназначенные для работы с полями объекта. Внутри объекта описываются только заголовки методов:

type имя = object

описание элементов

заголовки методов

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

Для каждого поля объекта задается его имя и тип. Он может быть любым, кроме типа того же объекта, но может быть указателем на этот тип.

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

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

Квадратные скобки означают, что эти директивы являются необязательными.

определенными «запасами» здоровья и вооружения и цветом, а также умеет атаковать противника: type monster = object

procedure init (x_, y_, health_, ammo_: word);

procedure attack;

procedure erase;

procedure move (x_, y_: word); private

health, ammo: word; color: word;

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

Описание методов (текст подпрограмм) размещается вне объекта в разделе описания процедур и функций:

procedure monster.init (х_, у_, health_, ammoj word); begin x:= x_; y:= y_; health:= health_; ammo:= ammo_; color:= yellow; end; procedure monster.draw; begin ... {процедуры вывода изображения} end;

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

private, а методы - public.

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

Экземпляры объектов Экземпляр объекта - это переменная

Объектного типа. Время жизни и видимость объектов зависят от вида и места их описания и подчиняются общим правилам ПАСКАЛЯ. Экземпляры объектов можно создавать в статической или динамической памяти:

var Vasia: monster; {описывается статический объект} pm: "?"monster; {описывается указатель на объект}

new (pm); {создается динамический объект}

Можно определять массивы объектов или указателей на объекты и создавать из них динамические структуры данных. Если объектный тип описан в модуле, для создания в программе переменных этого типа следует подключить модуль в разделе uses: uses graph, monsters;

Доступ к элементам объекта осуществляется либо с использованием составного имени, либо с помощью оператора with: Vasia.erase;

with pm A do begin init (100, 100, 30); draw; end;

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

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

Объекты одного типа можно присваивать друг другу, при этом выполняется поэлементное копирование всех полей. Кроме того, в ПАСКАЛЕ определены правила расширенной совместимости типов объектов. Они рассмотрены далее. Все остальные действия выполняются над отдельными полями объектов.

Управлять большим количеством Иерархии объектов

разрозненных объектов сложно. С этим -

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

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

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

Пусть требуется ввести в игру еще один тип персонажей, который должен обладать свойствами объекта monster, но по-другому выглядеть и атаковать:

type daemon = object (monster)

procedure init (x_, y_, health_, ammo_, magic_: word);

procedure attack; procedure draw; procedure erase; procedure wizardry; private

magic: word; end;

{---реализация методов объекта daemon---->

procedure daemon.init (x_, y_, health_, ammo_, magic_: word); begin

inherited init (x_, y_, health_, ammoj; color:= green; magic:= magic_; end;

Наследование полей. Унаследованные поля доступны в объекте точно так же, как и его собственные. Изменить или удалить поле при наследовании нельзя. Таким образом, потомок всегда содержит число полей, большее или равное числу полей своего предка.

Наследование методов. В «потомке» объекта можно не только описывать новые методы, но и переопределять существующие. Метод можно переопределить либо полностью, либо дополнив метод «предка».

Последний вариант иллюстрируется с помощью метода in it. В объекте daemon этот метод переопределен. Из него с помощью ключевого слова inherited (унаследованный) сначала вызывается старый метод, а затем выполняются дополнительные действия. Можно вызвать метод «предка» и явным образом с помощью конструкции monster, init.

Совместимость типов ПАСКАЛЬ - язык со строгой типизацией,

объектов Операнды выражений, параметры под-

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

  • 1) между экземплярами объектов;
  • 2) между указателями на экземпляры объектов;
  • 3) между параметрами и аргументами подпрограмм.

Во всех трех случаях родительскому объекту может быть присвоен экземпляр любого из его потомков, но не наоборот (рис. 6.6). Например, если определены переменные: type

pmonster = A monster; pdaemon = ^daemon; var m: monster; d: daemon; pm: pmonster; pd: pdaemon;

Рис. 6.6.

Поля и методы, введенные в потомке, после таких присваиваний недоступны. Даже если метод переопределен в потомке, через указатель на предка вызывается метод, описанный в предке. Так, в результате выполнения оператора pm A .draw на экране появится изображение объекта-предка, потому что тип вызываемого метода соответствует типу указателя, а не типу того объекта, на который он ссылается.

Если известно, что указатель на предка на самом деле хранит ссылку на потомка, можно обратиться к элементам, определенным в потомке, с помощью явного преобразования типа, например pdaemon (pm) A .wizardry.

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

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

Рассмотрим работу компилятора при Виртуальные методы

использовании в программе иерархий -

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

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

procedure attack; virtual;

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

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

type monster = object

constructor init (x_, y_, health_, ammo_: word);

procedure attack; virtual;

procedure draw; virtual;

procedure erase; virtual;

procedure move (x_, y_: word);

x, у: word; health, ammo: word; color: word;

daemon = object (monster)

constructor init (x_, y_, health_, ammo_, magic_: word); procedure attack; virtual;

procedure draw; virtual; procedure erase; virtual; procedure wizardry; private

magic: word; end;

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

Конструктор обычно используется для инициализации объекта.

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

Объект может содержать несколько конструкторов. Конструктор надо вызывать явным образом для каждого создаваемого объекта.

Вызов виртуального метода выполняется так: из объекта берется адрес его VMT, из VMT выбирается адрес метода, а затем управление передается этому методу (рис. 6.7). Таким образом, при использовании виртуальных методов из всех одноименных методов иерархии всегда выбирается тот, который соответствует фактическому типу вызвавшего его объекта.

Правила описания виртуальных методов.

  • 1. Если в объекте метод определен как виртуальный, во всех потомках он также должен быть виртуальным.
  • 2. Заголовки всех одноименных виртуальных методов должны совпадать.
  • 3. Переопределять виртуальный метод в каждом из потомков не обязательно: если он выполняет устраивающие потомка действия, он будет унаследован.
  • 4. Объект, имеющий хотя бы один виртуальный метод, должен содержать конструктор.
  • 5. При описании объектов рекомендуется определять как виртуальные те методы, которые в его потомках будут реализовываться по-другому.

Рис. 6.7.

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

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

Для выделения памяти под объекты используются процедура и функция new. Например, если определены указатели:

pmonster = A monster; pdaemon = A daemon;

var pm: pmonster; pd: pdaemon;

можно создать объекты с помощью вызовов: new (pm); {или pm:= new (pmonster);} new (pd); {или pd:= new (pdaemon);}

Так как после выделения памяти объект обычно инициализируют, для удобства определены расширенные формы new со вторым параметром - конструктором объекта: new (pm, init (1, 1, 1, 1);

{или pm:= new (pmonster, init (1, 1, 1, 1));} new (pd, init (1, 1, 1, 1, 1);

{или pd:= new (pdaemon, init (1, 1, 1, 1, 1));} Обращение к методам динамического объекта выполняется по обычным правилам:

pm^.draw; pmA.attack;

С объектами в динамической памяти часто работают через указатели на базовый класс, т.е. описывают указатель базового класса, а инициализируют его, создав объект производного класса: pm: = new (pdaemon, init (1, 1, 1, 1, 1));

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

Деструкторы

Для освобождения памяти, занятой объектом, применяется процедура

Она освобождает количество байтов, равное размеру объекта, соответствующего типу указателя. Если в указателе хранится ссылка на объект производного класса, будет освобождено неверное количество байтов. Для корректного освобождения памяти из-под полиморфных объектов следует использовать вторым параметром Dispose специальный метод - деструктор. В документации по Borland Pascal ему рекомендуется давать имя done:

destructor monster.done; begin end; Dispose (pm, done);

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

Компилятор по служебному слову destructor вставляет в конец тела метода операторы получения размера объекта из VMT. Деструктор передает этот размер процедуре Dispose, и она освобождает количество памяти, соответствующее фактическому типу объекта. Деструкторы обязательно использовать только для динамических полиморфных объектов. В объекте можно определить несколько деструкторов (в этом случае они должны иметь разные имена).

  • Инкапсулируйте все, что может изменяться;
  • Уделяйте больше внимания интерфейсам, а не их реализациям;
  • Каждый класс в вашем приложении должен иметь только одно назначение;
  • Классы - это их поведение и функциональность.

Базовые принципы ООП

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

Используйте следующее вместе с наследованием

  • Делегация - перепоручение задачи от внешнего объекта внутреннему;
  • Композиция - включение объектом-контейнером объекта-содержимого и управление его поведением; последний не может существовать вне первого;
  • Агрегация - включение объектом-контейнером ссылки на объект-содержимое; при уничтожении первого последний продолжает существование.

Не повторяйся (Don’t repeat yourself - DRY)

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

Принцип единственной обязанности

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

Принцип открытости/закрытости

Программные сущности должны быть открыты для расширения, но закрыты для изменений.

Принцип подстановки Барбары Лисков

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

Принцип разделения интерфейсов

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

Принцип инверсии зависимостей

Система должна конструироваться на основе абстракций «сверху вниз»: не абстракции должны формироваться на основе деталей, а детали должны формироваться на основе абстракций.



В продолжение темы:
Windows

Часть вторая : "Важнейшие характеристики каждого семейства процессоров Intel Core i3/i5/i7. Какие из этих чипов представляют особый интерес" Введение Сначала мы приведём...

Новые статьи
/
Популярные