Полиморфизм это в программировании
Язык программирования 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, теперь получают «ОК» и из-за этого ломаются.
Теперь вам нужно:
- либо переписывать все функции, чтобы научить их понимать новые ответы проверяльщика адресов;
- либо переделать сам проверяльщик адресов, чтобы он остался совместимым со старыми местами, но в нужном вам месте как-то ещё выдавал коды ошибок;
- либо написать новый проверяльщик, который выдаёт коды ошибок, а в старых местах использовать старый проверяльщик.
Задача, конечно, решаемая за час-другой.
Но теперь представьте, что у вас этих функций — сотни. И изменений в них нужно делать десятки в день. И каждое изменение, как правило, заставляет функции вести себя более сложным образом и выдавать более сложный результат. И каждое изменение в одном месте ломает три других места. В итоге у вас будут нарождаться десятки клонированных функций, в которых вы сначала будете разбираться, а потом уже нет.
Это называется спагетти-код, и для борьбы с ним как раз придумали объектно-ориентированное программирование.
Объектно-ориентированное программирование
Основная задача ООП — сделать сложный код проще. Для этого программу разбивают на независимые блоки, которые мы называем объектами.
Объект — это не какая-то космическая сущность. Это всего лишь набор данных и функций — таких же, как в традиционном функциональном программировании. Можно представить, что просто взяли кусок программы и положили его в коробку и закрыли крышку. Вот эта коробка с крышками — это объект.
Программисты договорились, что данные внутри объекта будут называться свойствами, а функции — методами. Но это просто слова, по сути это те же переменные и функции.
Объект можно представить как независимый электроприбор у вас на кухне. Чайник кипятит воду, плита греет, блендер взбивает, мясорубка делает фарш. Внутри каждого устройства куча всего: моторы, контроллеры, кнопки, пружины, предохранители — но вы о них не думаете. Вы нажимаете кнопки на панели каждого прибора, и он делает то, что от него ожидается. И благодаря совместной работе этих приборов у вас получается ужин.
Объекты характеризуются четырьмя словами: инкапсуляция, абстракция, наследование и полиморфизм.
Инкапсуляция — объект независим: каждый объект устроен так, что нужные для него данные живут внутри этого объекта, а не где-то снаружи в программе. Например, если у меня есть объект «Пользователь», то у меня в нём будут все данные о пользователе: и имя, и адрес, и всё остальное. И в нём же будут методы «Проверить адрес» или «Подписать на рассылку».
Абстракция — у объекта есть «интерфейс»: у объекта есть методы и свойства, к которым мы можем обратиться извне этого объекта. Так же, как мы можем нажать кнопку на блендере. У блендера есть много всего внутри, что заставляет его работать, но на главной панели есть только кнопка. Вот эта кнопка и есть абстрактный интерфейс.
В программе мы можем сказать: «Удалить пользователя». На языке ООП это будет «пользователь.удалить()» — то есть мы обращаемся к объекту «пользователь» и вызываем метод «удалить». Кайф в том, что нам не так важно, как именно будет происходить удаление: ООП позволяет нам не думать об этом в момент обращения.
Например, над магазином работают два программиста: один пишет модуль заказа, а второй — модуль доставки. У первого в объекте «заказ» есть метод «отменить». И вот второму нужно из-за доставки отменить заказ. И он спокойно пишет: «заказ.отменить()». Ему неважно, как другой программист будет реализовывать отмену: какие он отправит письма, что запишет в базу данных, какие выведет предупреждения.
Наследование — способность к копированию. ООП позволяет создавать много объектов по образу и подобию другого объекта. Это позволяет не копипастить код по двести раз, а один раз нормально написать и потом много раз использовать.
Например, у вас может быть некий идеальный объект «Пользователь»: в нём вы прописываете всё, что может происходить с пользователем. У вас могут быть свойства: имя, возраст, адрес, номер карты. И могут быть методы «Дать скидку», «Проверить заказ», «Найти заказы», «Позвонить».
На основе этого идеального пользователя вы можете создать реального «Покупателя Ивана». У него при создании будут все свойства и методы, которые вы задали у идеального покупателя, плюс могут быть какие-то свои, если захотите.
Идеальные объекты программисты называют классами.
Полиморфизм — единый язык общения. В ООП важно, чтобы все объекты общались друг с другом на понятном им языке. И если у разных объектов есть метод «Удалить», то он должен делать именно это и писаться везде одинаково. Нельзя, чтобы у одного объекта это было «Удалить», а у другого «Стереть».
При этом внутри объекта методы могут быть реализованы по-разному. Например, удалить товар — это выдать предупреждение, а потом пометить товар в базе данных как удалённый. А удалить пользователя — это отменить его покупки, отписать от рассылки и заархивировать историю его покупок. События разные, но для программиста это неважно. У него просто есть метод «Удалить()», и он ему доверяет.
Такой подход позволяет программировать каждый модуль независимо от остальных. Главное — заранее продумать, как модули будут общаться друг с другом и по каким правилам. При таком подходе вы можете улучшить работу одного модуля, не затрагивая остальные — для всей программы неважно, что внутри каждого блока, если правила работы с ним остались прежними.
Плюсы и минусы ООП
У объектно-ориентированного программирования много плюсов, и именно поэтому этот подход использует большинство современных программистов.
- Визуально код становится проще, и его легче читать. Когда всё разбито на объекты и у них есть понятный набор правил, можно сразу понять, за что отвечает каждый объект и из чего он состоит.
- Меньше одинакового кода. Если в обычном программировании одна функция считает повторяющиеся символы в одномерном массиве, а другая — в двумерном, то у них большая часть кода будет одинаковой. В ООП это решается наследованием.
- Сложные программы пишутся проще. Каждую большую программу можно разложить на несколько блоков, сделать им минимальное наполнение, а потом раз за разом подробно наполнить каждый блок.
- Увеличивается скорость написания. На старте можно быстро создать нужные компоненты внутри программы, чтобы получить минимально работающий прототип.
А теперь про минусы:
- Сложно понять и начать работать. Подход ООП намного сложнее обычного функционального программирования — нужно знать много теории, прежде чем будет написана хоть одна строчка кода.
- Требует больше памяти. Объекты в ООП состоят из данных, интерфейсов, методов и много другого, а это занимает намного больше памяти, чем простая переменная.
- Иногда производительность кода будет ниже. Из-за особенностей подхода часть вещей может быть реализована сложнее, чем могла бы быть. Поэтому бывает такое, что ООП-программа работает медленнее, чем функциональная (хотя с современными мощностями процессоров это мало кого волнует).
Что дальше
Впереди нас ждёт разговор о классах, объектах и всём остальном важном в ООП. Крепитесь, будет интересно!