Light-electric.com

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

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

Язык программирования C++

Язык С++. Полиморфизм

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

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

Чтобы использовать полиморфизм, необходимо чтобы:

1) все классы-потомки являлись наследниками одного и того же базового класса

2) функция, реализующая метод, должна быть объявлена виртуальной в базовом классе

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

Доступ к обычным методам через указатели

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

A, B, Base – это типы. Указатели на объекты производных классов совместимы по типу с указателями на объекты базового класса.

Base *ptr; ptr=&a; ptr=&b;

Однако, указатели производных классов между собой не совместимы!

Пример:

A *ptr; ptr=&a;

ptr=&b; // указатель класса A не совместим с указателем класса B.

Теперь необходимо понять, какая собственно функция выполняется в этой строчке

Это функция Base::show() или A::show() или B:show()?

Результат выполнения дает простой ответ

Base

Base

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

Доступ к виртуальным методам через указатели

Сделаем одно маленькое изменение в нашей программе: поставим ключевое слово virtual перед объявлением функции show() в базовом классе.

На выходе имеем:

Class A

Class B

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

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

Абстрактные классы и чисто виртуальные методы

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

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

Чисто виртуальная функция – это функция, после объявления которой добавлено выражение =0.

Пример: Объявить абстрактный класс person. Объявить два производных класса – student и teacher. В каждом из классов объявить метод, с помощью которого можно создать список выдающихся педагогов и студентов. Студентов со средним баллом больше 4 и педагогов с числом публикаций более 50 статей считать выдающимися.

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. Или ромба-прямоугольника, или круга-эллипса. Формулировки задач идентичны, только используются соответственно другие фигуры.

Введение в ООП с примерами на C#. Часть первая. Все, что нужно знать о полиморфизме

  • Переводы, 14 июля 2016 в 21:00
  • Пётр Соковых

Я много писал на смежные темы, вроде концепции MVC, Entity Framework, паттерна «Репозиторий» и т.п. Моим приоритетом всегда было полное раскрытие темы, чтобы читателю не приходилось гуглить недостающие детали. Этот цикл статей опишет абсолютно все концепции ООП, которые могут интересовать начинающих разработчиков. Однако эта статья предназначена не только для тех, кто начинает свой путь в программировании: она написана и для опытных программистов, которым может потребоваться освежить свои знания.

Сразу скажу, далеко в теорию мы вдаваться не будем — нас интересуют специфичные вопросы. Где это будет нужно, я буду сопровождать повествование кодом на C#.

Что такое ООП и в чём его плюсы?

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

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

Что за концепции ООП?

Сейчас коротко о принципах, которые мы позже рассмотрим в подробностях:

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

Ладно, тут мы коснулись большого количества теории, настало время действовать. Я надеюсь, это будет интересно.

Полиморфизм

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

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

  • Давайте создадим консольное приложение InheritanceAndPolymorphism и класс Overload.cs с тремя методами DisplayOverload с параметрами, как ниже:

В главном методе Program.cs теперь напишем следующее:

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

DisplayOverload 100
DisplayOverload method overloading
DisplayOverload method overloading100

Класс Overload содержит три метода, и все они называются DisplayOverload , они различаются только типами параметров. В C# (как и в большистве других языков) мы можем создавать методы с одинаковыми именами, но разными параметрами, это и называется «перегрузка методов». Это значит, что нам нет нужды запоминать кучу имён методов, которые совершают одинаковые действия с разными типами данных.

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

Мы получим ошибку компиляции:

Error: Type ‘InheritanceAndPolymorphism.Overload’ already defines a member called ‘DisplayOverload’ with the same parameter types

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

Что нужно запомнить: метод не идентифицируется по возвращаемому типу, это не полиморфизм.
Если мы попробуем скомпилировать

…то у нас это не получится:

Error: Type ‘InheritanceAndPolymorphism.Overload’ already defines a member called ‘DisplayOverload’ with the same parameter types

Здесь присутствуют два метода, принимающих целое число в качестве аргумента, с той лишь разницей, что один из них помечен как статический.

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

То нас ждёт разочарование:

Error: Cannot define overloaded method ‘DisplayOverload’ because it differs from another method only on ref and out

Что нужно запомнить: на идентификатор метода оказывают влияние только его имя и параметры (их тип, количество). Модификаторы доступа не влияют. Двух методов с одинаковыми идентификаторами существовать не может.

Роль ключевого слова params в полиморфизме

Параметры могут быть четырёх разных видов:

  • переданное значение;
  • преданная ссылка;
  • параметр для вывода;
  • массив параметров.

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

  • Если мы запустим следующий код:

То получим две ошибки:

Error1: The parameter name ‘a’ is a duplicate

Error2: A local variable named ‘a’ cannot be declared in this scope because it would give a different meaning to ‘a’, which is already used in a ‘parent or current’ scope to denote something else

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

  • Теперь попробуем запустить следующий код:

Overload.cs

Program.cs

Мы получим следующий вывод:

Akhil
Akhil 1
Akhil 2
Akhil 3

Мы можем передавать одинаковые ссылочные параметры столько раз, сколько захотим. В методе Display строка name имеет значение «Akhil». Когда мы меняем значение x на «Akhil1», на самом деле мы меняем значение name , т.к. через параметр x передана ссылка именно на него. То же и с y — все эти три переменных ссылаются на одно место в памяти.

Overload.cs

Program.cs

Это даст нам такой вывод:

Akhil 100
Mittal 100
OOP 100
Akhil 200

Нам часто может потребоваться передать методу n параметров. В C# такую возможность предоставляет ключевое слово params .

Важно: это ключевое слово может быть применено только к последнему аргументу метода, так что метод ниже работать не будет:

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

200 100
300 100
100 200

Важно запомнить: C# достаточно умён, чтоб разделить обычные параметры и массив параметров, даже если они одного типа.

    Посмотрите на следующие два метода:

    Разница между ними в том, что первый запустится, и такая синтаксическая конструкция будет подразумевать, что в метод будет передаваться n массивов строк. Вторая же выдаст ошибку:

    Error: The parameter array must be a single dimensional array

    Запомните: массив параметров должен быть одномерным.

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

Overload.cs

Program.cs

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

Akhil 3
Ekta 3
Arsh 3

Однако такой код:

Уже вызовет ошибку:

Error: The best overloaded method match for ‘InheritanceAndPolymorphism.Overload.DisplayOverload(int, params string[])’ has some invalid arguments

Error:Argument 2: cannot convert from ‘string[]’ to ‘string’

Думаю, тут всё понятно — или, или. Смешивать передачу отдельными параметрами и одним массивом нельзя.

  • Теперь рассмотрим поведение следующей программы:

Overload.cs

Program.cs

После её выполнения мы получим в консоли:

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

Результатом выполнения такого кода будет

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

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

C# рассматривает методы с массивом параметров последними, так что во втором случае будет вызван метод, принимающий два целых числа. В первом и третьем случае будет вызван метод с params , так как ничего кроме него запустить невозможно. Таким образом, на выходе мы получим:

parameterArray
The two integers 200 300
parameterArray

  • Теперь кое-что интересное. Как вы думаете, каким будет результат выполнения следующей программы?

Overload.cs

Program.cs

В консоли мы увидим:

System.Int32 System.String System.Double
System.Object[] System.Object[] System.Int32 System.String System.Double

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

В заключение

В этой статье мы рассмотрели перегрузку методов, особенности компиляции, с ней связанные, и буквально все возможные случаи использования ключевого слова params . В следующей мы рассмотрим наследование. Напоследок ещё раз повторим основные пункты, которые нужно запомнить:

  • Метод идентифицируется не только по имени, но и по его параметрам.
  • Метод не идентифицируется по возвращаемому типу.
  • Модификаторы вроде static также не являются свойствами, идентифицирующими метод.
  • На идентификатор метода оказывают влияние только его имя и параметры (их тип, количество). Модификаторы доступа не влияют. Двух методов с одинаковыми идентификаторами существовать не может.
  • Имена параметров должны быть уникальны. Также не могут быть одинаковыми имя параметра метода и имя переменной, созданной в этом же методе.
  • Ключевое слово params может быть применено только к последнему аргументу метода.
  • C# достаточно умён, чтоб разделить обычные параметры и массив параметров, даже если они одного типа.
  • Массив параметров должен быть одномерным.

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

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

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

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

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

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

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

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

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

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

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

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

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

Объектно-ориентированное программирование: на пальцах

Ста­тья не маль­чи­ка, но мужа.

Наста­ло вре­мя серьёз­ных тем: сего­дня рас­ска­жем про объектно-ориентированное про­грам­ми­ро­ва­ние, или ООП. Это тема для про­дви­ну­то­го уров­ня раз­ра­бот­ки, и мы хотим, что­бы вы его постиг­ли.

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

Обычное программирование (процедурное)

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

Напри­мер, в интернет-магазине может быть функ­ция «Про­ве­рить email». Она полу­ча­ет на вход какой-то текст, сопо­став­ля­ет со сво­и­ми пра­ви­ла­ми и выда­ёт ответ: это пра­виль­ный элек­трон­ный адрес или нет. Если пра­виль­ный, то true, если нет — то false.

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

Что не так с процедурным программированием

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

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

Тут при­хо­дит продакт-менеджер и гово­рит: «Хочу, что­бы поль­зо­ва­тель точ­но знал, в чём ошиб­ка при вво­де элек­трон­но­го адре­са». Теперь вам нуж­но научить функ­цию выда­вать не про­сто true — false, а ещё и код ошиб­ки: напри­мер, если в адре­се опе­чат­ка, то код 01, если адрес спа­мер­ский — код 02 и так далее. Это неслож­но реа­ли­зо­вать.

Вы зале­за­е­те внутрь этой функ­ции и меня­е­те её пове­де­ние: теперь она вме­сто true — false выда­ёт код ошиб­ки, а если ошиб­ки нет — пишет «ОК».

И тут ваш код лома­ет­ся: все десять мест, кото­рые ожи­да­ли от про­ве­ряль­щи­ка true или false, теперь полу­ча­ют «ОК» и из-за это­го лома­ют­ся.

Теперь вам нуж­но:

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

Зада­ча, конеч­но, реша­е­мая за час-другой.

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

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

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

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

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

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

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

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

Инкап­су­ля­ция — объ­ект неза­ви­сим: каж­дый объ­ект устро­ен так, что нуж­ные для него дан­ные живут внут­ри это­го объ­ек­та, а не где-то сна­ру­жи в про­грам­ме. Напри­мер, если у меня есть объ­ект «Поль­зо­ва­тель», то у меня в нём будут все дан­ные о поль­зо­ва­те­ле: и имя, и адрес, и всё осталь­ное. И в нём же будут мето­ды «Про­ве­рить адрес» или «Под­пи­сать на рас­сыл­ку».

Абстрак­ция — у объ­ек­та есть «интер­фейс»: у объ­ек­та есть мето­ды и свой­ства, к кото­рым мы можем обра­тить­ся извне это­го объ­ек­та. Так же, как мы можем нажать кноп­ку на блен­де­ре. У блен­де­ра есть мно­го все­го внут­ри, что застав­ля­ет его рабо­тать, но на глав­ной пане­ли есть толь­ко кноп­ка. Вот эта кноп­ка и есть абстракт­ный интер­фейс.

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

Напри­мер, над мага­зи­ном рабо­та­ют два про­грам­ми­ста: один пишет модуль зака­за, а вто­рой — модуль достав­ки. У пер­во­го в объ­ек­те «заказ» есть метод «отме­нить». И вот вто­ро­му нуж­но из-за достав­ки отме­нить заказ. И он спо­кой­но пишет: «заказ.отменить()». Ему неваж­но, как дру­гой про­грам­мист будет реа­ли­зо­вы­вать отме­ну: какие он отпра­вит пись­ма, что запи­шет в базу дан­ных, какие выве­дет пре­ду­пре­жде­ния.

Насле­до­ва­ние — спо­соб­ность к копи­ро­ва­нию. ООП поз­во­ля­ет созда­вать мно­го объ­ек­тов по обра­зу и подо­бию дру­го­го объ­ек­та. Это поз­во­ля­ет не копи­па­стить код по две­сти раз, а один раз нор­маль­но напи­сать и потом мно­го раз исполь­зо­вать.

Напри­мер, у вас может быть некий иде­аль­ный объ­ект «Поль­зо­ва­тель»: в нём вы про­пи­сы­ва­е­те всё, что может про­ис­хо­дить с поль­зо­ва­те­лем. У вас могут быть свой­ства: имя, воз­раст, адрес, номер кар­ты. И могут быть мето­ды «Дать скид­ку», «Про­ве­рить заказ», «Най­ти зака­зы», «Позво­нить».

На осно­ве это­го иде­аль­но­го поль­зо­ва­те­ля вы може­те создать реаль­но­го «Поку­па­те­ля Ива­на». У него при созда­нии будут все свой­ства и мето­ды, кото­рые вы зада­ли у иде­аль­но­го поку­па­те­ля, плюс могут быть какие-то свои, если захо­ти­те.

Иде­аль­ные объ­ек­ты про­грам­ми­сты назы­ва­ют клас­са­ми.

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

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

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

Плюсы и минусы ООП

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

  • Визу­аль­но код ста­но­вит­ся про­ще, и его лег­че читать. Когда всё раз­би­то на объ­ек­ты и у них есть понят­ный набор пра­вил, мож­но сра­зу понять, за что отве­ча­ет каж­дый объ­ект и из чего он состо­ит.
  • Мень­ше оди­на­ко­во­го кода. Если в обыч­ном про­грам­ми­ро­ва­нии одна функ­ция счи­та­ет повто­ря­ю­щи­е­ся сим­во­лы в одно­мер­ном мас­си­ве, а дру­гая — в дву­мер­ном, то у них боль­шая часть кода будет оди­на­ко­вой. В ООП это реша­ет­ся насле­до­ва­ни­ем.
  • Слож­ные про­грам­мы пишут­ся про­ще. Каж­дую боль­шую про­грам­му мож­но раз­ло­жить на несколь­ко бло­ков, сде­лать им мини­маль­ное напол­не­ние, а потом раз за разом подроб­но напол­нить каж­дый блок.
  • Уве­ли­чи­ва­ет­ся ско­рость напи­са­ния. На стар­те мож­но быст­ро создать нуж­ные ком­по­нен­ты внут­ри про­грам­мы, что­бы полу­чить мини­маль­но рабо­та­ю­щий про­то­тип.

А теперь про мину­сы:

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

Что дальше

Впе­ре­ди нас ждёт раз­го­вор о клас­сах, объ­ек­тах и всём осталь­ном важ­ном в ООП. Кре­пи­тесь, будет инте­рес­но!

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