что такое наследование ооп
Принципы объектно-ориентированного программирования
Привет, Хабр! Меня зовут Владислав Родин. В настоящее время я являюсь руководителем курса «Архитектор высоких нагрузок» в OTUS, а также преподаю на курсах, посвященных архитектуре ПО.
Специально к старту занятий в новом потоке курса «Архитектура и шаблоны проектирования» я подготовил еще один авторский материал.
Введение
Когда речь заходит о классических паттернах проектирования, нельзя не вспомнить о самом объектно-ориентированном программировании. Ведь паттерны GoF являются паттернами именно объектно-ориентированного программирования. В функциональном же программировании есть свои собственные паттерны.
Вообще устроено все следующим образом: есть само объектно-ориентированное программирование. У него есть принципы. Из принципов объектно-ориентированного программирования следуют разобранные нам шаблоны GRASP (как вариант — SOLID принципы), из которых, в свою очередь, следуют шаблоны GoF. Из них же следует ряд интересных вещей, например, enterprise паттерны.
Объектно-ориентированная парадигма
Определение гласит, что «Объектно-ориентированное программирование – это парадигма программирования, в которой основной концепцией является понятие объекта, который отождествляется с предметной областью.»
Таким образом, система представляется в виде набора объектов предметной области, которые взаимодействуют между собой некоторым образом. Каждый объект обладает тремя cоставляющими: идентичность (identity), состояние (state) и поведение (behaviour).
Состояние объекта — это набор всех его полей и их значений.
Поведение объекта — это набор всех методов класса объекта.
Идентичность объекта — это то, что отличает один объект класса от другого объекта класса. С точки зрения Java, именно по идентичности определяется метод equals.
Принципы объектно-ориентированного программирования
Объектно-ориентированное программирование обладает рядом принципов. Представление об их количестве расходится. Кто-то утверждает, что их три (старая школа программистов), кто-то, что их четыре (новая школа программистов):
Инкапсуляция
Вопреки мнению многих собеседующихся (а иногда и собеседуемых), инкапсуляция это не «когда все поля приватные». Инкапсуляция является фундаментальнейшим принципом проектирования ПО, ее следы наблюдаются на только на уровне микро-, но и на уровне макропроектирования.
Научное определение гласит, что «Инкапсуляция – это принцип, согласно которому любой класс и в более широком смысле – любая часть системы должны рассматриваться как «черный ящик»: пользователь класса или подсистемы должен видеть только интерфейс (т.е. список декларируемых свойств и методов) и не вникать во внутреннюю реализацию.»
Таким образом, получается, что если класс A обращается к полям класса B напрямую, это приводит не к тому, что «нарушается информационная безопасность», а к тому, что класс A завязывается на внутренне устройство класса B, и попытка изменить внутреннее устройство класса B приведет к изменению класса А. Более того, класс A не просто так работает с полями класса B, он работает по некоторой бизнес-логике. То есть логика по работе с состоянием класса В лежит в классе А, и когда мы захотим переиспользовать класс В, это не удастся сделать, ведь без кусочка класса А класс В может быть бесполезным, что приведет к тому, что класс В придется отдавать вместе с классом А. Экстраполируя это на всю систему, получается, что переиспользовать можно будет только всю систему целиком.
Инкапсуляция является самым недооцененным принципом, который, к сожалению, мало кем интерпретируется правильно. Она позволяет минимизировать число связей между классами и подсистемами и, соответственно, упростить независимую реализацию и модификацию классов и подсистем.
Наследование
Наследование — это возможность порождать один класс от другого с сохранением всех свойств и методов класса-предка (суперкласса), добавляя при необходимости новые свойства и
методы.
Наследование является самым переоцененным принципом. Когда-то считалось, что «У идеального программиста дерево наследования уходит в бесконечность и заканчивается абсолютно пустым объектом», потому как когда-то люди не очень хорошо понимали то, что наследование — это способ выразить такое свойство реального мира как иерархичность, а не способ переиспользовать код, отнаследовав машину от холодильника, потому что у обоих предметов есть ручка. Наследования желательно по возможности избегать, потому что наследование является очень сильной связью. Для уменьшения количества уровней наследования рекомендуется строить дерево «снизу-вверх».
Полиморфизм
Полиморфизм — это возможность использовать классы – потомки в контексте, который был предназначен для класса – предка.
За самым садистским определением кроется возможность языка программирования для декомпозиции задачи и рефакторинга if’ов и switch’ей.
ООП. Часть 5. Наследование и ещё немного полиморфизма
Вы всё время пользуетесь результатами наследования, даже если не знаете этого. Рассказываем, как меньше дублировать код и что общего у всех классов.
Вот мы и подобрались к последнему столпу объектно-ориентированного программирования — наследованию. С его помощью можно создавать классы с общим функционалом, не копируя каждый раз одни и те же поля и методы.
Все статьи про ООП
Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Как наследовать класс
Для начала создадим класс, от которого будем наследовать. Обычно его называют базовым или родительским:
Этот класс (Vehicle) представляет собой транспортное средство, но пока у него есть только слишком общие свойства (название, координаты и скорость) и поведение (перемещение). Нам может понадобиться реализовать класс, который тоже относится к транспортным средствам, но более конкретным. Например, это будет автомобиль (Car).
Если мы хотим, чтобы класс Car наследовал поля и методы класса Vehicle, то при его объявлении после названия нужно поставить двоеточие и имя родительского класса:
Теперь объекты класса Car обладают всеми полями и методами класса Vehicle:
Внимание! Наследовать можно только от одного класса.
Добавление новых полей и методов
Чтобы добавить в дочерний класс новое поле или метод, нужно просто объявить их:
Теперь объекты этого класса могут использовать как метод Move (), так и метод Beep (). То же самое касается и полей.
Наследование конструкторов
Допустим, у родительского класса есть конструктор, который принимает один аргумент:
Все дочерние классы должны вызывать его в своих конструкторах, передавая аргумент того же типа. Для этого используется ключевое слово base:
В скобках после base указывается аргумент, который нужно передать в родительский класс. При этом повторно описывать логику присваивания name не нужно.
Если вы не хотите ничего вызывать, то просто создайте в наследуемом классе пустой конструктор.
Переопределение методов
Часто бывает нужно, чтобы какой-то метод в дочернем классе работал немного иначе, чем в родительском. Например, в методе Move () для класса Car можно прописать условие, которое будет проверять, не кончилось ли топливо. Точно так же может появиться необходимость переопределить свойство.
Методы и свойства, которые можно переопределить, называются виртуальными. В родительском классе для них указывается модификатор virtual:
А в дочернем для переопределения используется модификатор override:
Таким образом можно определить разную логику для разных классов. Это тоже можно считать полиморфизмом.
Наследование от класса Object
Несмотря на то что наследовать можно только от одного класса, существует также и класс Object, который является родительским для всех остальных. У него есть четыре метода:
Хеш — результат преобразования данных, который используется в криптографии.
Любой из них также может быть переопределён или перегружен. Например, метод Equals () можно использовать, чтобы он проверял, равны ли поля объектов:
В данном случае это именно перегрузка, потому что ни один из вариантов метода Equals () не принимал объект класса Car. Отсюда следует, что переопределить можно только метод с такими же принимаемыми аргументами.
Особенности наследования
Есть несколько особенностей, которые нужно знать при работе с наследованием:
Чтобы лучше это усвоить, стоит попробовать поработать с каждой особенностью на практике и немного поэкспериментировать.
Домашнее задание
Создайте несколько классов персонажей: например, воин, лучник и маг.
Каждый из них должен быть родительским для нескольких других классов допустим, воин будет базовым классом для рыцаря и берсеркера.
У всех персонажей должен быть метод Attack (), при вызове которого у разных персонажей будут выводиться различные сообщения. Например, если атаковать будет маг, то мы должны увидеть сообщение, что он запустил огненный шар.
Заключение
С помощью наследования можно создавать множество полезных классов с общим поведением и свойствами, при этом не дублируя код. Однако это ещё не всё, что можно использовать, — в следующей статье вы узнаете про интерфейсы и абстрактные классы.
А чтобы на практике узнать, как используется ООП со всеми его особенностями, записывайтесь на курс «C#-разработчик с 0 до PRO». Вы попробуете разрабатывать на C# сайты и десктопные приложения, выжимая максимум из объектно-ориентированного программирования.
Наследование (программирование)
Из Википедии — свободной энциклопедии
Наследование (англ. inheritance) — концепция объектно-ориентированного программирования, согласно которой абстрактный тип данных может наследовать данные и функциональность некоторого существующего типа, способствуя повторному использованию компонентов программного обеспечения.
Энциклопедичный YouTube
Субтитры
Содержание
Терминология
Суперкласс (англ. super class ), родительский класс (англ. parent class ), предок, родитель или надкласс — класс, производящий наследование в подклассах, т. е. класс, от которого наследуются другие классы. Суперклассом может быть подкласс, базовый класс, абстрактный класс и интерфейс.
Подкласс (англ. subclass ), производный класс (англ. derived class ), дочерний класс (англ. child class ), класс потомок, класс наследник или класс-реализатор — класс, наследуемый от суперкласса или интерфейса, т. е. класс определённый через наследование от другого класса или нескольких таких классов. Подклассом может быть суперкласс.
Базовый класс (англ. base class ) — это класс, находящийся на вершине иерархии наследования классов и в основании дерева подклассов, т. е. не являющийся подклассом и не имеющий наследований от других суперклассов или интерфейсов. Базовым классом может быть абстрактный класс и интерфейс. Любой не базовый класс является подклассом.
Интерфейс (англ. interface ) — это структура, определяющая чистый интерфейс класса, состоящий из абстрактных методов. Интерфейсы участвуют в иерархии наследований классов и интерфейсов.
Суперинтерфейс (англ. super interface ) или интерфейс-предок — это аналог суперкласса в иерархии наследований, т. е. это интерфейс производящий наследование в подклассах и подинтерфейсах.
Интерфейс-потомок, интерфейс-наследник или производный интерфейс (англ. derived interface ) — это аналог подкласса в иерархии наследований интерфейсов, т. е. это интерфейс наследуемый от одного или нескольких суперинтерфейсов.
Базовый интерфейс — это аналог базового класса в иерархии наследований интерфейсов, т. е. это интерфейс, находящийся на вершине иерархии наследования.
Иерархия наследования или иерархия классов — дерево, элементами которого являются классы и интерфейсы.
Применение
Наследование является механизмом повторного использования кода (англ. code reuse) и способствует независимому расширению программного обеспечения через открытые классы (англ. public classes) и интерфейсы (англ. interfaces). Установка отношения наследования между классами порождает иерархию классов (англ. class hierarchy).
Введение в ООП с примерами на C#. Часть вторая. Все, что нужно знать о наследовании
Авторизуйтесь
Введение в ООП с примерами на C#. Часть вторая. Все, что нужно знать о наследовании
Вступление
В первой статье этой серии мы рассматривали работу разных вариантов реализации перегрузки. В этой части мы сосредоточимся на таком разделе объектно-ориентированного программирования, как наследование.
Давайте сразу тезисно опишем, что такое наследование:
Рассмотрим наследование в действии
Как вы можете видеть, класс A пуст, а в B мы добавили два метода и переменную x со значением 100.
Теперь в главном методе Program.cs напишите следующее:
Разумеется, этот код вызовет ошибку:
Error: ‘InheritanceAndPolymorphism.ClassA’ does not contain a definition for ‘Display1’ and no extension method ‘Display1’ accepting a first argument of type ‘InheritanceAndPolymorphism.ClassA’ could be found (are you missing a using directive or an assembly reference?)
Очевидно, причина в том, что в классе А нет метода, который мы вызываем. Однако он есть у класса B. Было бы здорово, если бы мы могли получить доступ ко всему коду в B из A!
Теперь измените описание первого класса на следующее:
Теперь после выполнения программы мы получим:
Что нужно запомнить: как сын получается похожим на отца, наследует его черты, так и дочерний класс имеет параметры родительского.
Теперь давайте представим, что ClassA тоже имеет метод Display1 :
Что будет, если мы запустим код теперь? Каким будет вывод? И будет ли вывод вообще или выйдет ошибка компиляции? Давайте проверим.
Однако мы также получим предупреждение:
Warning: ‘InheritanceAndPolymorphism.ClassA.Display1()’ hides inherited member ‘InheritanceAndPolymorphism.ClassB.Display1()’. Use the new keyword if hiding was intended.
Что нужно запомнить: ничто не может помешать создать в дочернем классе такой же метод, как и в родительском.
Что нужно запомнить: методы дочерних классов имеют приоритет при выполнении.
Такая возможность нам даётся для того, чтобы мы могли изменить поведение методов предка, если оно нас не устраивает. Однако мы всё равно можем вызывать методы родительского класса следующим образом:
В таком случае вывод будет:
Что нужно запомнить: ключевое слово base может быть использовано для обращения к методам класса-предка.
Что же, вверх по иерархии мы обращаться можем. Давайте попробуем сделать наоборот:
Error: ‘InheritanceAndPolymorphism.ClassB’ does not contain a definition for ‘Display2’ and no extension method ‘Display2’ accepting a first argument of type ‘InheritanceAndPolymorphism.ClassB’ could be found (are you missing a using directive or an assembly reference?)
Что нужно запомнить: наследование не работает в обратном направлении.
Когда класс A наследуется от B, он получает все его методы и может их использовать. Однако методы, которые были добавлены в A, не загружаются наверх в B, наследование не имеет обратной совместимости. Если попытаться вызвать из класса-родителя метод, который создан в классе-наследнике, вы получите ошибку.
Что нужно запомнить: кроме конструкторов и деструкторов, дочерний класс получает от родителя абсолютно всё.
Если класс С, будет унаследован от класса B, который, в свою очередь, будет унаследован от класса A, то класс C унаследует члены как от класса B, так и от класса A. Это транзитивное свойство наследования. Потомок перенимает все члены родителей и не может исключить какие-либо. Он может «спрятать» их, создав свой метод с тем же именем. Конечно, это никак не повлияет на родительский класс, просто в дочернем метод не будет виден.
Автоматически воспринимается C# так:
Теперь ещё один момент. Если мы захотим сделать так:
То у нас это не получится:
‘InheritanceAndPolymorphism.ClassW’ cannot derive from special class ‘System.ValueType’
‘InheritanceAndPolymorphism.ClassX’ cannot derive from special class ‘System.Enum’
‘InheritanceAndPolymorphism.ClassY’ cannot derive from special class ‘System.Delegate’
‘InheritanceAndPolymorphism.ClassZ’ cannot derive from special class ‘System.Array’
Заметили словосочетание «special class»? Такие классы нельзя расширять.
Compile time Error: Class ‘InheritanceAndPolymorphism.ClassY’ cannot have multiple base classes: ‘InheritanceAndPolymorphism.ClassW’ and ‘ClassX’.
Что ещё нужно запомнить: класс может иметь только одного родителя, множественное наследование в C# не поддерживается (оно поддерживается у интерфейсов, но в этой статье мы о них речи не ведём).
Если мы попробуем обойти это правило таким образом:
Error: Circular base class dependency involving ‘InheritanceAndPolymorphism.ClassX’ and ‘InheritanceAndPolymorphism.ClassW’.
Что нужно запомнить: классы не могут наследоваться циклически (1-й от 2-го, 2-й от 3-го 3-й от 1-го), что, в общем-то, логично.
Операции с объектами
Здесь мы пытаемся приравнять объект от разных классов друг к другу.
Cannot implicitly convert type ‘InheritanceAndPolymorphism.ClassB’ to ‘InheritanceAndPolymorphism.ClassA’
Cannot implicitly convert type ‘InheritanceAndPolymorphism.ClassA’ to ‘InheritanceAndPolymorphism.ClassB’
Однако у нас это плохо получается. Даже несмотря на то, что они имеют одинаковые поля с одинаковыми значениями. Даже если бы эти поля имели одинаковые названия. C# работает с типами очень чётко — вы не можете приравнять два объекта от двух независимых классов. Однако, если бы класс A наследовался от B:
…мы бы продвинулсь немногим дальше:
Error: Cannot implicitly convert type ‘InheritanceAndPolymorphism.ClassB’ to ‘InheritanceAndPolymorphism.ClassA’. An explicit conversion exists (are you missing a cast?)
Как я уже говорил, C# подходит к вопросам типов очень дотошно. Класс A унаследован от B, значит, имеет все его поля и методы — при назначении переменной типа B объекта типа A проблем не возникает. Однако вы уже знаете, что в обратную сторону это не работает — в классе B нет полей и методов, которые могут быть в A.
Что нужно запомнить: вы можете назначить переменной родительского типа объект дочернего, но не наоборот.
Здесь нам наконец-то представляется шанс обмануть правило:
Приведение типа здесь сработает, но только потому, что эти классы находятся в наследственных отношениях. Два обособленных непримитивных типа привести друг к другу нельзя.
Итак, наш последний блок кода:
Error: Cannot implicitly convert type ‘int’ to ‘char’. An explicit conversion exists (are you missing a cast?)
Заключение
В этой части мы рассмотрели наследование. Мы попробовали запускать разные варианты кода, чтобы возможно глубже понять суть этого принципа. *этот текст будет изменён после перевода следующей статьи* In my next article, we’ll be discussing about run time polymorphism. Inheritance plays a very important role in run time polymorphism.
Вот что вы должны были запомнить за сегодня:
Напоминаем вам, что в первой статье этой серии вы можете прочитать о полиморфизме. Продолжайте учиться программировать с нами!
Зачем нужно понимать ООП
Часто я встречаю разработчиков, которые пишут код на объектно-ориентированном языке программирования, но не понимают принципов ООП. Это могут быть начинающие девелоперы, которые еще на собеседованиях сталкиваются с проблемами объяснения принципов. А также это могут быть, казалось бы, опытные программисты, которые не понимают принципов, заложенных в язык программирования, на котором они пишут. Второй случай хотелось бы встречать реже, но на практике это не так. Часто разработчики смотрят на наследование или полиморфизм, как на особенности языка, как на какой-то технический инструмент и не думают, о вещах, которые лежат в основе этих механизмов.
Все, что будет изложено ниже — сугубо мои размышления, я не претендую на статус истины в последней инстанции и не жду, что все примут мою точку зрения. Я надеюсь, эта статья натолкнет на размышления и даст толчок к развитию собственного понимания у каждого читателя.
Примеры кода буду приводить из iOS разработки.
Я считаю, если ты пишешь код на объектно-ориентированном языке программирования, ты обязан не только знать определения, но и понимать суть, которая вложена в эту парадигму.
На мой взгляд, если функциональное или структурное программирование — принципы которые больше относятся к написанию именно кода, то ООП это уже не про код, а моделирование сложных систем. Наш мозг воспринимает мир как набор объектов, которые взаимодействуют друг с другом. Если функция — просто действие без контекста, то метод класса это уже действие в определенном контексте, это действие, которое относится к определенному объекту. И система приобретает вид взаимодействия различных объектов между собой. Таким образом, ООП позволяет сделать описание системы более понятным для восприятия.
Также заранее хочу добавить, это описание идеального сферического программирования в вакууме и в реальности множество вещей нарушаются в угоду практичности. Но стремление к идеалу только улучшит качество кода. И не стоит забывать, что гонка за крайностью — тоже плохо.
Наследование
Что такое программирование в целом — это написание детальной инструкции, которую должна выполнить машина. К тому же эта инструкция должна быть понятной как машине, так и человеку, который будет вносить изменения в инструкцию в будущем.
Так как ООП — это про моделирование, то код мы пишем начиная с абстракции частей системы и взаимодействия между этими частями, которые мы должны записать в виде кода. Например, социальная сеть, которая состоит из пользователей, взаимодействующих друг с другом. Помимо пользователей, система состоит из более мелких компонентов, таких как сообщения, посты, лайки, комментарии. Даже сам пользователь может являть собой подсистему в системе. Как человек состоит из различных органов и частей тела: сердце, мозг, руки, пальцы, так и пользователь в системе может состоять из более мелких составляющих. Но уже не руки или глаза, а адрес, интересы, записи об образовании, которые можно выносить как отдельные объекты. Уже на этапе анализа можно проследить принципы объектно-ориентированного подхода. У нас есть пользователь — абстракция реального человека. Но у пользователя могут быть разные роли: админ, обычный пользователь, VIP пользователь, анонимный посетитель. Они все являются абстракциями реальных людей и пользователями данной системы. Но каждая из вышеперечисленных ролей имеет свои особенности и при этом все имеют общее — они пользуются системой и должны зарегистрироваться в системе (у каждого может быть свой способ) и они все должны пройти процедуру входа в систему.
Это и есть принцип наследования, где каждый админ/VIP-клиент/аноним являются пользователями, но не каждый пользователь должен быть админом или 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% от оригинального. А также я добавляю метод, который возвращает бэкграунд цвет кнопки в оригинальный, без альфа канала.
Если эти два метода будут доступны при использовании моей кастомной кнопки, то может нарушиться поведение этой кнопки. К примеру в момент нажатия можно вызвать метод, который устанавливает оригинальный цвет. Или же вдруг сделать кнопку светлее, как будто она нажата, хотя это не так.
Инкапсуляция — не бездумное сокрытие каких-то полей или методов, а проектирование класса с определенным набором возможностей, которые не должны нарушаться.
По поводу объединения данных и методов, которые обрабатывают эти данные, я с этим согласен, но с оговоркой. В моем понимании ООП — моделирование объектов в коде, что само собой подразумевает, наличие состояния и действий у этого объекта.
Если просуммировать, то инкапсуляция — проектирование самостоятельной единицы (объекта), которая выполняет некую роль в системе, имеет набор параметров и методов. При этом все, что может нарушить роль этого объекта, скрыто.