Необходимое введение Я не гарантирую, что представленные здесь трактовки общепринятых терминов и принципов совпадают с тем, что изложили калифорнийские профессора в солидных научных статьях второй половины прошлого века.
Я не гарантирую, что мои интерпретации полностью разделяются большинством ИТ-специалистов в промышленности или научных кругах.
Я даже не гарантирую, что мои интерпретации помогут вам на собеседовании, хотя предполагаю, что они будут полезны.
Но я гарантирую, что если вы замените отсутствие какого-либо понимания моими интерпретациями и начнете их применять, то написанный вами код будет легче поддерживать и изменять.
Я также прекрасно понимаю, что в комментариях написанное мною будет яростно дополняться, что позволит исправить весьма вопиющие упущения и нестыковки.
Такие маленькие гарантии вызывают вопросы о причинах написания статьи.
Я считаю, что этим вещам нужно учить везде, где преподают программирование, вплоть до уроков информатики в школах с ее углубленным изучением.
Однако для меня стала пугающе нормальной ситуация, когда я узнаю, что собеседник — мой коллега, работает уже несколько лет, но он «что-то слышал об инкапсуляции».
Необходимость собрать все это в одном месте и дать ссылку при возникновении вопросов назревала уже давно.
И тут мой «пет-проект» дал мне много пищи для размышлений.
Тут мне могут возразить, что рано учить этим вещам в школе и вообще свет на ООП не сошелся.
Во-первых, это зависит от того, как вы учите.
Во-вторых, 70% материала этой статьи относится не только к ООП.
Что я отмечу отдельно.
ООП в двух словах
Для меня это, пожалуй, самый сложный раздел.Но все же необходимо заложить основу и очень кратко описать, в чем заключается сама суть ООП, чтобы было понятно, почему инкапсуляция, полиморфизм, наследование и принципы SOLID усилили его.
И я сделаю это, рассказав о том, как можно было такое придумать.
Все началось с Дейкстры, который доказал, что любой алгоритм можно выразить тремя способами выбора следующей инструкции: линейное выполнение (по порядку), условное ветвление, выполнение цикла, пока условие выполнено.
Используя эти три связи, можно построить любой алгоритм.
Более того, рекомендовалось писать программы, ограничивающиеся линейным расположением команд одна за другой, ветвлением и циклами.
Это называлось "процедурное структурное программирование" (спасибо за разъяснения сшиков ).
Здесь также отметим, что последовательности команд должны быть объединены в подпрограммы и каждая подпрограмма может выполняться с помощью одной команды.
Помимо порядка действий для нас важно еще и то, над чем выполняются действия.
И выполняются они над данными, которые стало принято хранить в переменных.
Переменные хранят данные.
Они интерпретируются в зависимости от их типа.
Очевидно, до скрежета зубов, но потерпите меня немного, пожалуйста.
С самого начала сформировался более-менее общий набор примитивных типов данных.
Целые числа, действительные числа, логические переменные, массивы, строки.
Алгоритмы + структуры данных = программы, как завещал Никлаус Вирт. Также с самого начала времен существовал такой тип данных как подпрограмма в разных формах.
Или кусок кода, если хотите.
Кто-то может сказать, что использование подпрограмм в качестве переменных — это область функционального программирования.
Даже если так, то возможность создать кусок переменного кода есть даже в ассемблере.
Пусть эта возможность сводится к «ну вот номер байта в оперативной памяти, где живет эта подпрограмма, а потом команда CALL со стеком вызовов в зубах и крутись как умеешь».
Конечно, нескольких типов данных было недостаточно и люди начали задумываться о добавлении возможности создания собственных типов на различных языках.
Одной из разновидностей этой возможности были так называемые рекорды.
Ниже приведены два примера написания на несуществующем языке программирования (далее NEPL — Non-Existing Programming Language):
То есть вместо того, чтобы перетаскивать две-три связанные по смыслу переменные, вы группируете их в одну структуру с именованными полями.type Name: record consisting of FirstName: String, MiddleName: String, LastName: String. type Point: record consisting of X: Double, Y: Double.
Затем вы объявляете переменную типа Name и получаете доступ, например, к полю FirstName. Что такого ценного в этой «расширенной» переменной для нашей темы? Дело в том, что отсюда до ООП остался всего один шаг.
Я не зря посвятил целый жирный абзац указанию, что куски кода тоже можно помещать в переменные.
Посмотрите, как переменные превращаются в объекты: type Name: class consisting of
FirstName: String,
MiddleName: String,
LastName: String,
GetFullName: subprogram with no parameters returns String.
type Point: class consisting of
X: Double,
Y: Double,
ScalarMultiply: subprogram with (Double) parameters returns Point.
Н.
Б.
NEPL активно развивается и уже заменил ключевое слово Record на class. То есть мы можем получить доступ к полю «GetFullName» и позвони ему .
Переменная содержит не только данные, описывающие ее состояние, но и поведение.
Таким образом, переменная превращается в объект, обладающий некоторыми навыками и состоянием.
И мы уже работаем не просто с переменными, а с небольшими системами, которым можно подавать команды.
В юности эта идея меня увлекла.
Просто подумай об этом вы можете создать любой тип данных .
И вы работаете не с какими-то цифрами, а с объектами создаваемого вами мира.
Больше не нужно мучиться со скучными массивами или замысловатыми наборами чисел.
Мы работаем напрямую с объектами типов Игрок, Враг, Пуля, Босс! Да, я хотел создавать видеоигры, когда был молод. На деле все оказалось не так просто.
А без каких-то «улучшающих» идей ООП превратит жизнь программиста в ад. Но прежде чем двигаться дальше, давайте дадим еще несколько терминов:
- Типы данных, «улучшенные» их поведением в ООП, называются занятия .
- Переменные такого типа называются объекты .
- А подпрограммы, задающие поведение объектов, называются методы .
Как правило, свой набор методов имеет каждый класс, а не каждый объект. Чтобы каждый объект определенного класса вел себя как другие объекты того же класса.
Буду рад узнать из комментариев о языках, где ситуация иная.
"Святая Троица"
Исторически сложилось так, что об этих вещах спрашивают на собеседованиях.О них написано в любом учебнике по ООП-языку.
Почему? Потому что если вы спроектируете ООП-программу без учета инкапсуляции и полиморфизма, вы получите «гроб, гроб, кладбище, невозможность сопровождения».
Наследование больше не является строго необходимым, но эта концепция позволяет лучше понять ООП и является одним из основных инструментов при проектировании с использованием ООП.
Инкапсуляция
Что ж, начнем с определения из Википедии: «упаковка данных и функций в один компонент».Определение кажется ясным, но в то же время слишком общим.
Поэтому давайте поговорим о том, зачем это вообще нужно, что нам это даст и как именно нам следует упаковать данные и функции в один компонент. Я уже написал статью, посвященную инкапсуляции.
И там меня справедливо упрекали в том, что я свел инкапсуляцию к сокрытию информации, а это несколько разные вещи.
В частности, ИнженерСпок дал такую изящную формулировку, как инвариантная защита .
Я признаю свою ошибку и здесь объясню, почему я это сделал.
А пока мое предварительное определение принципа инкапсуляции, или, если хотите, процесса инкапсуляции, которое дает описание не только принципа инкапсуляции, но и того, что предполагается достичь с его помощью: Любой программный объект, имеющий нетривиальное состояние, необходимо превратить в закрытую систему, которую можно лишь перевести из одного правильного состояния в другое.
О той части, где «любой программный объект, имеющий нетривиальное состояние», поговорим чуть позже.
Пока мы будем говорить исключительно об объектах.
И по поводу второй части моего определения.
Зачем нам это надо? Здесь все просто: то, что можно только перевести из одного правильного состояния в другое, сломать невозможно.
То есть нам нужно сделать так, чтобы любой предмет невозможно было сломать.
Звучит, мягко говоря, амбициозно.
Как этого можно достичь? В нулевые все, что касается объекта, должно лежать в одном месте, в одной архитектурной границе, так сказать.
Если это покажется слишком умным, повторю определение из Википедии: «упаковка данных и функций в один компонент».
Во-первых, четко разделите интерфейс и его реализацию.
Думаю, всем моим коллегам знакома аббревиатура API .
Итак, если быть дотошным, каждый объект должен иметь свой собственный API или PI. Для чего он создан, и чем будут пользоваться другие, как именно они будут называться.
Каким оно должно быть? Чтобы никому и в голову не пришло проникнуть внутрь объекта и использовать его не по назначению.
Но не более того.
В какой-то книге, увы, не помню в какой, это объяснялось на примере микроволновой печи.
У него есть кнопки.
Ручки.
Они позволяют разогревать еду.
Чтобы разогреть вчерашний суп, не обязательно включать микроволновку и что-либо припаивать.
У вас есть интерфейс, кнопки.
Поставьте тарелку, нажмите пару кнопок, подождите минутку и будьте счастливы.
Теперь подумайте, какие кнопки нужно нажимать пользователю вашего объекта, и отделите их от внутренних внутренностей.
И ни в коем случае не добавляйте лишних кнопок! Это было первое.
Во-вторых, Уважайте границу между интерфейсом и реализацией и заставляйте других уважать ее.
В принципе, эта идея интуитивна и во многих формах плавает в народной мудрости.
Возьмем, к примеру, «если вы использовали что-то недокументированное, а потом что-то сломалось — это ваша вина».
Думаю, все понятно с «не раскручивай микроволновку, пока она работает так, как ты хочешь».
Теперь о том, как заставить других уважать пресловутую границу.
Вот тут-то и приходит на помощь сокрытие информации.
Да, вы всегда можете договориться, спросить, установить соглашения по коду, указать код-ревью, что это невозможно.
Но сама возможность выхода за эту границу останется.
Вот тут-то и приходит на помощь сокрытие информации.
Мы не сможем пересечь пресловутую границу, если наш код не сможет распознать ее существование и то, что находится за ее пределами.
А даже если и узнает, то компилятор сделает вид, что такого поля или метода нет, а даже если и есть, то трогать его нельзя, я отказываюсь компилировать, да и кто вы вообще такие, мы этого не делали.
Чтобы позвонить вам, используйте интерфейсную часть.
Здесь в игру вступают всевозможные публичные, приватные и другие модификаторы доступа из вашего любимого языка.
Это самое «сокрытие информации» — самый надежный способ не упустить преимущества инкапсуляции впустую.
Как ни крути, нет смысла группировать все, что относится к одному классу, в одном месте, если код использует что хочет и откуда хочет. А вот с сокрытием информации такой ситуации больше возникнуть не должно в принципе.
И этот метод настолько надежен, что в сознании тысяч и тысяч программистов (включая меня, конечно) разница между инкапсуляцией и сокрытием информации как-то стирается.
Что делать, если любимый язык совершенно не позволяет скрыть информацию? Вы можете весело провести время, обмениваясь комментариями по этой теме.
Я вижу следующий выход. Все чаще:
- Документируйте только интерфейсную часть и считайте все, что не задокументировано, реализацией.
- Отделите реализацию от интерфейса через код-соглашение (например, в питоне есть переменная __all__, которая указывает, что именно будет импортировано из модуля, когда вы попросите импортировать все).
- Сделайте эти самые соглашения кода достаточно строгими, чтобы их можно было проверять автоматически, после чего любое их нарушение будет приравниваться к ошибке компиляции и неудачной сборке.
- Все, что относится к одному классу, упаковано в один модуль.
- Между классами проводятся строгие архитектурные границы.
- Для любого класса интерфейсная часть отделена от реализации этой самой интерфейсной части.
- Вы должны сами уважать границы между классами и заставлять уважать других!
type Microwave: class consisting of
private fancyInnerChips: List of Chip,
private foodWarmingThing: FoodWarmerController,
private buttonsPanel: ButtonsPanel,
public GetAccessToControlPanel: subprogram with no parameters returns ButtonsPanel,
public OpenDoor: subprogram with no parameters returns nothing,
public Put: subprogram with (Food) parameters return nothing,
public CloseDoor: subprogram with no parameters returns nothing.
type ButtonsPanel: class consisting of
private buttons: List of ButtonState,
public PressOn: subprogram with no parameters returns nothing,
public PressOff: subprogram with no parameters returns nothing,
public PressIncreaseTime: subprogram with no parameters returns nothing,
public PressDecreaseTime: subprogram with no parameters returns nothing,
public PressStart: subprogram with no parameters returns nothing,
public PressStop: subprogram with no parameters returns nothing.
Надеюсь, из кода понятно, в чем суть примера.
Уточню только один момент: GetAccessToControlPanel проверяет, можем ли мы вообще прикоснуться к микроволновке.
Что, если он сломан? Тогда вы не сможете ничего нажать.
Вы можете получить только сообщение об ошибке.
Ну а тот факт, что ButtonsPanel стал отдельным классом, плавно подводит нас к важному вопросу: что такое «единый компонент» из определения инкапсуляции в Википедии? Где и как должны проходить границы между классами? Мы обязательно вернемся к этому вопросу чуть позже.
Спойлер Принцип единой ответственности
Использование вне ООП
Многие программисты узнали об инкапсуляции из учебника по Java/C++/C#/подставьте свой первый ООП-язык.Поэтому инкапсуляция в массовом сознании так или иначе связана с ООП.
Но давайте вернемся к двум определениям инкапсуляции.
Упаковка данных и функций в один компонент.
Любой программный объект, имеющий нетривиальное состояние, необходимо превратить в закрытую систему, которую можно лишь перевести из одного правильного состояния в другое.Ты заметил? О классах и объектах вообще ничего нет! Итак, пример.
Ты администратор баз данных .
Ваша работа — следить за какой-то реляционной базой данных.
Пусть это будет, например, в MySQL. Несколько программ используют вашу драгоценную базу данных.
Некоторые из них вы не можете контролировать.
Что делать? Создаем набор хранимых процедур.
Давайте объединим их в одну диаграмму, которую назовем интерфейсом.
Для наших программ мы создаем одного пользователя без каких-либо прав.
Это команда CREATE USER. Затем с помощью команды GRANT пользователю предоставляется только право выполнять эти хранимые процедуры из схемы интерфейса.
Все.
У нас есть база данных, та самая программная сущность с нетривиальным состоянием, которую довольно легко взломать.
И чтобы его не сломать, мы создаем интерфейс из хранимых процедур.
А затем, используя сам MySQL, делаем так, чтобы сторонние сущности могли использовать только этот интерфейс.
Обратите внимание, что пресловутая инкапсуляция, как она есть и как она была описана, используется на полную катушку.
Но разрыв между реляционным представлением данных и объектов таков, что его приходится закрывать громоздкими ОРМ -фреймворки.
Вот почему классы и объекты не включены в определение инкапсуляции.
Идея гораздо шире, чем ООП.
И это приносит слишком много преимуществ, чтобы их можно было обсуждать только в учебниках по ООП-языкам.
Полиморфизм
Полиморфизм имеет множество форм и определений.Достаточно было того, что Кондратий схватил меня, когда я открыла Википедию.
Здесь я буду говорить о полиморфизме, как его сформулировал Страуструп: один интерфейс — множество реализаций .
В таком виде идея полиморфизма может значительно укрепить позиции тех, кто пишет программы с прицелом на инкапсуляцию.
Ведь если мы отделили интерфейс от реализации, то тем, кто использует интерфейс, не обязательно знать, что в реализации что-то изменилось.
Тому, кто пользуется интерфейсом (в идеале), даже не обязательно знать, что реализация вообще изменилась! И это открывает безграничные возможности для расширения и модификации.
Ваш предшественник решил, что лучше всего разогревать еду с помощью военного радара? Если бы этот безумный гений отделил интерфейс от реализации и четко формализовал его, то военный радар можно было бы адаптировать для других нужд, а его интерфейс для разогрева еды можно было бы реализовать с помощью микроволновой печи.
NEPL быстро развивается и под влиянием C# обрастает (будьте осторожны, не споткнитесь о формулировке) такого типа типов данных, как интерфейс.
type FoodWarmer: interface consisting of
GetAccessToControlPanel: no parameters returns FoodWarmerControlPanel,
OpenDoor: no parameters returns nothing,
Put: have (Food) parameters returns nothing,
CloseDoor: no parameters returns nothing.
type FoodWarmerControlPanel: interface consisting of
PressOn: no parameters returns nothing,
PressOff: no parameters returns nothing,
PressIncreaseTime: no parameters returns nothing,
PressDecreaseTime: no parameters returns nothing,
PressStart: no parameters returns nothing,
PressStop: no parameters returns nothing.
type EnemyFinder: interface consisting of
FindEnemies: no parameters returns List of Enemy.
type Radar: class implementing FoodWarmer, EnemyFinder and consisting of
private secretMilitaryChips: List of Chip,
private giantMicrowavesGenerator: FoodWarmerController,
private strangeControlPanel: AlarmClock,
public GetAccessToControlPanel: subprogram with no parameters returns FoodWarmerControlPanel,
public OpenDoor: subprogram with no parameters returns nothing,
public Put: subprogram with (Food) parameters return nothing,
public CloseDoor: subprogram with no parameters returns nothing,
public FindEnemies: subprogram with no parameters returns List of Enemy.
type AlarmClock: class implementing FoodWarmerControlPanel and consisting of
private mechanics: List of MechanicPart,
public PressOn: subprogram with no parameters returns nothing,
public PressOff: subprogram with no parameters returns nothing,
public PressIncreaseTime: subprogram with no parameters returns nothing,
public PressDecreaseTime: subprogram with no parameters returns nothing,
public PressStart: subprogram with no parameters returns nothing,
public PressStop: subprogram with no parameters returns nothing.
type Microwave: class implementing FoodWarmer and consisting of
private fancyInnerChips: List of Chip,
private foodWarmingThing: FoodWarmerController,
private buttonsPanel: ButtonsPanel,
public GetAccessToControlPanel: subprogram with no parameters returns FoodWarmerControlPanel,
public OpenDoor: subprogram with no parameters returns nothing,
public Put: subprogram with (Food) parameters return nothing,
public CloseDoor: subprogram with no parameters returns nothing.
type ButtonsPanel: class implementing FoodWarmerControlPanel and consisting of
private buttons: List of ButtonState,
public PressOn: subprogram with no parameters returns nothing,
public PressOff: subprogram with no parameters returns nothing,
public PressIncreaseTime: subprogram with no parameters returns nothing,
public PressDecreaseTime: subprogram with no parameters returns nothing,
public PressStart: subprogram with no parameters returns nothing,
public PressStop: subprogram with no parameters returns nothing.
Итак, если класс объявлен как реализующий интерфейс, то он должен реализовать все методы из этого интерфейса.
В противном случае компилятор сообщит нам «фи».
И у нас есть два интерфейса: FoodWarmer и FoodWarmerControlPanel. Посмотрите на них внимательно, а затем давайте посмотрим на реализации.
В наследство от тяжелого советского прошлого мы получили класс «Радар» двойного назначения, который может как разогревать еду, так и обнаруживать противника.
А вместо пульта управления используется будильник, потому что план перевыполнен, и их нужно куда-то деть.
Но, к счастью, безымянный МНС из НИИ химии, удобрений и ядов, которому это навязали, реализовал интерфейсы FoodWarmer для радара и FoodWarmerControlPanel для будильника.
Спустя поколение кому-то пришла в голову мысль, что еду лучше разогревать в микроволновке, а управлять микроволновкой лучше с помощью кнопок.
А вот созданные классы Microwave и ButtonsPanel. И они реализуют одни и те же интерфейсы.
FoodWarmer и FoodWarmerControl. Что это нам дает? Если бы мы везде в нашем коде использовали переменную типа FoodWarmer для подогрева еды, то мы можем просто заменить реализацию на более современную, и никто ничего не заметит. То есть код, использующий интерфейс, не заботится о деталях реализации.
Или к тому, что она изменилась полностью.
Мы даже можем создать класс FoodWarmerFactory, который генерирует различные реализации FoodWarmer в зависимости от конфигурации вашего приложения.
Также обратите внимание на закрытые поля в классе Микроволновое оборудование и Радар.
Там у нас есть будильник и панель с кнопками.
Но мы передаем переменную типа FoodWarmerControlPanel наружу.
Где-то на Пикабу была история о том, как некий кандидат объяснил принцип полиморфизма следующим образом:
Вот у меня есть ручка.Картинка смешная, ситуация пугающая, а объяснение принципа полиморфизма бесполезно.Я могу написать на нем свое имя или воткнуть его тебе в глаз.
Это принцип полиморфизма.
Принцип полиморфизма не означает, что класс ручки с какого-то испуга реализует интерфейсы канцелярского продукта и клинкового оружия одновременно.
Принцип полиморфизма заключается в том, что все, что может колоть, может застрять в глазу.
Потому что с ним можно делать инъекции.
И результат будет разным, но в идеале он должен указывать на слепоту.
И метод полиморфизма позволяет отразить этот факт в модели, которую вы строите для своего мира.
Использование вне ООП
Есть такой веселый и забавный язык во всех смыслах этого слова, как Эрланг.И у него есть такая особенность, как поведение.
Следите за своими руками: Код там разбит на модули.
Вы можете использовать имя модуля в качестве переменной.
То есть написать вызов функции из модуля можно так: %option 1
foobar:function(),
%option 2
Module = foobar,
Module:function().
Для того, чтобы быть уверенным, что у модуля есть определенные функции, существует особенность языка, называемая поведением.
В модуле, который использует другие модули, вы указываете требования к переменным модуля с помощью объявления Behavior_info. И тогда модули, которые будет использовать ваш папа-модуль, который устанавливает поведение_info, используют объявление поведения, чтобы сообщить компилятору: «мы обязуемся реализовать это поведение, чтобы папа-модуль мог нас использовать».
Например, модуль gen_server позволяет создать сервер, который синхронно или асинхронно в отдельном процессе (в Эрланге нет потоков, только тысячи мелких параллельных процессов) выполняет запросы от других процессов.
А gen_server содержит всю логику, касающуюся запросов от других процессов.
Но фактическая обработка этих запросов выполняется теми, кто реализует поведение gen_server. И хотя другие модули реализуют это корректно (даже если это пустые заглушки), gen_server совершенно не волнует, как эти запросы обрабатываются.
Более того, модуль обработки можно менять на лету.
Один интерфейс — множество реализаций.
Как завещал нам Страуструп.
Как написано в умных книжках по ООП.
А теперь цитата из Википедии в адрес студии:
Erlang — функциональный язык программирования со строгой динамической типизацией, предназначенный для создания распределенных вычислительных систем.Просто идея «один интерфейс — множество реализаций» слишком красивая и базовая, скажем так, чтобы ограничивать ее применение исключительно ООП.
Пример номер два.
Есть такой .
NET framework. И это позволяет множеству программ на разных языках взаимодействовать друг с другом.
Ответственный за это среда CLR .
И Microsoft не стала блокировать эту самую CLR миллиардами легальных блокировок, а описала, что она должна уметь делать, в достаточно строгом техническом стандарте (Google ECMA-335).
.
NET и программы, написанные на нем, работали на Windows, Windows Phone, XBox (Google XNA и все, что с ним связано), на любых платформах.
Если Microsoft удержит его.
Но поскольку существует открытый стандарт, там описан интерфейс.
потом нашлись люди, которые реализовали это в виде Моно Проекта.
Их реализация работала в Linux и позволяла запускать программы на .
NET. на Линуксе.
Казалось бы, вот и все.
Но потом ребята из Microsoft решили, что они тоже хотят сделать .
NET для Linux. И они это сделали.
Открытый исходный код, оптимизированный в меру моих возможностей, все прекрасное .
NET Core. А в будущем этот самый .
NET Core станет пятой версией .
NET Framework, и всё древнее наследие, написанное с начала 2000-х годов, уйдет впустую.
Да, пример с новой микроволновкой в реальной жизни.
Я описал здесь эту ситуацию, потому что это идея «один интерфейс — множество реализаций», достигающая какого-то космического уровня.
Полиморфизм в масштабах всей программной платформы, откуда объекты и классы не видны даже в телескоп.
Наследование
Красивая концепция, позволяющая сочетать повторное использование кода и силу полиморфизма.Но, конечно, это не панацея, а один из инструментов.
Не всегда уместно, но иногда очень и очень полезно.
Суть в том, что вы можете создать новый класс на основе существующего и дополнить его, либо частично изменить его поведение.
Ладно, мне сложно идти дальше без примера, так что продолжим развивать NEPL. Сначала о возникшей проблеме.
Помните класс Name? Вот улучшенная версия, в которой имя вежливо сообщается.
Класс EtiquetteInfo находится где-то в другом месте.
import class EtiquetteInfo from Diplomacy.
type PoliteName: class consisting of
private FirstName: String,
private MiddleName: String,
private LastName: String,
for descendants GetPoliteFirstName: subprogram with (EtiquetteInfo) parameters returns String,
for descendants GetPoliteMiddleName: subprogram with (EtiquetteInfo) parameters returns String,
for descendants GetPoliteLastName: subprogram with (EtiquetteInfo) parameters returns String,
public GetFullName: subprogram with (EtiquetteInfo) parameters returns String.
subprogram GetPoliteFirstName.PoliteName with (EtiquetteInfo _EtiquetteInfo) parameters returning String implemented as
return _EtiquetteInfo.PoliteFirstName(FirstName).
subprogram GetPoliteMiddleName.PoliteName with (EtiquetteInfo _EtiquetteInfo) parameters returning String implemented as return _EtiquetteInfo.PoliteMiddleName(MiddleName).
subprogram GetPoliteLastName with (EtiquetteInfo _EtiquetteInfo) parameters returning String implemented as return _EtiquetteInfo.PoliteLastName(LastName).
subprogram GetFullName with (EtiquetteInfo _EtiquetteInfo) parameters returning String implemented as return GetPoliteFirstName(_EtiquetteInfo) + GetPoliteMiddleName(_EtiquetteInfo) + GetPoliteLastName(_EtiquetteInfo).
Допустим, в методе GetFullName у нас была довольно сложная логика обработки имен (давайте представим, что это сложно, ладно?).
Мы пользовались этим классом и были относительно счастливы, но потом у нас появился клиент из какой-то далекой страны.
Где с именами все, мягко говоря, сложно.
Там одни и те же имя и фамилия, одинаковые, назовем их так, к ним применены модификаторы вежливости, но дополнительных имен там может быть много.
Возможно, даже много.
Наш класс PoliteName становится неуклюжим.
Написание отдельного класса ExoticPoliteName с общим интерфейсом означает создание кучи повторяющегося кода.
Я не буду вам здесь рассказывать, сколько боли и страданий оно приносит, когда его сопровождают. Здесь в игру вступает наследование.
Мы создаем класс ExoticPoliteName, который расширяет класс PoliteName и использует его реализацию.
Ниже представлена реализация класса PoliteExoticName. Будем считать, что оно находится в том же модуле, что и PoliteName. import class EtiquetteInfo from Diplomacy.
type PoliteExoticName: class extending PoliteName and consisting of
private MoreMiddleNames: List of String,
for descendants overridden GetPoliteMiddleName: subprogram with (EtiquetteInfo) parameters returns String,
public overriden GetFullName: subprogram with (EtiquetteInfo) parameters returns String.
subprogram GetPoliteMiddleName.PoliteExoticName with (EtiquetteInfo _EtiquetteInfo) parameters returning String implemented as
String AggregatedMiddleName = String.Join(" ", MoreMiddleNames),
return base.GetPoliteMiddleName(_EtiquetteInfo + AggregatedMiddleName).
subprogram GetFullName with (EtiquetteInfo _EtiquetteInfo) parameters returning String implemented as String Prefix = "", String FirstName = GetFirstName(_EtiquetteInfo), if _EtiquetteInfo.ComplimentIsAppropriate(FirstName) then Prefix = "Oh, joy of my heart, dear ", return Prefix + base.GetFullName(_EtiquetteInfo).
На всякий случай: класс PoliteName применительно к наследованию вызывается базовый .
А класс PoliteExoticName является классом-потомком.
Наше повторное использование кода проявляется в том, что мы используем логику базового класса, тогда как в классе-наследнике логика не указана.
То есть нам не нужно переписывать методы GetPoliteFirstName и GetPoliteLastName. Они у нас уже есть.
И когда мы хотим добавить какую-то логику к методу GetFullName, мы добавляем ее, а не пересоздаем.
Полиморфизм наследования заключается в том, что мы можем там, где у нас просят класс PoliteName, дать объект типа PoliteExoticName и спокойно вызвать метод GetFullName. Компилятор поймет, что нам дали наследника PoliteName, и проверит, есть ли у него собственная реализация этого метода.
Обратите внимание внутри реализации на такую конструкцию как base.GetFullName(etiquetteInfo).
Это значит, что реализацию этого метода мы вызываем из базового класса, чтобы не дублировать логику оттуда.
Здесь надо сказать, что таким образом устанавливается связь между базовым классом и его наследником" является ".
Вежливое имя остается вежливым именем, даже если оно экзотическое.
Классический пример: квадрат - это фигура, как и круг.
И их можно нарисовать.
Но по-разному.
ООП считается истинным, если все в программе является объектом.
В Теги: #ООП #solid #полиморфизм #инкапсуляция #наследование
-
Идентификатор Oauth
19 Oct, 24 -
Английские Субтитры С Лео
19 Oct, 24