Перейти к содержанию

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

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

  • создавать собственные пользовательские типы данных (классы) для описания объектов реального мира

  • манипулировать объектами такого пользовательского типа также просто, как переменными базового типа.

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

  • при процедурном подходе акцент делается в основном на реализацию программы посредством эффективных функций. Такой подход хорош для небольших программ, в которых требуется оптимизировать время выполнения и/или объем занимаемой памяти;

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

https://ratcatcher.ru/media/inf/pr/pr8/image1.svg

Основы ООП#

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

Суть понятия объектно-ориентированного программирования в том, что все программы, написанные с применением этой парадигмы, состоят из объектов. Каждый объект — это определённая сущность со своими данными и набором доступных действий.

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

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

Этот подход к программированию хорошо подходит для больших, сложных и активно обновляемых или поддерживаемых программ.

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

Причины возникновения ООП#

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

Программное обеспечение является по своей сути очень сложным. Сложность программных систем часто превосходит человеческий интеллектуальный потенциал. Как утверждает один из основателей объектно-ориентированной методологии Грейди Буч, эта сложность следует из четырех элементов:

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

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

Впервые термины "объекты" и "объектно-ориентированный" в современном смысле объектно-ориентированного программирования появились исследованиях группы искусственного интеллекта Массачусетского технологического института в конце 1950-х - начале 1960-х годов. Понятия "объект" и "экземпляр" появились в глоссарии, разработанном Иваном Сазерлендом в 1961 г. и связаны с описанием светового пера (Sketchpad).

Составляющие объектно-ориентированной методологии#

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

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

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

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

Концепции ООП#

https://ratcatcher.ru/media/inf/pr/pr8/11212312312323412.png
В коде, написанном по парадигме ООП, выделяют четыре основных элемента:

  1. Объект.

    Часть кода, которая описывает элемент с конкретными характеристиками и функциями. Карточка товара в каталоге интернет-магазина — это объект. Кнопка «заказать» — тоже.

  2. Класс.

    Шаблон, на базе которого можно построить объект в программировании. Например, у интернет-магазина может быть класс «Карточка товара», который описывает общую структуру всех карточек. И уже из него создаются конкретные карточки — объекты.

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

  3. Метод.

    Функция внутри объекта или класса, которая позволяет взаимодействовать с ним или другой частью кода. В примере с карточками товара метод может:

    ● Заполнить карточку конкретного объекта нужной информацией.
    ● Обновлять количество товара в наличии, сверяясь с БД.
    ● Сравнивать два товара между собой.
    ● Предлагать купить похожие товары.

  4. Атрибут. (Свойство)

    Характеристики объекта в программировании — например, цена, производитель или объём оперативной памяти. В классе прописывают, что такие атрибуты есть, а в объектах с помощью методов заполняют эти атрибуты данными.

Понятия, связанные с ООП#

https://ratcatcher.ru/media/inf/pr/pr8/image2.svg

1) Наследование
Наследование – это концепция OOPS, в которой один объект приобретает свойства и поведение родительского объекта. Это создает родительско-дочерние отношения между двумя классами. Он предлагает надежный и естественный механизм для организации и структуры любого программного обеспечения.

2) Полиморфизм
Полиморфизм относится к способности переменной, объекта или функции принимать несколько форм. Например, в английском языке бег глагола имеет другое значение, если вы используете его с ноутбуком , пешеходом и бизнесом . Здесь мы понимаем значение бега, основываясь на других словах, используемых вместе с ним. То же самое относится и к полиморфизму.

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

4) Инкапсуляция
Инкапсуляция – это ООП метод упаковки данных и кода. В этой концепции OOPS переменные класса всегда скрыты от других классов. Доступ к нему возможен только с использованием методов их текущего класса. Например – в школе ученик не может существовать без класса.

Преимущества и недостатки ООП#

Преимущества Недостатки
В парадигме объектов легче писать код. Удобно создать класс или метод и использовать. Сложность в освоении. ООП требует больше знаний. Необходимо разобраться в классах, наследовании, писать публичные и внутренние функции, изучать взаимодействие объектов.
Читать код гораздо проще. Видны конкретные объекты и методы. Громоздкость. Требуется создавать классы, объекты, методы и атрибуты даже для маленьких программ.
Код легче обновлять. Изменение в одном месте распространяется на все объекты. Низкая производительность. Объекты потребляют больше памяти и могут снижать скорость компиляции.
Удобство работы в команде. Разные люди могут отвечать за разные объекты.
Код можно переиспользовать. Один раз написанный класс можно использовать в разных проектах.
Шаблоны проектирования. Готовые решения для взаимодействия классов, шаблоны упрощают написание кода.

Объявление класса#

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

Принципиальным отличием классов от старых структур Си является объединение (инкапсуляция) в одной программной единице, как данных, так и методов для работы с этими данными. Определяя термин «инкапсуляция», говорят также, что переменные класса (member variables) инкапсулированы вместе с набором функций (member functions) для работы с этими переменными. Полученный в результате класс — это программный модуль, который можно использовать как строительный блок при разработке приложения (приложений).

3амечание: объявление класса рекомендуется помещать в заголовочный файл.

Общий вид оператора описания класса следующий:

https://ratcatcher.ru/media/inf/pr/pr8/image7.svg

Формально простейшее объявление класса выглядит так:

class ИмяКласса 
{
    private: 
    список элементов класса;
    закрытый режим доступа – доступный только собственным методам класса; область видимости в пределах класса, доступ имеют только функции – элементы данного класса
    protected: 
    список элементов класса;
    защищенный режим доступа – доступный только собственным методам и методам производных классов
    public: 
    список элементов класса;
    открытый режим доступа – доступный любым методам;
    видимы вне класса, может осуществляться доступ извне.
};

список_членов_класса — включает описание как типов и имен переменных (member variables), так и прототипы функций класса (member functions), которые принято в русскоязычной литературе называть методами.

Замечание: ; после закрывающей фигурной скобки обязательна!

Какую информацию получает компилятор, встречая объявления класса:

объявлен новый пользовательский (агрегатный) тип данных;

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

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

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

Отличия процедурного и объектно-ориентированного подходов#

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

Объектно-ориентированное программирование (C++) решения к объектно-ориентированному. Исходными данными являются: день месяца, месяц, год. Требуется вычислить порядковый день в году. Например, первому марта 2006 года соответствует 60-ый порядковый день года.

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

Решение в стиле Си (процедурный подход)#

Замечание: процедурное решение этой задачи было приведено в качестве примера еще создателями языка Си (Керниган и Ричи).
#include <iostream>
//сервер:
int DayOfYear(int day, int month, int year)
{
    //Вспомогательный массив, содержащий количество дней в
    //каждом месяце
    int ar[][12] = { {31, 28, 31, 30, ...},   //не високосный год
                     {31, 29, 31, 30, ...} }; //високосный год
    //Определяю — по какой строчке суммировать
    int I = (year % 4 == 0) &&
            ((year % 100 != 0) || (year % 400 == 0));
    for (int i = 0; i < (month - 1); i++)
        day += ar[I][i];  //копим порядковый день года
    return day;
}
//клиент
int main()
{
    int d1 = 1, m1 = 3, y1 = 2006;
    std::cout << DayOfYear(d1, m1, y1) << std::endl;  //вызов
    int d100 = 3, m100 = 10, y100 = 2004;
    std::cout << DayOfYear(d100, m100, y100) << std::endl;  //вызов
    //Неудобства: — оперирую с разными датами, их много -> все время
    //придется помнить, какая тройка переменных к какой дате
    //относится
}

Графически вызов функции можно представить:

https://ratcatcher.ru/media/inf/pr/pr8/image3.svg

Важно:

данные для совершения сервером действия предоставляет клиент!

данных много!

о том, что несколько элементов данных относятся к одной сущности (дате), знает только программист!

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

Использование структур Си (укрупнение данных)#

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

#include <iostream>
struct Date
{
    int year;
    int month;
    int day;
};
//сервер:
int DayOfYear(const /*struct*/ Date* date)
{
    int ar[2][12] = { { 31, 28, 31, 30, ... },  //не високосный год
                      {31, 29, 31, 30, ... } }; //високосный год
    int I = (date->year % 4 == 0) &&
        ((date->year % 100 != 0) || (date->year % 400 == 0));
    int YearDay = date->day;//здесь будем копить порядковый день
                            //года, так как нелогично «портить»
                            //содержимое по адресу date
    for (int i = 0; i < date->month - 1; i++)
        YearDay += ar[I][i];
    return YearDay;
}
//клиент:
int main() //
{
    Date d1 = { 1, 9, 2006 }; //создание и инициализация переменной
                              //типа Date
    std::cout << DayOfYear(&d1) << std::endl; //передача параметра
                                              //по указателю
    Date d100 = { 31, 12, 2006 };
    std::cout << DayOfYear(&d100) << std::endl;
}

https://ratcatcher.ru/media/inf/pr/pr8/image4.svg

Важно:

данные сгруппированы посредством структуры

данные все еще на стороне клиента!

зато за счет укрупнения данных программисту нужно запоминать гораздо меньше

и объем передаваемой информации тоже сократился (передается только адрес структурной переменной)! Замечание: если передавать объект по значению, то экономии памяти не будет.

Использование классов C++#

Объектно-ориентированный подход позволяет в одном типе совместить как данные, описывающие этот объект, так и операции над этими данными.

#include <iostream>
//сервер:
class Date
{
    int year;
    int month;
    int day;
    bool IsLeapYear()//аргументы не передаются!!! — метод выполняет
                     //действия над данными объекта! ! !
    {
        return (year % 4 == 0) &&
            ((year % 100 != 0) || (year % 400 == 0));
    }
public: //спецификатор доступа понятие будет введено позже
    int DayOfYear()//параметры не передаются!!! Это не обычная
                   //Функция, а метод класса, который обращается к
                   //данным того объекта, для которого он был
                   //вызван!!!
    {
        int ar[2][12] = { { 31, 28, 31, 30, ... },//не високосный год
                          { 31, 29, 31, 30, ... } }; //високосный год
        int I = IsLeapYear();
        int YearDay = day;//здесь будем копить порядковый день года
        for (int i = 0; i < month - 1; i++)
        {
            YearDay += ar[I][i];
        }
        return YearDay;
    }
};
//клиент:
int main()
{
    Date my; //создание объекта типа Date
    ... //Инициализация членов данных объекта — пока не привожу код
    std::cout << my.DayOfYear(); //вызов метода класса. Эта функция
                                 //не принимает параметров.Она
                                 //высчитывает порядковый день года,
                                 //исходя из данных объекта my
} 

https://ratcatcher.ru/media/inf/pr/pr8/image5.svg

Важно: данные на стороне сервера!

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