Вселенная программирования. ООП.
Продолжаю развивать тему «Вселенная программирования». В данной статье я хотел бы немного затронуть парадигму ООП (ну или концепцию, я больше склоняюсь к тому, что ООП это концепция в рамках императивной парадигмы, нежели отдельная парадигма). Ну или точнее, показать, что абстракция данных, рассматриваемая в предыдущих статьях, применительно к сущности «объект», получила свое воплощение в отдельной концепции – ООП.
На сегодняшний день, наверное, только ленивый не слышал об объектно-ориентированном программировании (ООП). Эту тему очень любят спрашивать на собеседованиях. Я сам буквально недавно сталкивался с этим. Причем мой ответ не очень удовлетворил интервьюверов. Да и сам я понимал, что мои объяснения принципов ООП «своими словами» звучали немного размыто. Так что, давайте разбираться во всем в данной статье из «Вселенной».
ООП, как методология, появилась еще в 60х годах, тогда в Массачусетском технологическом университете Алан Кэй, и чуть позднее Иван Сазерленд определяет понятия «объект» и «экземпляр» и связывает их с концепцией классов. Однако, в доминирующую методологию ООП войдет гораздо позднее, примерно в начале 90х.
Сама идеология ООП создавалась как связь поведения сущности (объекта) с её данными в рамках идеи спроецировать объекты реального мира в программный код. В итоге мы имеем высокоуровневую абстракцию, которая облегчает взаимодействие для разработчиков, т. к. людям свойственно воспринимать окружающий мир, как множество взаимодействующих между собой объектов, поддающихся определенной классификации. Идею ООП описывают ее важнейшие принципы. Да-да, те самые всем до боли известные:
- Полиморфизм.
- Наследование.
- Инкапсуляция.
Те самые принципы, о которых так любят все спрашивать, и ждут совершенно типовых ответов. Но давайте посмотрим на них с точки зрения абстракций, о которых мы говорили ранее во «Вселенной программирования».
Полиморфизм.
Важнейший принцип ООП. Сущность можно назвать полиморфной, если она может работать с аргументами различных типов.
В более техническом смысле, полиморфизм - это автоматический вызов корректного метода соответствующего класса-потомка, полностью совпадающего по сигнатуре с родительским методом предка (механизм переопределения методов). То есть выбор подходящего метода в момент его вызова будет выполнен автоматически в зависимости от типов конкретных аргументов.
Если говорить о семантике полиморфизма, то полиморфизм – это способность системы использовать объекты с одинаковым интерфейсом (сигнатурой) без учета информации о внутренней реализации этих объектов.
Давайте рассмотрим следующий пример:
Водитель приехал в сервис с задачей починить свой автомобиль. Так вот, автомобиль будет отремонтирован автосервисом в любом случае, но с учетом специализации мастера-ремонтника.
В данном случае задача «починить» - полиморфна. Она может быть поставлена любому типу мастера-ремонтника, потому что все ремонтники умеют ремонтировать автомобиль, но каждый делает это с учетом своей специализации.
Для ясности, я приведу пример, используя псевдокод.
ВодительАлекс = Водитель(…) – наследник класса Водитель.
МастерРемонтаДжон = Моторист(…) – Наследник класса РемонтникАвтомобилей.
ВодительАлекс.ПочинитьАвтомобиль(МастерРемонтаДжон)
В данном примере в метод ПочинитьАвтомобиль()
класса Водитель
в качестве аргументов можно передавать не просто наследников класса РемонтникАвтомобилей
но и другие классы, например, ЗаправщикКондиционера
, которые поддерживают (реализуют) интерфейс (соответствует сигнатуре), требуемый методом ПочинитьАвтомобиль()
.
Ну или более простой пример, так сказать, более примитивное ООП.
Автомобиль() – родительский класс (АТД)
ЛегковойАвтомобиль(Автомобиль) – Наследник класса Автомобиль,
реализующий его абстрактные методы.
ФордФокус = ЛегковойАвтомобиль() – Экземпляр класса ЛегковойАвтомобиль.
ФордФокус.ЗапускДвигателя() – будет вызван конкретный метод
ЗапускДвигателя() класса ЛегковойАвтомобиль. При этом
родительский АТД Автомобиль обязательно содержит свой
собственный абстрактный метод ЗапускДвигателя().
Наследование.
Избыточность кода является большой проблемой при разработке программных систем. Повторяющийся код служит источником ошибок, потому что, если баг был обнаружен и пофиксен в одном месте, то вполне вероятно, что могли упустить все повторяющиеся участки данного кода. Тоже само касается и внесения изменений в код. Каждый раз необходимо искать все повторяющиеся фрагменты.
Наследование можно охарактеризовать как определение (создание) абстракций с выделением их общих взаимосвязей, но без повторений общих частей кода.
Другими словами, наследование – это свойство системы, позволяющее описать новый класс на основе уже существующего с частично или полностью заимствующейся функциональностью.
Класс, от которого производится наследование, называется базовым или родительским. Новый класс – потомком, наследником или производным классом (хотя правильнее всего будет называть его подтипом - subtype).
Необходимо отметить, что класс-потомок полностью удовлетворяет спецификации родительского класса, однако может иметь дополнительную функциональность (расширяет родительскую функциональность). Если мы говорим о построении более-менее сложной иерархии наследования, используя абстракцию данных, то с точки зрения интерфейсов (или АТД), каждый класс-наследник полностью реализует интерфейс (абстракцию) родительского класса. И из этого мы плавно переходим к методу подстановки.
В серьезном взрослом программировании наследование должно отвечать принципу подстановки Барбары Лисков (Буква L из правил SOLID):
Допустим у нас есть небольшая иерархия наследования:
Родительский класс Parrent
, и его класс-потомок (подтип) Child
. (Parrent --> Child
).
Также у нас есть функция, однозначно корректно работающая с входным параметром arg
:
function(arg)
, где arg
– это объект типа Parrent
.
Так вот метод подстановки Барбары Лисков (Liskov substitution principle, LSP) заключается в том, что если мы подставим в нашу function(arg)
, аргумет arg
типа Child
(подтип Parrent
), то функция должна также корректно работать, как и работала с аргументом родительского типа.
Это говорит о том, что подклассы (или классы наследники), должны поддерживать родительский функционал и не должны менять родительские интерфейсы.
Наследование, как концепция относится к понятию «is-a» то есть класс «является» подклассом другого класса, в отличие от другой концепции композиции, которая относится к понятию «has-a», что означает данный класс «содержит» в себе (или ссылается на) экземпляр другого класса. При сложном проектировании программных систем, использование наследования лучше избегать, и если уж избежать этого не получается, то наследование должно соответствовать LSP (принципу подстановки описанному выше). Во всех остальных случаях, лучше использовать композицию.
Но это тема уже для отдельного разговора.
Инкапсуляция.
Если прямо и дословно, то инкапсуляция - это сокрытие реализации внутри сущности, концепция черного ящика.
Если более детально разобрать этот принцип, то можно сказать, что инкапсуляция - это такое свойство системы, позволяющее объединить данные и допустимые методы работы с этими данными внутри сущности (класса или объекта). То есть это сокрытие и защита внутреннего состояния объекта от внешнего мира, и предоставление возможности изменять это состояние только при помощи допустимых методов (public) этого же объекта.
Получается, что инкапсуляция — это не просто сокрытие. А вот сокрытие - это следствие инкапсуляции (обертывании) данных и методов внутри класса. И, соответственно, из этого и вытекает механизм модификации доступа (Public, Protected, Private), при помощи которого разработчик может определять различные градации доступа к внутренней реализации своего класса. Напомню:
-
Public – доступ к данным полностью разрешен из вне.
-
Protected - доступ к данным возможен только внутри своего класса или из классов-предков, которые состоят в иерархии наследования к данному классу.
-
Private – доступ к данным возможет только внутри своего класса.
На этом по ООП я пожалуй закончу. Конечно ООП является огромной темой для отдельного изучения и правильного понимания, и безусловно данная концепция (парадигма) стала одной из важнейших и опорных тем в мире программирования.