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

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

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

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

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

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

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

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

Такой подход позволяет программировать каждый модуль независимо от остальных. Главное — заранее продумать, как модули будут общаться друг с другом и по каким правилам. При таком подходе вы можете улучшить работу одного модуля, не затрагивая остальные — для всей программы неважно, что внутри каждого блока, если правила работы с ним остались прежними.
Плюсы и минусы ООП
У объектно-ориентированного программирования много плюсов, и именно поэтому этот подход использует большинство современных программистов.
- Визуально код становится проще, и его легче читать. Когда всё разбито на объекты и у них есть понятный набор правил, можно сразу понять, за что отвечает каждый объект и из чего он состоит.
- Меньше одинакового кода. Если в обычном программировании одна функция считает повторяющиеся символы в одномерном массиве, а другая — в двумерном, то у них большая часть кода будет одинаковой. В ООП это решается наследованием.
- Сложные программы пишутся проще. Каждую большую программу можно разложить на несколько блоков, сделать им минимальное наполнение, а потом раз за разом подробно наполнить каждый блок.
- Увеличивается скорость написания. На старте можно быстро создать нужные компоненты внутри программы, чтобы получить минимально работающий прототип.
А теперь про минусы:
- Сложно понять и начать работать. Подход ООП намного сложнее обычного процедурного программирования — нужно знать много теории, прежде чем будет написана хоть одна строчка кода.
- Требует больше памяти. Объекты в ООП состоят из данных, интерфейсов, методов и много другого, а это занимает намного больше памяти, чем простая переменная.
- Иногда производительность кода будет ниже. Из-за особенностей подхода часть вещей может быть реализована сложнее, чем могла бы быть. Поэтому бывает такое, что ООП-программа работает медленнее, чем процедурная (хотя с современными мощностями процессоров это мало кого волнует).
Что дальше
Впереди нас ждёт разговор о классах, объектах и всём остальном важном в ООП. Крепитесь, будет интересно!
Зачем нужно ООП
Класс — описание логики поведения (через свойства и методы).
Объект — экземпляр класса, который содержит данные и может выполнять логику класса.
Интерфейс — функции, которые должен реализовать класс.
Ещё есть наследование (когда один класс расширяет другой), полиморфизм (когда класс-потомок меняет логику поведения родителя), инкапсуляция (красивое слово, которое означает "сокрытие логики" или "давай запихаем эту хрень так, чтобы её никто никогда не нашёл"). Не расстраивайтесь, если вы не знаете точного определения какого-то из модных программерских слов, в 99% случаев можете сказать, что это "хрень какая-то", не ошибётесь.
Важно помнить: в ООП данные хранятся в объектах, а логика в классах. Объекты всегда создаются из класса.
Код без ООП ¶
Представим себя на месте программиста, которому нужно сделать простую задачу: вывести список пользователей.
Код выше выведет список всех пользователей, даже если их миллион. Если мы хотим работать со списком в более удобном формате, то нужно сделать постраничную навигацию:
Давайте добавим в вывод ещё тэги HTML, чтобы список был более красивым:
Для полного счастья здесь не хватает разделения чётных и нечётных строк. Давайте добавим:
Как видите, код получился, мягко говоря, не очень красивый и понятный. Такой код называют "смешанный" или "грязный" (ну или "говнокод"). Тут переплетается высокоуровневая логика (показ списка пользователей, постраничная навигация) и низкоуровневое форматирование (HTML-тэги, экранирование, обращение к массиву и так далее).
А что будет, если нам скажут, что имя пользователя должно быть ссылкой? А что будет, если вместо списка потребуется таблица со множеством колонок? А что будет, если нам надо будет выгрузить список в PDF? А что будет, если . (в этом месте можно подставить любые обстоятельства).
Если код оставить в том виде, в котором он сейчас есть, то рано или поздно он перестанет быть управляемым и начнёт диктовать разработчикам свои условия. То есть вместо того, чтобы добавлять новые плюшки, программист будет больше думать, как бы не сломать старое.
Философия ООП ¶
Первое, что нужно понять, ООП это лишь один из способов организации кода. С помощью объектов мы разделяем большую логику на на много маленьких, чтобы нам легче было управлять своим же кодом. Это значит, что не нужно во все места совать классы и интерфейсы. Если ООП не упрощает код, а только запутывает, то это неправильное ООП! Каждый раз спрашивайте себя: "А что я упрощу, если сделаю объект?"
Но давайте вернёмся к примеру выше и посмотрим, как можно упростить себе жизнь.
Первое, на что нужно обратить внимание, это то, что мы работаем с пользователем как с массивом:
А что будет, если свойств у пользователя будет много или ключи массива будут меняться? Тогда придётся где-то вести документацию, какие ключи можно использовать, а какие нет. И кто-то должен будет контролировать, чтобы программисты использовали только правильные ключи. Оказывается, эту задачу можно переложить на среду исполнения, если использовать ООП.
Давайте создадим класс пользователя, который на вход принимает данные пользователя и имеет функцию для возврата имени:
Исправим объявление списка:
Теперь для доступа к имени пользователя можно использовать функцию $user->name() . Мы "спрятали" (инкапсулировали) структуру исходного массива с данными пользователя за функциями класса User . Если исходный массив поменяется, то изменения нужно будет внести только в функции класса.
Дальше решим проблему с постраничной навигацией. Поскольку базовый массив не поддерживает такую навигацию, "спрячем" его в объект, а уже объект "научим" выдавать страницы. Для начала опишем класс коллекции:
Изменим объявление исходных данных:
Теперь можно обращаться к нашим данным так:
Осталось решить проблему с форматом вывода пользователей. Дело в том, что echo в этом цикле является низкоуровневой логикой, которую надо "прятать". Собственно, foreach также является лишним звеном в этой цепочке. В идеале логику отображения нужно вынести в шаблон и передавать ему список пользователей:
Шаблон представляет из себя псевдо язык разметки, в котором в теории удобно делать вёрстку:
Основная цель таких шаблонов — разделить логику отображения от логики данных. Преимущество, которое нам дают шаблоны заметны сразу: основной код стал "чище" и проще. Ходят легенды, что подобные шаблоны могут делать дизайнеры, верстальщики, папы, мамы, кошки без участия программистов. То есть налицо разделение труда (а если вспомнить историю за 5-й класс, то это уже прогресс в развитии).
Когда нужно использовать ООП ¶
Вот ситуации, когда, на мой взгляд, ООП даёт преимущества:
- Работа с базой данных Через ООП можно "скрыть" внутреннюю структуру базы от конечного пользователя (в данном случае программиста). Если база будет меняться, то это не приведёт к переписыванию всего проекта. Если вы используете объект, вам неважно, как он хранится в базе данных.
- Сложная логика С помощью ООП можно разбить сложную логику на несколько простых классов. Преимущество в том, что можно создавать универсальный интерфейс и узкоспециализированные классы. Вам не придётся менять код, если потребуется добавить "что-то похожее вон на тот класс".
- Большой проект Ахиллесовой пятой больших проектов является сильное связывание, когда один код вызывается из множества разных частей системы. В последствии такой код становится "неприкасаемым". То есть разработчики боятся в нём что-то менять, потому что неизвестно, какие части проекта после этого отвалятся. Если использовать ООП, то с такой проблемой будет разы проще справиться. Объект можно разбить, сделать фасадом или написать тест. С обычным кодом такое сделать труднее.
- Автоматическое тестирование Поскольку объекты несут в себе конечную логику (имеется в виду, что объекты имеют конечное число свойств и методов), то их можно тестировать. Вызвал один метод, проверил результат, вызвал другой метод, проверил результат и так далее. Автоматические тесты помогают избежать ошибок и за счёт этого улучшают скорость и качество разработки.
Когда НЕ нужно использовать ООП ¶
Я часто слышу мнение, что ООП нужно использовать везде и всегда. Нет! ООП должно помогать программисту писать код, а не превращать его жизнь в ад из "высоких стандартов опп".
Ситуации, когда ООП не даст вам преимуществ:
- Однофайловые скрипты Бывают ситуации, когда нужно сделать что-то быстро и одноразово. Например, выгрузку или отчёт. НЕ нужно тратить время на продумывание объектов и разнесение логики. Сделайте всё в одном файле. Это будет быстрее и проще для изменения. Удалить один файл легче, чем выковыривать потом классы по всему проекту.
- Отчёты Три раза подумайте перед тем, как пытаться сделать "универсальный построитель отчётов". Логику в больших отчётах далеко не всегда можно описать идиомами ООП. Лучше использовать однофайловые скрипты с простой линейной логикой, чем тратить время на придумывание монстров из классов.
- SQL-билдеры Чем проще SQL, тем крепче спит программист. Если вы используете ООП для генерирования SQL, то и понятия не имеете, насколько сложным будет конечный запрос. А что ещё хуже, билдеры имеют свои баги, которые приводят к скрытым ошибкам проекта. Вообще, SQL в проектах нужен только в двух местах: отчётах и репозиториях. Но в отчётах слишком сложный SQL, чтобы использовать билдер, а в репозиториях слишком простой.
- MVC в популярных PHP-фрейморках Я уже писал о проблемах MVC в PHP. Если кратко, то использование MVC приносит больше вреда, чем пользы, из-за неправильного понимания самого шаблона. Классы-контроллеры пухнут и превращаются в монстров, которыми сложно управлять. Единственный плюс MVC, это наличие шаблонов. Но шаблоны можно подключить и без MVC,
Не забывайте, что суть программирования — постоянный баланс между технологиями и задачами, которые надо решать. ООП лишь одна из техник написания кода, которая имеет свои достоинства и недостатки. Не нужно зацикливаться на какой-то одной технике, игнорируя преимущества других. Есть ещё и функции, и старый добрый include . Самое главное, соблюдать баланс и писать простой и понятный код (используйте метод утёнка, чтобы объяснить логику своего кода).
Зачем нужно понимать ООП

Часто я встречаю разработчиков, которые пишут код на объектно-ориентированном языке программирования, но не понимают принципов ООП. Это могут быть начинающие девелоперы, которые еще на собеседованиях сталкиваются с проблемами объяснения принципов. А также это могут быть, казалось бы, опытные программисты, которые не понимают принципов, заложенных в язык программирования, на котором они пишут. Второй случай хотелось бы встречать реже, но на практике это не так. Часто разработчики смотрят на наследование или полиморфизм, как на особенности языка, как на какой-то технический инструмент и не думают, о вещах, которые лежат в основе этих механизмов.
Все, что будет изложено ниже — сугубо мои размышления, я не претендую на статус истины в последней инстанции и не жду, что все примут мою точку зрения. Я надеюсь, эта статья натолкнет на размышления и даст толчок к развитию собственного понимания у каждого читателя.
Примеры кода буду приводить из iOS разработки.
На мой взгляд, если функциональное или структурное программирование — принципы которые больше относятся к написанию именно кода, то ООП это уже не про код, а моделирование сложных систем. Наш мозг воспринимает мир как набор объектов, которые взаимодействуют друг с другом. Если функция — просто действие без контекста, то метод класса это уже действие в определенном контексте, это действие, которое относится к определенному объекту. И система приобретает вид взаимодействия различных объектов между собой. Таким образом, ООП позволяет сделать описание системы более понятным для восприятия.
Наследование
Что такое программирование в целом — это написание детальной инструкции, которую должна выполнить машина. К тому же эта инструкция должна быть понятной как машине, так и человеку, который будет вносить изменения в инструкцию в будущем.
Так как ООП — это про моделирование, то код мы пишем начиная с абстракции частей системы и взаимодействия между этими частями, которые мы должны записать в виде кода. Например, социальная сеть, которая состоит из пользователей, взаимодействующих друг с другом. Помимо пользователей, система состоит из более мелких компонентов, таких как сообщения, посты, лайки, комментарии. Даже сам пользователь может являть собой подсистему в системе. Как человек состоит из различных органов и частей тела: сердце, мозг, руки, пальцы, так и пользователь в системе может состоять из более мелких составляющих. Но уже не руки или глаза, а адрес, интересы, записи об образовании, которые можно выносить как отдельные объекты. Уже на этапе анализа можно проследить принципы объектно-ориентированного подхода. У нас есть пользователь — абстракция реального человека. Но у пользователя могут быть разные роли: админ, обычный пользователь, VIP пользователь, анонимный посетитель. Они все являются абстракциями реальных людей и пользователями данной системы. Но каждая из вышеперечисленных ролей имеет свои особенности и при этом все имеют общее — они пользуются системой и должны зарегистрироваться в системе (у каждого может быть свой способ) и они все должны пройти процедуру входа в систему.
Неправильное понимание принципа приводит к ошибкам в коде. Типичная ошибка — если есть общие поля или методы, значит нужно делать базовый класс, хотя классы наследники по логике не имеют ничего общего.
Еще пример ошибочной трактовки принципа наследования, это когда базовый класс и наследник являются представителями разных логических групп. Выглядит это следующим образом, реализуем MVC в iOS проекте, где UIViewController это Controller с абстрактными методами, которые должен реализовать наследник. А наследник — это уже Model. Там где по логике проектирования должно быть взаимодействие между двумя группами классов, один класс становиться одновременно и Model и Controller. Не говорим уже о том, что UIViewController в реалиях iOS разработки еще и берет на себя роль View. В итоге мы получаем один объект, который делает все сам. Если у нас есть пользователь (User), то он будет и Controller и View одновременно.
Представляю эту картину в реальной жизни, у нас есть регистратура, где некий администратор ведет записи о посетителях в тетрадку. Также в этой тетради можно читать данные о посетителях. Мы получаем посетителя — Model, человека в регистратуре — Controller и тетрадь View. И в вышеуказанной интерпретации принципа наследования мы получаем, что посетитель всегда является регистратором и тетрадкой. Уже дико выглядит то, что человек и тетрадка одно целое. И логика наследования нарушается. Если рассматривать, что посетитель и регистратор — люди и принять тот факт, что представитель класса регистратора может стать посетителем, то логичней сказать, что регистратор является наследником посетителя.
По принципу наследования, базовый класс содержит общие свойства для некоторой группы наследников, которая связана общим логическим смыслом. Если наследники не имеют общего смысла, а просто имеют случайные общие свойства, то скорей всего наследование неправильно реализовано.
Абстракция
Я уже затронул принцип наследования, хотя хотелось бы начать с такого принципа как абстракция. На мой взгляд это базовый принцип ООП (также абстракция относится и к остальным парадигмам) и он незаслуженно перешел в разряд опционального принципа.
Как это выглядит: когда мы описываем что-то, мы упоминаем только о тех вещах, которые важны в нашем повествовании. Например, когда парень рассказывает другу о том, как в салоне видел крутую машину, он говорит о важных для них вещах: мощность двигателя, систему тормозов, диаметр колес. Хотя особенностей автомобиля безграничное множество. В своем разговоре ребята не упоминают о молекулярном составе автомобиля, хотя такая характеристика определенно существует у физического тела. Незнание законов физики заставляет упустить такой показатель как сила трения между определенными деталями. Даже дело не в образовании, в данном разговоре все эти детали не важны, они упускаются.
В этом примере используется модель описания автомобиля с набором только необходимых качеств.Так же и в программировании. Когда будет создаваться мобильное приложения для продажи автомобилей, программисту определенно нужно будет описать модель этого автомобиля. Естественно разработчик не будет писать модель у которой 100500 полей и методов.
Наверно пренебрежением этим принципом в реальной разработке, является добавление ненужных методов и свойств классу. Впоследствии, получаем непонимание и нарушение принципа единственной ответственности с SOLID.
Также к абстракции я бы отнес декомпозицию, когда сложный объект разбивается на систему. Мы абстрагируемся от некоторых особенностей и переносим их в отдельный компонент. Пример: пользователь у которого есть место проживания, то есть адрес. Адрес в свою очередь состоит из города, улицы, номера дома и т. д. В этот момент мы думаем, а нужно ли указывать страну или регион? Если это приложения для пользования администрацией конкретного района города, то можно упустить такие детали. В итоге мы получаем пользователя, который абстрагируется от некоторых деталей адреса. Опять-таки, непонимание того, что мы не только пишем код, но и занимаемся моделированием, приводит к тому, что у нас есть, допустим, MenuViewController, который состоит из 5000+ строк кода.
Полиморфизм
Следующим стоило бы написать об инкапсуляции, принцип, наверное, самый спорный и интересный. Так что я его оставлю на закуску. Наследование уже упомянул, так что можно перейти к полиморфизму.
Полиморфизм плавно вытекает из наследования. Гласит он следующее: можно создавать классы наследники, которые будут имитировать интерфейс базового класса, но со своей собственной реализацией. Этот принцип отражается в таком принципе SOLID как принцип Барбары Лисков: мы можем подставлять объекты классов наследников там, где предполагается использование базового класса, при этом замена не должна никак себя проявлять.
Если взять пример с регистратурой, когда у нас есть просто человек (посетитель, не будем абстрагироваться и делать посетителя наследником человека) и есть регистратор. Должна быть возможность регистратору из другого отдела пройти через текущую регистратуру как обычному посетителю. В жизни это вполне реальный пример.
Пример в коде: множество наследников UIViewController, которые пушатся, презентятся и добавляются в UITabBarController как обычные UIViewController.
Как по мне, самый простой принцип. Но он довольно часто нарушается, когда класс наследник превращается в что-то новое и его уже нельзя использовать там, где использовался базовый класс. Также к нарушению относятся многочисленные опциональные методы и поля, которые в ходе неправильного наследования не нужны классу-наследнику. В этот момент, когда предполагается выполнение определенного метода базового класса, ничего не происходит. В лучшем случае ничего не произойдет, но может получиться так, что приложение либо неадекватно начинает себя вести либо крашится вовсе.
Пример: есть базовый класс автомобиль, который предположительно должен заехать в гараж. Создаем летающий автомобиль-наследник (как в фильме «Назад в будущее 2»). Какой будет результат при попытке загнать этого монстра в гараж? Да, он может залететь в гараж, если функция езды будет полностью заменена на функцию полета. А если функция полета была новым функционалом, а функция езды вообще заблокирована? То мы не сможем спрятать нашего летуна от непогоды. Это уже не будет автомобиль, это будет что-то новое. Результат — из-за неправильного моделирования и наследования был нарушен принцип полиморфизма и получился нежелательный результат.

Инкапсуляция
И вот пришла очередь инкапсуляции. Ходит много дискуссий, что же собой представляет инкапсуляция. И существует как минимум два определения:
Инкапсуляция — это сокрытие методов и полей класса, которые не нужны при использовании объектов этого класса.
Инкапсуляция — это обьединение данных и методов, которые обрабатывают эти данные.
Первый вариант иногда воспринимается как инкапсуляция = сокрытие, что, как я считаю, не совсем верное понимание.
Для меня инкапсуляция — это проектирование класса таким образом, чтобы скрыть те методы и поля, которые могут нарушить логику работы класса, заложенную в этот класс. То есть, инкапсуляция != сокрытие. Сокрытие это механизм, который попросту скрывает часть возможностей класса, тогда как инкапсуляция отвечает за то, какие возможности будут доступны, а какие могут нарушить абстракцию этого класса и, соответственно, должны быть скрыты.
К примеру, я разрабатываю кнопку, которая в момент нажатия становится немного светлее. Проще говоря, когда кнопка в состоянии “нажата”, цвет фона становится светлее.
Для этого я создаю класс наследник UIButton и в наследнике добавляю метод, который устанавливает цвет кнопки в цвет с альфа каналом 50% от оригинального. А также я добавляю метод, который возвращает бэкграунд цвет кнопки в оригинальный, без альфа канала.
Если эти два метода будут доступны при использовании моей кастомной кнопки, то может нарушиться поведение этой кнопки. К примеру в момент нажатия можно вызвать метод, который устанавливает оригинальный цвет. Или же вдруг сделать кнопку светлее, как будто она нажата, хотя это не так.
По поводу объединения данных и методов, которые обрабатывают эти данные, я с этим согласен, но с оговоркой. В моем понимании ООП — моделирование объектов в коде, что само собой подразумевает, наличие состояния и действий у этого объекта.