Основы объектно-ориентированного программирования

Содержание:

Введение

Почти все популярные языки программирования являются объектно-ориентированными. В таблице приведены данные о популярности языков (рейтинг TIOBE) за сентябрь 2019 года :

Не являются объектно-ориентированными лишь 20% — это языки программирования аппаратуры (Си, Assembly language), декларативный язык программирования баз данных (SQL) и визуальный язык MATLAB. Не удивительно, что почти в каждом описании вакансии программиста требуется что-то типа «Понимание ООП» или «Понимание принципов SOLID» (что очень близко).

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

В этой статье-учебнике:

  1. развития языков программирования, объясняющие существующую систему ценностей;
  2. на примерах показаны и их роль в обеспечении ценностей;
  3. приведены на материалы для дальнейшего изучения.

Для освоения материала, читатель должен иметь базовые навыки программирования на любом языке программирования и практику (хотя бы неудачную) применения классов.

Когда нужно использовать ООП ¶

Вот ситуации, когда, на мой взгляд, ООП даёт преимущества:

Работа с базой данных
Через ООП можно «скрыть» внутреннюю структуру базы от конечного пользователя (в данном случае программиста). Если база будет меняться, то это не приведёт к переписыванию всего проекта

Если вы используете объект, вам неважно, как он хранится в базе данных.

Сложная логика
С помощью ООП можно разбить сложную логику на несколько простых классов. Преимущество в том, что можно создавать универсальный интерфейс и узкоспециализированные классы

Вам не придётся менять код, если потребуется добавить «что-то похожее вон на тот класс».

Большой проект
Ахиллесовой пятой больших проектов является сильное связывание, когда один код вызывается из множества разных частей системы. В последствии такой код становится «неприкасаемым». То есть разработчики боятся в нём что-то менять, потому что неизвестно, какие части проекта после этого отвалятся. Если использовать ООП, то с такой проблемой будет разы проще справиться. Объект можно разбить, сделать фасадом или написать тест. С обычным кодом такое сделать труднее.

Автоматическое тестирование
Поскольку объекты несут в себе конечную логику (имеется в виду, что объекты имеют конечное число свойств и методов), то их можно тестировать. Вызвал один метод, проверил результат, вызвал другой метод, проверил результат и так далее. Автоматические тесты помогают избежать ошибок и за счёт этого улучшают скорость и качество разработки.

Методы

Методом класса называют функцию или процедуру, которая принадлежит классу или объекту. Отличие функции от процедуры в том, что функция возвращает значение, а процедура нет. В общем виде синтаксис объявления метода выглядит следующим образом:

модификатор(ы) тип_возвращаемого_значения имя_функции(аргументы)

Модификаторы определяют область видимости, принадлежность метода объекту или классу, является ли метод переопределением и т.п. Тип возвращаемого значения – это любой доступный в C# тип. В качестве типа возвращаемого значения не может использоваться ключевое слово var. Если метод не возвращает ничего, то указывается тип void. Метод может содержать ноль или более аргументов, которые также могут иметь специальные модификаторы, указывающие на то является ли аргумент входным или выходным и т.п. Более подробно про все эти аспекты будет рассказано в одном из уроков, посвященных более глубокому изучению ООП в C#. В рамках данного урока, наша задача – это на интуитивном уровне научиться принципам работы с классами в C#.

Работа с модификатором доступа

Если метод объявлен с модификатором public, то его можно использовать вне класса, например метод Printer из DemoClass

public void Printer()
{
    Console.WriteLine($"field: {field}, Property: {Property}");
}

Такой метод может вызываться в любом месте программы у соответствующих объектов:

var d6 = new DemoClass(11) { Property = 12 };
d6.Printer(); // field: 11, Property: 12

Если мы объявим метод с модификатором private или без модификатора (тогда, по умолчанию, будет принят private), то его уже нельзя будет вызвать снаружи класса:

class DemoClass 
{ 
    // ... 
    private void PrivateMethod() 
    { 
        Console.WriteLine($"Secret method"); 
    } 
    // ... 
}

(Код в методе Main):

var d7 = new DemoClass();
d7.PrivateMethod(); // Ошибка компиляции!!!

Но при этом внутри класса его вызвать можно:

class DemoClass
{
    // ... 
    public void PublicMethod() 
    { 
        Console.WriteLine($"Public method");     
        PrivateMethod(); 
    } 
    // ... 
}

Статические методы и методы объекта

Различают статические методы и методы объекта. Статические имеют модификатор static перед именем метода и принадлежат классу. Для вызова таких методов не обязательно создавать экземпляры класса, мы уже пользовались такими методами из класса Console – это методы Write и WriteLine. Для вызова метода объекта, необходимо предварительно создать экземпляр класса, пример – это метод PublicMethod и Priter у класса DemoClass. Добавим статический метод и метод класса в DemoClass

class DemoClass
{
    // ...
    public static void StaticMethod()
    {
        Console.WriteLine("Message from static method");
    }
    public void NoneStaticMethod()
    {
        Console.WriteLine("Message from non static method");
    }
    // ...
}

Вызовем эти методы из класса DemoClass в методе Main

DemoClass.StaticMethod(); // Message from static method
var d8 = new DemoClass();
d8.NoneStaticMethod(); // Message from none static method

Методы принимающие аргументы и возвращающие значения

Как было сказано в начале данного раздела, методы могут принимать данные через аргументы и возвращать значения, продемонстрируем эту возможность на примере:

class DemoClass 
{ 
    // ... 
    public int MulField(int value) 
    { 
        return field * value; 
    } 
    // ... 
}

(Код в Main):

var d8 = new DemoClass(10);
Console.WriteLine($"MulField() result: {d8.MulField(2)}"); // MulField() result: 20

7.1 Понятие класса в ООП.

Класс – такая абстракция множества предметов реального мира, что все предметы этого множества(объекты) имеют одни и те же характеристики, все экземпляры подчинены и согласованы с одним и тем же поведением.

Отношения между классами:

  • Наследование – на основе одного класса, мы строим новый класс, путем добавления новых характеристик и методов.

  • Использование – один класс вызывает методы другого класса.

  • Представление (наполнение) – это когда один класс содержит другие классы.

  • – класс, существующий для создания других классов.

Класс в Java — это шаблон для создания объекта, а объект — это экземпляр класса. Класс определяет структуру и поведение, которые будут совместно использоваться набором объектов. Класс содержит переменные и методы, которые называются элементами класса, членами класса. Он составляет основу инкапсуляции в Java. Каждый объект данного класса содержит структуру и поведение, которые определены классом. Иногда объекты называют экземплярами класса.

Методы используются для описания того, что объект класса умеет делать или что можно с ним сделать. Переменные — для описания свойств или характеристик объекта.

Рассмотрим картинку, приведенную ниже. Объявлен класс Student, у которого есть переменные name и rollNo, а также методы setName() и setRollNo() для установки этих значений. На основе этого класса создано несколько объектов: Jenna, John, Maria, James. У каждого объекта, то есть студента, есть name и rollNo, но они разные.

Рассмотрим как создать класс в языке Java. Упрощенная общая форма определения класса:

После ключевого слова class пишется имя класса. В теле класса объявляются переменные и методы класса. Их может быть сколько угодно.

Опишем класс для объекта Box (коробка). У коробки есть три главные характеристики: ширина, высота и глубина, описанные с помощью переменных:

Класс и взаимоотношение между классами можно смоделировать в виде

а так же с помощью Диаграммы Венна или кругов Эйлера

Схематичное изображение наследования классом B класса А с помощю диаграмы Венна (или Диаграммы Эйлера)

Объект

Ключевым концептом объектно-ориентированного программирования является объект. Вещи вроде классов необязательны (например, JavaScript является ОО-языком, но у него на самом деле нет классов; он эмулирует их с помощью прототипов). Давайте начнет с реализации объектов.

Что же нужно нашим объектам? Я уже упомянул «объектный цикл» и каналы. Помимо этого, нам нужна функция — обработчик сообщений.

У Clojure есть собственная реализация каналов в библиотеке , так что мы будем использовать ее. Но сначала нам нужно подумать о структуре данных для наших объектов. Собственно, ничего сложного:

Теперь нам просто нужно добавить объектный цикл:

Функция попросту ждет сообщения из канала. Функция в :message-handler по идее должна принимать сам объект (self, this), состояние и само сообщение как аргументы.

Все готово, нам нужно только объединить все это — создать объект:

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

Используем объекты

Это все, конечно, здорово, но работает ли оно? Давайте попробуем. Я решил протестировать это, реализовав string builder.

String builder — это просто объект, который склеивает несколько строк:

Давайте попробуем реализовать его:

(это немного измененная версия теста, который я написал)

По сути, мы можем относиться к обработчику сообщений как к диспечеру, который передает сообщения нужным методам, в зависимости от того, какое сообщение пришло. Здесь у нас есть 5 методов.

Давайте попробуем запустить наш пример с «hello world»:

Первые две строки вполне понятны и без объяснений. Но что происходит дальше?

Наш объект живет в другом потоке и ему как-то нужно вернуть какой-то результат. Как же нам получить этот результат? Используя колбеки и промисы (promises).

Здесь я просто решил использовать колбек и выставить промис в нем. Я думаю, что это очень плохой дизайн и мне стоило использовать промисы с самого начала. Но это просто для демонстрации, так что пффффф.

просто вытаскивает значение из промиса. Если оно еще не установлено, то она будет ждать (блокирует текущий поток).

Обратите внимание на метод , он немного поинтересней, т.к. в нем объект отправляет сообщения сам себе

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

Когда я тестировал этот метод, я сделал что-то вроде такого:

Но тест не прошел. Это происходит из-за того, что сообщение было отправлено до того, как метод отправил сообщения (не забывайте, у нас очередь сообщений).

Я потратил значительное количество времени, пытаясь понять, что было не так. Это произошло из-за того, что я не привык к параллельному программированию (мой бекграунд — Ruby on Rails) и это довольно распространенная проблема.
Собственно, это одна из причин, почему функциональное программирование становится все более популярным в наше время — чистые функции уменьшают шанс подобных ошибок. В моем объекте просто случился race condition (два потока пытались получить доступ к одному куску памяти). Мютабилити — зло!:)

Это было фундаментом для нашей объектной системы. Мы можем построить множество всего на нем. Давайте попробуем классы?

Наследование[]

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

Другими словами, класс-наследник реализует спецификацию уже существующего класса (базовый класс). Это позволяет обращаться с объектами класса-наследника точно так же, как с объектами базового класса.

Простое наследование:

Класс, от которого произошло наследование, называется базовым или родительским (англ. base class). Классы, которые произошли от базового, называются потомками, наследниками или производными классами (англ. derived class).

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

Множественное наследование

При множественном наследовании у класса может быть более одного предка. В этом случае класс наследует методы всех предков. Достоинства такого подхода в большей гибкости. Множественное наследование реализовано в C++. Из других языков, предоставляющих эту возможность, можно отметить Python и Эйфель. Множественное наследование поддерживается в языке UML.

Множественное наследование — потенциальный источник ошибок, которые могут возникнуть из-за наличия одинаковых имен методов в предках. В языках, которые позиционируются как наследники C++ (Java, C# и др.), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость все-таки возникла, то, для разрешения конфликтов использования наследованных методов с одинаковыми именами, возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.

Попытка решения проблемы наличия одинаковых имен методов в предках была предпринята в языке Эйфель, в котором при описании нового класса необходимо явно указывать импортируемые члены каждого из наследуемых классов и их именование в дочернем классе.

Большинство современных объектно-ориентированных языков программирования (C#, Java, Delphi и др.) поддерживают возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.

3 Что учить дальше?

Мы разобрали ценности и знаем к чему стоит стремиться, кроме того, мы поверхностно посмотрели на механизмы, которые предоставляет нам объектно-ориентированное программирование, не фокусируясь на каком-либо определенном языке

Однако, в программировании важна практика — посмотрите как описанные механизмы реализуются в вашем любимом языке и попробуйте реализовать небольшой проект.
Прочитайте про SOLID — постарайтесь заметить не только принципы, но также — проблемы и подходы к их решению.
Обратите внимание на модульное тестирование и TDD — это будет особенно полезно, если у вас не получается удачная архитектура. По моим наблюдениям, даже начинающий программист начинает писать более качественный код, если сначала подумает о том, как он этим кодом хочет пользоваться

Изучите библиотеку модульного тестирования вашего языка программирования и попробуйте писать тесты.
Посмотрите литературу о шаблонах проектирования , при этом я рекомендую не пытаться запомнить детали реализации паттернов, а ответить на вопросы «где я могу применить этот шаблон?», «какую пользу и вред я с этого получу?». Если это кажется вам тяжелым — посмотрите как с помощью паттернов можно улучшить качество кода .

3.1 Список использованных источников

  1. Рейтинг популярности языков программирования TIOBE. URL: https://tiobe.com/tiobe-index/
  2. SOLID принципы. Рефакторинг. URL: https://pro-prof.com/archives/1914
  3. Почему мне кажется, что студентов учат ООП неправильно. URL: https://habr.com/ru/post/345658/
  4. C++ Russia 2018: Фёдор Короткий, Память – идеальная абстракция. URL: https://vk.com/wall-105242702_701
  5. Мейер Б. Объектно-ориентированное конструирование программных систем. М.: Издательско-торговый дом «Русская Редакция», «Интернет-университет информационных технологий», 2005. 1232 с.: ил.
  6. Мартин Р. Чистый код. Создание, анализ и рефакторинг. Библиотека программиста. – СПб.: Питер, 2014. – 464 с.
  7. Джейсон Мак-Колм Смит Элементарные шаблоны проектирования : Пер. с англ. — М. : ООО “И.Д. Вильямс”, 2013. — 304 с.
  8. Диаграммы классов UML. URL: https://pro-prof.com/archives/3212
  9. Юнит-тестирование. Пример. Boost Unit Test. URL: https://pro-prof.com/archives/1549
  10. Э. Гамма Приемы объектно-ориентированного проектирования. Паттерны проектирования / Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидес. – СПб.: Питер, 2009. – 366 с.

Две методики

Все компьютерные программы состоят из двух элементов: кода и данных. Более того, программа концептуально может быть организована вокруг своего кода или своих данных. Иными словами, организация одних программ определяется тем, “что происходит”, а других — тем, “на что оказывается влияние”. Существуют две методики создания программ. Первая из них называется моделью, ориентированной на процессы и характеризует программу как последовательность линейных шагов (т.е. кода). Модель, ориентированную на процессы, можно рассматривать в каче­стве кода, воздействующего на данные. Такая модель довольно успешно применяется в процедурных языках вроде С. Но, как отмечалось в главе 1, подобный подход по­рождает ряд трудностей в связи с увеличением размеров и сложности программ.

С целью преодолеть увеличение сложности программ была начата разработ­ка подхода, называемого объектно-ориентированным программированием. Объектно- ориентированное программирование позволяет организовать программу вокруг ее данных (т.е. объектов) и набора вполне определенных интерфейсов с этими данными. Объектно-ориентированную программу можно охарактеризовать как данные, управляющие доступом к коду. Как будет показано далее, передавая функции управления данными, можно получить несколько организационных преимуществ.

Инкапсуляция

Инкапсуляция ограничивает доступ компонентов к другим, связывает данные с методами для обработки. Для инкапсуляции используется спецификатор доступа private.

Обычно понятия инкапсуляция и сокрытие отождествляются, но некоторые языки различают эти понятия. Другими словами, критичные для работы свойства защищаются, а их изменение становится невозможным.

class Animal {

private $name;

function __construct($name) {

$this->name = $name;

}

function getName() {

return $this->name;

}

}

Name принимается в качестве аргументов конструктора. Когда конструктор будет использован в других частях кода, ничто не сможет изменить элемент name. Как видим, он указывается внутри, для других частей кода он недоступен.

Расскажите про основные понятия ООП: «класс», «объект», «интерфейс».

Класс – это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью (контракт).

С точки зрения программирования класс можно рассматривать как набор данных (полей, атрибутов, членов класса) и функций для работы с ними (методов).

С точки зрения структуры программы, класс является сложным типом данных.

Объект (экземпляр) – это отдельный представитель класса, имеющий конкретное состояние и поведение, полностью определяемое классом. Каждый объект имеет конкретные значения атрибутов и методы, работающие с этими значениями на основе правил, заданных в классе.

Интерфейс – это набор методов класса, доступных для использования. Интерфейсом класса будет являться набор всех его публичных методов в совокупности с набором публичных атрибутов. По сути, интерфейс специфицирует класс, чётко определяя все возможные действия над ним.

ФП против ООП

К этому моменту и сторонники ООП и сторонники ФП уже смотрят на меня через оптические прицелы. Редукционизм не лучший способ завести друзей. Но иногда он полезен. В данном случае, я думаю что полезно пролить свет на никак не утихающий холивар ФП против ООП.

Ясно, что два редукционистских определения, которые я выбрал, совершенно ортогональны. Полиморфизм и Референциальная Прозрачность не имеют никакого отношения друг к другу. Они никак не пересекаются.

Но ортогональность не подразумевает взаимного исключения (спросите Джеймса Клерка Максвелла). Вполне можно создать систему, которая использует и динамический полиморфизм и референциальную прозрачность. Это не только возможно, это правильно и хорошо!

Почему эта комбинация хороша? По точно тем же причинам, что оба её компонента! Системы построенные на динамическом полиморфизме хороши, потому что они обладают низкой связностью. Зависимости можно инвертировать и расположить по разные стороны архитектурных границ. Эти системы можно тестировать используя Моки и Фейки и другие виды Тестовых Дублей. Модули можно модифицировать не внося изменения в другие модули. Поэтому такие системы проще изменять и улучшать.

Системы, построенные на референциальной прозрачности тоже хороши, потому что они предсказуемы. Неизменяемость состояния делает такие системы проще для понимания, изменения и улучшения. Это значительно уменьшает вероятность возникновения гонок и других проблем, связанных с многопоточностью.

Главная мысль тут такая:

ФП и ООП хорошо работают вместе. И то и другое хорошо и правильно использовать в современных системах. Система, которая построена на комбинации принципов ООП и ФП максимизирует гибкость, поддерживаемость, тестируемость, простоту и прочность. Если убрать одно ради добавления другого это только ухудшит структуру системы.

Объектно-ориентированное программирование

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

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

Программисты договорились, что данные внутри объекта будут называться свойствами, а функции — методами. Но это просто слова, по сути это те же переменные и функции.

Объект можно представить как независимый электроприбор у вас на кухне. Чайник кипятит воду, плита греет, блендер взбивает, мясорубка делает фарш. Внутри каждого устройства куча всего: моторы, контроллеры, кнопки, пружины, предохранители — но вы о них не думаете. Вы нажимаете кнопки на панели каждого прибора, и он делает то, что от него ожидается. И благодаря совместной работе этих приборов у вас получается ужин.

Объекты характеризуются четырьмя словами: инкапсуляция, абстракция, наследование и полиморфизм. Если интересно, что это такое, приглашаем в кат:

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

Абстракция — у объекта есть «интерфейс»: у объекта есть методы и свойства, к которым мы можем обратиться извне этого объекта. Так же, как мы можем нажать кнопку на блендере. У блендера есть много всего внутри, что заставляет его работать, но на главной панели есть только кнопка. Вот эта кнопка и есть абстрактный интерфейс.

В программе мы можем сказать: «Удалить пользователя». На языке ООП это будет «пользователь.удалить()» — то есть мы обращаемся к объекту «пользователь» и вызываем метод «удалить»

Кайф в том, что нам не так важно, как именно будет происходить удаление: ООП позволяет нам не думать об этом в момент обращения

Например, над магазином работают два программиста: один пишет модуль заказа, а второй — модуль доставки. У первого в объекте «заказ» есть метод «отменить». И вот второму нужно из-за доставки отменить заказ. И он спокойно пишет: «заказ.отменить()»

Ему неважно, как другой программист будет реализовывать отмену: какие он отправит письма, что запишет в базу данных, какие выведет предупреждения

Наследование — способность к копированию. ООП позволяет создавать много объектов по образу и подобию другого объекта. Это позволяет не копипастить код по двести раз, а один раз нормально написать и потом много раз использовать.

Например, у вас может быть некий идеальный объект «Пользователь»: в нём вы прописываете всё, что может происходить с пользователем. У вас могут быть свойства: имя, возраст, адрес, номер карты. И могут быть методы «Дать скидку», «Проверить заказ», «Найти заказы», «Позвонить».

На основе этого идеального пользователя вы можете создать реального «Покупателя Ивана». У него при создании будут все свойства и методы, которые вы задали у идеального покупателя, плюс могут быть какие-то свои, если захотите.

Идеальные объекты программисты называют классами.

Полиморфизм — единый язык общения

В ООП важно, чтобы все объекты общались друг с другом на понятном им языке. И если у разных объектов есть метод «Удалить», то он должен делать именно это и писаться везде одинаково

Нельзя, чтобы у одного объекта это было «Удалить», а у другого «Стереть».

При этом внутри объекта методы могут быть реализованы по-разному. Например, удалить товар — это выдать предупреждение, а потом пометить товар в базе данных как удалённый. А удалить пользователя — это отменить его покупки, отписать от рассылки и заархивировать историю его покупок

События разные, но для программиста это неважно. У него просто есть метод «Удалить()», и он ему доверяет

Такой подход позволяет программировать каждый модуль независимо от остальных. Главное — заранее продумать, как модули будут общаться друг с другом и по каким правилам

При таком подходе вы можете улучшить работу одного модуля, не затрагивая остальные — для всей программы неважно, что внутри каждого блока, если правила работы с ним остались прежними

Что такое ООП?

Я подойду к вопросу с редукционистских позиций. Есть много правильных определений ООП которые покрывают множество концепций, принципов, техник, паттернов и философий. Я намерен проигнорировать их и сосредоточиться на самой соли. Редукционизм тут нужен из-за того, что всё это богатство возможностей, окружающее ООП на самом деле не является чем-то специфичным для ООП; это просто часть богатства возможностей встречающихся в разработке программного обеспечения в целом. Тут я сосредоточусь на части ООП, которая является определяющей и неустранимой.

Посмотрите на два выражения:

1: f(o); 2: o.f();

В чём разница?

Никакой семантической разницы явно нет. Вся разница целиком и полностью в синтаксисе. Но одно выглядит процедурным, а другое объектно ориентированным. Это потому что мы привыкли к тому, что для выражения 2. неявно подразумевается особая семантика поведения, которой нет у выражения 1. Эта особая семантика поведения — полиморфизм.

Когда мы видим выражение 1. мы видим функцию f, которая вызывается в которую передаётся объект o. При этом подразумевается, что есть только одна функция с именем f, и не факт, что она является членом стандартной когорты функций, окружающих o.

С другой стороны, когда мы видим выражение 2. мы видим объект с именем o которому посылают сообщение с именем f. Мы ожидаем, что могут быть другие виды объектов, котоые принимают сообщение f и поэтому мы не знаем, какого конкретно поведения ожидать от f после вызова. Поведение зависит от типа o. то есть f — полиморфен.

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

Программисты, использующие Си и Паскаль (и до некоторой степени даже Фортран и Кобол) всегда создавали системы инкапсулированных функций и структур. Чтобы создать такие структуры даже не нужен объектно ориентированный язык программирования. Инкапсуляция и даже простое наследование в таких языках очевидны и естественны. (В Си и Паскале более естественно, чем в других)

Поэтому то, что действительно отличает ООП программы от не ООП программ это полиморфизм.

Возможно вы захотите возразить, что полифорфизм можно сделать просто используя внутри f switch или длинные цепочки if/else. Это правда, поэтому мне нужно задать для ООП ещё одно ограничение.

Использование полиморфизма не должно создавать зависимости вызывающего от вызываемого.

Чтобы это объяснить, давайте ещё раз посмотрим на выражения. Выражение 1: f(o), похоже зависит от функции f на уровне исходного кода. Мы делаем такой вывод потому что мы также предполагаем, что f только одна и что поэтому вызывающий должен знать о вызываемом.

Однако, когда мы смотрим на Выражение 2. o.f() мы предполагаем что-то другое. Мы знаем, что может быть много реализаций f и мы не знаем какая из этих функций f будет вызвана на самом деле. Следовательно исходный код, содержащий выражение 2 не зависит от вызываемой функции на уровне исходного кода.

Если конкретнее, то это означает, что модули (файлы с исходным кодом), которые содержат полиморфные вызовы функций не должны ссылаться на модули (файлы с исходным кодом), которые содержат реализацию этих функций. Не может быть никаких include или use или require или каких-то других ключевых слов, которые создают зависимость одних файлов с исходным кодом от других.

Итак, наше редукционистское определение ООП это:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *