Базовые принципы ооп. Основные принципы ооп и их использование

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

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

Основные принципы и этапы объектно-ориентированного

программирования

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

наследованием свойств .

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

П р и м е ч а н и е. Такое представление программы впервые было использовано в языке имитационного моделирования сложных систем Simula, появившемся еще в 60-х годах.

Естественный для языков моделирования способ представления программы получил развитие в другом специализированном языке моделирования - языке Smalltalk (70-е годы), а затем был

Страница 2 из51

Основные принципы ООП

использован в новых версиях универсальных языков программирования, таких как Pascal, С++,

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

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

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

(объектов) программы.

Кроме этого, объектный подход предлагает новые технологические средства разработки, такие как наследование, полиморфизм, композиция, наполнение ,

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

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

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

В основу ООП положены следующие п р и н ц и п ы : абстрагирование,

ограничение доступа, модульность, иерархичность, типизация, параллелизм,

устойчивость.

Рассмотрим, что представляет собой каждый принцип.

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

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

третьем - материалы, из которых он сделан, в четвертом - закон движения

Страница 3 из51

Основные принципы ООП

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

так и определяющих его поведение) в единую программную единицу некий

абстрактный тип (класс).

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

Необходимость ограничения доступа предполагает разграничение двух частей в описании абстракции:

интерфейс - совокупность доступных извне элементов реализации абстракции (основные характеристики состояния и поведения);

реализация - совокупность недоступных извне элементов реализации абстракции (внутренняя организация абстракции и механизмы реализации ее поведения).

Ограничение доступа в ООП позволяет разработчику:

выполнять конструирование системы поэтапно, не отвлекаясь на особенности реализации используемых абстракций;

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

Сочетание объединения всех свойств предмета (составляющих его состояния и поведения) в единую абстракцию и ограничения доступа к реализации этих свойств получило название инкапсуляции.

М о д у л ь н о с т ь - принцип разработки программной системы,

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

Страница 4 из51

Основные принципы ООП

модульного программирования, следование ему упрощает проектирование и

отладку программы.

И е р а р х и я - ранжированная или упорядоченная система абстракций.

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

В ООП используются два вида иерархии.

Иерархия «целое/часть» - показывает, что некоторые абстракции включены

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

Иерархия «общее/частное» - показывает, что некоторая абстракция является частным случаем другой абстракции, например, « обеденный стол -

конкретный вид стола», а « столы - конкретный вид мебели». Используется при

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

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

наследование).

Т и п и з а ц и я - ограничение, накладываемое на свойства объектов и

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

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

Страница 5 из51

Основные принципы ООП

соответствующим программным объектом. Рассматриваемые далее языки программирования на основе Паскаля используют строгую, а на основе С -

среднюю степень типизации.

Использование принципа типизации обеспечивает:

раннее обнаружение ошибок, связанных с недопустимыми операциями над программными объектами (ошибки обнаруживаются на этапе компиляции программы при проверке допустимости выполнения данной операции над программным объектом);

упрощение документирования;

возможность генерации более эффективного кода.

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

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

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

например, относятся задачи автоматического управления несколькими процессами.

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

Страница 6 из51

Основные принципы ООП

разделение времени может выполняться либо разрабатываемой системой (как в

MS DOS), либо используемой ОС (как в системах Windows).

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

Различают:

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

∙ локальные объекты, существующие внутри подпрограмм, время жизни которых исчисляется от вызова подпрограммы до ее завершения;

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

∙ сохраняемые объекты, данные которых хранятся в файлах внешней памяти между сеансами работы программы.

Все указанные выше принципы в той или иной степени реализованы в различных версиях объектно-ориентированных языков.

Объектно-ориентированные языки программирования.Язык считается объектно-ориентированным, если в нем реализованы первые четыре из рассмотренных семи принципов.

Особое место занимают объектные модели Delphi и C++Builder. Эти модели обобщают опыт ООП для MS DOS и включают некоторые новые средства,

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

Сложность программирования под Windows удалось существенно

снизить за счет создания специальных библиотек объектов, « спрятавших» многие элементы техники программирования.

Страница 7 из51

Основные принципы ООП

Этапы разработки программных систем с использованием ООП.

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

Рассмотрим эти этапы.

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

Проект ирование .Различают :

логическое проектирование, при котором принимаемые решения практически не зависят от условий эксплуатации (операционной системы и используемого оборудования);

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

Логическое проектирование заключается в разработке структуры классов:

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

композиция, наполнение, полиморфизм и т.д.). Результатом является иерархия или диаграмма классов, отражающие взаимосвязь классов, и описание классов.

Физическое проектирование включает объединение описаний классов в модули, выбор схемы их подключения (статическая или динамическая компоновка), определение способов взаимодействия с оборудованием, с

операционной системой и/или другим программным обеспечением (например,

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

Страница 8 из51

Основные принципы ООП

Э в о л ю ц и я с и с т е м ы. Это процесс поэтапной реализации и

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

работающий прототип будущей системы. Он тестируется и отлаживается.

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

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

Использование поэтапной реализации существенно упрощает тестирование и отладку программного продукта.

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

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

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

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

Страница 9 из51

Основные принципы ООП

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

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

Рассмотрение основных приемов объектного подхода начнем с объектной декомпозиции.

Объектная декомпозиция

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

происходящие в предметной области поставленной задачи.

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

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

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

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

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

Страница 10 из51

Основные принципы ООП

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

Пример. Объектная декомпозиция (имитационная модель

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

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

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

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

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

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

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

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

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

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

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

Преимущества ООП

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

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

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

Недостатки ООП

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

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

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

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

Всё является объектом – число, строка, список, функция, экземпляр класса, сам класс, модуль. Так класс – объект, способный порождать другие объекты – экземпляры.

В Python нет просто типов данных. Все типы – это классы.

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

И напоследок

Python – это все-таки скриптовый интерпретируемый язык. Хотя на нем пишутся в том числе крупные проекты, часто он используется в веб-разработке, системном администрировании для создания небольших программ-сценариев. В этом случае обычно достаточно встроенных средств языка, "изобретать" собственные классы излишне.

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

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

Сегодня существует достаточное количество объектно-ориентированных языков программирования , наиболее популярными из которых в настоящее время являются C++, Delphi, Java, Visual Basic, Flash. Но, кроме того, многие языки, которые принято причислять к процедурной парадигме, тоже обладают свойствами ООП, имея возможность работать с объектами. Так, объектно-ориентированное программирование в C — это большой раздел программирования на данном языке, то же самое касается ООП в python и многих других структурных языках.

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

Основные принципы ООП

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

Принцип наследования ООП и подклассы

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

Полиморфизм ООП

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

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