Light-electric.com

IT Журнал
0 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Полиморфизм в программировании

NEWOBJ.RU → Введение в ООП с примерами на C# →

3.5. Полиморфизм

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

Переменная shape типа Shape в первой итерации ( i == 0 ) обозначает объект типа Triangle , во второй итерации ( i == 1 ) объект типа Circle , в третьей ( i == 2 ) – Polygon . Мы подробно рассматривали эту ситуацию в предыдущих главах. В общем случае метод Scale может быть: 1) не виртуальным методом базового класса Shape ; 2) виртуальным или абстрактным методом, переопределенным в производном классе; 3) методом интерфейса Shape (если Shape – интерфейс), реализуемом в соответствующем классе, реализующем этот интерфейс ( Triangle , Circle или Polygon ). Обратим внимание, что конкретная реализация метода, которую нужно вызывать во втором и третьем случае, определяется на этапе выполнения программы и не может быть определена на этапе компиляции. Эта возможность объектно-ориентированных языков программирования – записи кода с использованием переменных базовых типов (классов или интерфейсов), откладывая до момента выполнения кода определение того, какую именно реализацию метода нужно выполнить, и называется полиморфизмом. Прежде всего полиморфизм позволяет существенно повысить уровень абстракции: мы оперируем ровно с тем минимально детализированным представлением объекта (методом Shape.Scale ), которое нам нужно в данный момент, не отвлекаясь на особенности производных классов.

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

Сам термин «полиморфизм» составлен из греческих слов πολύς, много, и μορφή, форма, и позаимствован из естественных наук.

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

Остановимся особо на вопросе о том, что на этапе компиляции неизвестно, к какому именно конкретному типу будет относиться объект и, соответственно, какие именно реализации вызываемых методов следует использовать. Компилятор не может связать вызов метода с реализацией этого метода. Термин «связывание» здесь обозначает сопоставление имени метода в коде (в примере: shape.Scale ) с конкретной реализацией этого метода (адресом кода метода в памяти). Если связывание выполняется на этапе компиляции, то говорят о статическом связывании, если на этапе выполнения – о динамическом связывании. Таким образом, полиморфизм возможен только при динамическом связывании 54 .

В заключение отметим, что в более широком значении, вне контекста ООП, выделяют три типа полиморфизма. Первый тип, который мы рассматриваем в настоящем параграфе, называют полиморфизмом подтипов, или «подтипизацией» ( subtyping ). Другой тип полиморфизма – параметрический полиморфизм, используется в рамках обобщённого программирования. Эта тема будет обзорно рассмотрена в следующей главе. И к третьему типу – ad hoc полиморфизму – относят перегрузку методов, исходя из логики, что одно и то же имя метода в зависимости от параметров может обозначать разные реализации.

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

Начнем с классической задачи, называемой проблемой квадрата-прямоугольника 55 . Положим, у нас есть классы квадрата Square и прямоугольника Rectangle . Логично считать, что квадрат – это разновидность прямоугольника, ведь именно так оно и есть с точки зрения геометрии. Соответственно, класс Square представляется логичным сделать производным от класса Rectangle . Однако рассмотрим следующую реализацию:

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

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

Этот принцип называется принципом подстановки Лисков (Liskov substitution), по фамилии известного американского специалиста Барбары Лисков, которая сформулировала его в 1987 г 56 .

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

На правах профессионального фольклора приведем еще один пример, демонстрирующий нарушение принципа подстановки. Положим, у нас в программе есть класс птиц Bird , у которого есть метод Fly ( float height ) (не важно какой: конкретный или абстрактный или виртуальный). От этого класса наследуются классы разных птиц: чиж Siskin , стриж Swift и другие. А теперь мы решили создать класс утка Duck . Утка – птица, но она не умеет летать. При вызове метода Fly программа поведет себя некорректно. Читатель, конечно, может справедливо заметить, что утка всё-таки умеет летать, а некоторые виды уток летают очень хорошо, высоко и далеко. Поэтому в некоторых изложениях утку заменяют пингвином. Или, другой вариант: есть базовый класс утка Duck и производный класс механическая утка на батарейках ElectroDuck . При этом утка может полететь всегда, а на батарейках – только если батарейки заряжены. Во всех вариантах производный класс сужает поведение базового класса, нарушая принцип подстановки.

Другой принцип проектирования, также имеющий непосредственное отношение к наследованию и полиморфизму, мы уже рассматривали в предыдущем разделе – принцип инверсии зависимости (dependency inversion).

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

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

Нарушение правил проектирования существенно обесценивает преимущества ООП. Сегодня сформулировано множество таких правил, более общих, более частных, некоторые из них так или иначе пересекаются. Здесь мы ограничимся указанными двумя принципами, связанными с вопросом иерархических типов данных. Для дальнейшего изучения порекомендуем классическую книгу Р. Мартина «Чистая архитектура» [Мартин 11].

Вопросы и задания

Что такое полиморфизм?

Что такое «динамическое связывание»? Что с чем связывается? Почему при использовании полиморфизма мы не можем применять статическое связывание?

Вернитесь к главам 3.2, 3.3 и 3.4 и укажите, где именно в них шла речь о полиморфизме и какие именно практические преимущества мы получаем в каждом из случаев.

Можно ли написать программу, используя наследование, механизмы виртуальных и абстрактных методов, но не применяя полиморфизм?

Полиморфизм перегрузки методов (ad hoc) – это полиморфизм динамический (с динамическим связыванием) или статический (со статическим связыванием)?

Охарактеризуйте «проблему квадрата-прямоугольника». Сформулируйте и охарактеризуйте принцип подстановки Лисков. Приведите примеры.

* Сформулируйте и охарактеризуйте принцип инверсии зависимости. Приведите примеры.

54. Не следует путать статическое/динамическое связывание и статическую/динамическую типизацию. В случае статической типизации на этапе компиляции известен тип каждой переменной и, соответственно, перечень методов, которые поддерживаются этим типом. Однако конкретная реализация метода, которая должна быть вызвана, может быть и неизвестна. То есть статическая система типов (типизация) может поддерживать как статическое, так и динамическое связывание.

55. Или ромба-прямоугольника, или круга-эллипса. Формулировки задач идентичны, только используются соответственно другие фигуры.

Читать еще:  Ростелеком ошибка 769

Полиморфизм (программирование)

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

Полиморфи́зм (от греч. πολὺ- — много, и μορφή — форма) в языках программирования — возможность объектов с одинаковой спецификацией иметь различную реализацию.

Язык программирования поддерживает полиморфизм, если классы с одинаковой спецификацией могут иметь различную реализацию — например, реализация класса может быть изменена в процессе наследования [1] .

Кратко смысл полиморфизма можно выразить фразой: «Один интерфейс, множество реализаций».

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

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

Содержание

Примеры

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

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

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

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

Класс-потомок сам может быть родителем. Это позволяет строить сложные схемы наследования — древовидные или сетевидные.

Абстрактные (или чисто виртуальные) методы не имеют реализации вообще (на самом деле некоторые языки, например C++, допускают реализацию абстрактных методов в родительском классе). Они специально предназначены для наследования. Их реализация должна быть определена в классах-потомках.

Класс может наследовать функциональность от нескольких классов. Это называется множественным наследованием. Множественное наследование создаёт известную проблему (в C++), когда класс наследуется от нескольких классов-посредников, которые в свою очередь наследуются от одного класса (так называемая «Проблема ромба»): если метод общего предка был переопределён в посредниках, неизвестно, какую реализацию метода должен наследовать общий потомок. Решается эта проблема путём отказа от множественного наследования для классов и разрешением множественного наследования для полностью абстрактных классов (то есть интерфейсов) (C#, Delphi, Java), либо через виртуальное наследование (C++).

В функциональных языках

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

В Haskell существует два вида полиморфизма — параметрический (чистый) и специальный, (на основе классов [Прим. 2] ). Специальный называют еще ad hoc (от лат. ad hoc — специально). Их можно отличить следующим образом:

Параметрический полиморфизм

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

Специальный полиморфизм

Специальный (или лат. ad hoc ) полиморфизм допускает специальную реализацию для данных каждого типа. Например, используемая в нашем примере функцией сортировки функция сравнения должна быть определена по-разному для чисел, кортежей, списков, т. е. она является специально полиморфной. [источник не указан 931 день]

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

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

Неявная типизация

В некоторых языках программирования (например, в Python и Ruby) применяется так называемая утиная типизация [2] (другие названия: латентная, неявная), которая представляет собой разновидность сигнатурного полиморфизма. Таким образом, например, в языке Python полиморфизм не обязательно связан с наследованием.

Формы полиморфизма

Статический и динамический полиморфизм

(упоминается в классической книге Саттера и Александреску, которая является источником).

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

В одном случае конкретный смысл фрагмента зависит от того, в каком окружении код был построен. Это т.н. статический полиморфизм. Перегрузка функций, шаблоны в Си++ реализуют именно статический полиморфизм. Если в коде шаблонного класса вызвана, например, std::sort, то реальный смысл вызова зависит от того, для каких именно типовых параметров будет развернут данный шаблон — вызовется одна из std::sort .

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

Полиморфизм включения

Этот полиморфизм называют чистым полиморфизмом. Применяя такую форму полиморфизма, родственные объекты можно использовать обобщенно. С помощью замещения и полиморфизма включения можно написать один метод для работы со всеми типами объектов TPerson. Используя полиморфизм включения и замещения можно работать с любым объектом, который проходит тест «is-A». Полиморфизм включения упрощает работу по добавлению к программе новых подтипов, так как не нужно добавлять конкретный метод для каждого нового типа, можно использовать уже существующий, только изменив в нем поведение системы. С помощью полиморфизма можно повторно использовать базовый класс; использовать любого потомка или методы, которые использует базовый класс.

Параметрический полиморфизм

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

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

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

Полиморфизм переопределения

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

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

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

Сравнение полиморфизма в функциональном и объектно-ориентированном программировании

Система классов в ФП и в ООП устроены по-разному, поэтому к их сравнению следует подходить очень осторожно.

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

Несмотря на концептуальные различия систем классов в ФП и ООП, реализуются они примерно одинаково — с помощью таблиц виртуальных методов.Используется часто в Java.

ООП. Часть 4. Полиморфизм, перегрузка методов и операторов

C# позволяет использовать один метод для разных типов данных и даже переопределить логику операторов. Разбираемся в перегрузках.

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

Читать еще:  Ошибка при доступе к реестру

В C# это реализуется с помощью перегрузок (overloading).

Евгений Кучерявый

Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.

Перегрузка методов

C# — строго типизированный язык. Это значит, что вы не можете поместить строку в переменную типа int — сначала нужно провести преобразование. Так же и в метод нельзя передать параметр типа float, если при объявлении метода был указан тип double.

Однако если вы экспериментировали с методом WriteLine() класса Console, то могли заметить, что в него можно передавать аргументы разных типов:

Кажется, что нарушена типизация, но компилятор не выдаёт ошибку. Вместо этого всё успешно выводится на экран:

Так происходит потому, что у метода WriteLine() есть перегрузки — методы с таким же названием, но принимающие другие аргументы:

Когда вы вызовете метод Sum(), компилятор по переданным аргументам узнает, какую из его перегрузок вы имели в виду — так же, как это происходит с методом WriteLine().

При этом стоит учитывать, что значение имеют только типы и количество передаваемых аргументов. Например, можно написать такие перегрузки:

У этих методов одинаковые параметры, но разный возвращаемый тип. Попытка скомпилировать такой код приведёт к ошибке — так же, как и создание перегрузки с такими же аргументами, но с другими названиями:

Оглавление

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

То же самое можно сделать и с конструкторами классов:

Альтернатива этому решению — указать значения для аргументов по умолчанию:

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

Перегрузка операторов

Перегрузить можно даже операторы, то есть:

Так как использоваться этот оператор должен без объявления экземпляра класса (item1 + item2, а не item1 item1.+ item2), то указываются модификаторы public static.

Например, мы хотим улучшать предметы в играх. Во многих MMO 1 популярна механика, когда один предмет улучшается за счёт другого. Мы можем сделать это с помощью перегрузки оператора сложения:

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

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

1) MMO (англ. Massively Multiplayer Online Game, MMO, MMOG)

Массовая многопользовательская онлайн-игра

Перегрузка операторов преобразования типов

Хотя типизация в C# строгая, типы можно преобразовывать. Например, мы можем конвертировать число типа float в число типа int:

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

В этом классе хранятся данные о персонаже. В MMO часто можно увидеть такой параметр, как мощь — это сумма всех характеристик героя или предмета. Например, её можно посчитать по следующей формуле:

Мощь = (сила + ловкость + интеллект) * уровень.

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

Модификатор implicit говорит компилятору, что преобразование может быть неявным. То есть оно сработает, если написать так:

Explicit, наоборот, означает, что преобразование должно быть явным:

Вот как будет выглядеть перегрузка преобразования объекта класса Hero в int:

Вот как она будет использоваться:

Вывод в консоль будет следующим:

Проблемы читаемости

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

Или же непонятно, зачем конвертировать Hero в int. Ясность вносит название переменной (power), но этого недостаточно.

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

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

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

Домашнее задание

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

Заключение

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

Вы можете изучить ООП гораздо глубже, записавшись на курс «Профессия C#-разработчик». Он раскрывает лучшие практики работы с C# в объектно-ориентированной парадигме программирования.

Инкапсуляция, полиморфизм, наследование

Все языки OOP, включая С++, основаны на трёх основополагающих концепциях, называемых инкапсуляцией, полиморфизмом и наследованием. Рассмотрим эти концепции.

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

Инкапсуляция (encapsulation) — это механизм, который объединяет данные и код, манипулирующий зтими данными, а также защищает и то, и другое от внешнего вмешательства или неправильного использования. В объектно-ориентированном программировании код и данные могут быть объединены вместе; в этом случае говорят, что создаётся так называемый «чёрный ящик». Когда коды и данные объединяются таким способом, создаётся объект (object). Другими словами, объект — это то, что поддерживает инкапсуляцию.

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

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

2. Полиморфизм

Полиморфизм (polymorphism) (от греческого polymorphos) — это свойство, которое позволяет одно и то же имя использовать для решения двух или более схожих, но технически разных задач. Целью полиморфизма, применительно к объектно-ориентированному программированию, является использование одного имени для задания общих для класса действий. Выполнение каждого конкретного действия будет определяться типом данных. Например для языка Си, в котором полиморфизм поддерживается недостаточно, нахождение абсолютной величины числа требует трёх различных функций: abs(), labs() и fabs(). Эти функции подсчитывают и возвращают абсолютную величину целых, длинных целых и чисел с плавающей точкой соответственно. В С++ каждая из этих функций может быть названа abs(). Тип данных, который используется при вызове функции, определяет, какая конкретная версия функции действительно выполняется. В С++ можно использовать одно имя функции для множества различных действий. Это называется перегрузкой функций (function overloading).

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

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

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

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

Наследование (inheritance) — это процесс, посредством которого один объект может приобретать свойства другого. Точнее, объект может наследовать основные свойства другого объекта и добавлять к ним черты, характерные только для него. Наследование является важным, поскольку оно позволяет поддерживать концепцию иерархии классов (hierarchical classification). Применение иерархии классов делает управляемыми большие потоки информации. Например, подумайте об описании жилого дома. Дом — это часть общего класса, называемого строением. С другой стороны, строение — это часть более общего класса — конструкции, который является частью ещё более общего класса объектов, который можно назвать созданием рук человека. В каждом случае порождённый класс наследует все, связанные с родителем, качества и добавляет к ним свои собственные определяющие характеристики. Без использования иерархии классов, для каждого объекта пришлось бы задать все характеристики, которые бы исчерпывающи его определяли. Однако при использовании наследования можно описать объект путём определения того общего класса (или классов), к которому он относится, с теми специальными чертами, которые делают объект уникальным. Наследование играет очень важную роль в OOP.

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

Классы

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

Читать еще:  Сканирование оперативной памяти на ошибки

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

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

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

В соответствии с UML ( Unified Modelling Language — унифицированный язык моделирования ), класс имеет следующее графическое представление .

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

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

  • конструктор (constructor) — выполняется при создании объектов ;
  • деструктор ( destructor ) — выполняется при уничтожении объектов .

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

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

Инкапсуляция (encapsulation) — это сокрытие реализации класса и отделение его внутреннего представления от внешнего (интерфейса). При использовании объектно-ориентированного подхода не принято применять прямой доступ к свойствам какого-либо класса из методов других классов . Для доступа к свойствам класса принято задействовать специальные методы этого класса для получения и изменения его свойств.

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

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

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

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

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

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

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

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

В качестве примера можно рассмотреть задачу, в которой необходимо реализовать классы «Легковой автомобиль» и «Грузовой автомобиль». Очевидно, эти два класса имеют общую функциональность. Так, оба они имеют 4 колеса, двигатель, могут перемещаться и т.д. Всеми этими свойствами обладает любой автомобиль, независимо от того, грузовой он или легковой, 5- или 12-местный. Разумно вынести эти общие свойства и функциональность в отдельный класс , например, «Автомобиль» и наследовать от него классы «Легковой автомобиль» и «Грузовой автомобиль», чтобы избежать повторного написания одного и того же кода в разных классах .

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

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

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

Все животные в зоомагазине являются наследниками класса «Животное», а также наследниками класса «Товар». Т.е. все они имеют возраст, нуждаются в пище и воде и в то же время имеют цену и могут быть проданы.

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

Не все объектно-ориентированные языки программирования содержат языковые конструкции для описания множественного наследования .

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

Полиморфизм

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

Предположим, мы хотим создать векторный графический редактор, в котором нам нужно описать в виде классов набор графических примитивов — Point , Line , Circle , Box и т.д. У каждого из этих классов определим метод draw для отображения соответствующего примитива на экране.

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

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

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

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

Для описанной выше иерархии классов, используя полиморфизм , можно написать следующий код:

В описанном выше примере массив p[] может содержать любые объекты , порожденные от наследников класса Point . При вызове какого-либо метода у любого из элементов этого массива будет выполнен метод того объекта , который содержится в ячейке массива. Например, если в ячейке p[0] находится объект Circle , то при вызове метода draw следующим образом:

нарисуется круг, а не точка.

В заключение приведем формальное определение полиморфизма .

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

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

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

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

Ссылка на основную публикацию
Adblock
detector