Программирование на основе классов и шаблонов

Теоретические основы ООП

Аладин Дмитрий Владимирович

iu5edu.ru/wiki/cpp2

План

  • Сцепление и связанность проектов.
  • Пространство имен.
  • Объектно-ориентированное проектирование (OOD) и программирование (OOP).
  • Главные и сопутствующие принципы объектно-ориентированного программирования (ООП).
  • Объект, прототип и класс.

В предыдущей серии...

До появления объектно-ориентированного программирования (ООП)

center

В предыдущей серии...

После появления ООП

center

Вспомним про показатели качества программного проекта

Для оценки качества программного проекта нужно учитывать, кроме всех прочих, следующие два показателя:

  • Сцепление (cohesion, [kəʊˈhiːʒən]) характеризует целостность, "плотность" модуля, т.е. насколько модуль является простым с точки зрения его использования. В идеале модуль должен выполнять одну единственную функцию и иметь минимальное число "ручек управления".

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

  • Связанность (coupling, [ˈkʌplɪŋ]) характеризует степень независимости модулей. При проектировании систем необходимо стремиться, чтобы модули имели минимальную зависимость друг от друга, т.е. были минимально связаны между собой.

Как запомнить?

Согласно словарю мультитран:

  • cohesion [kəu'hi:ʒ(ə)n] сущ. - нефтепром. - сцепление молекул
  • coupling ['kʌplɪŋ] сущ. - нефт. - соединение (постановка (вагона в поезд) , перемещение точек)

center

К чему должны стремиться?

Мы должны стремиться к достижению низкой связанности (low coupling) и высокому сцеплению (high cohesion) при работе над кодовой базой

center

Про степени сравнения другими словами

  • Связанность (coupling) представляет собой степень взаимосвязи между блоками. Другими словами, это количество соединений между двумя или более блоками. Чем меньше число, тем ниже связанность (low coupling).
  • Высокое сцепление (high cohesion) означает хранение связанных друг с другом частей кода в одном месте. В то же время низкое сцепление (low cohesion) заключается в максимально возможном разделении несвязанных частей кодовой базы.

Теоретически рекомендации выглядят довольно просто. Но на практике...

На практике все очень плохо...

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

center

Типы кода с точки зрения связанности (coupling) и сцепления (cohesion)

Помимо кода, который одновременно имеет высокое сцепление и низкую связанность, существует, по крайней мере, еще три типа.

1. Низкая связанность (low coupling) и высокое сцепление (high cohesion)

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

2. Высокая связанность (high coupling) и высокое сцепление (high cohesion)

Это антипаттерн "божественный объект" (God Object) в основном означает один фрагмент кода, который выполняет всю работу сразу.

Другое название такого кода - Большой шар/ком грязи (Big Ball of Mud).

3. Высокая связанность (high coupling) и низкое сцепление (low cohesion)

Границы между различными классами или модулями выбраны плохо.

Проблема здесь в том, что границы выбраны неправильно и часто не отражают фактическую семантику домена. Такой код довольно часто нарушает "Принцип единой ответственности" (Single Responsibility Principle).

4. Низкая связанность (high coupling) и низкое сцепление (low cohesion)

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

Резюме по связанности (coupling) и сцеплению (cohesion)

Давайте подведем итоги:

  • Сцепление (cohesion) - степень, в которой часть кодовой базы образует логически единую атомарную единицу.
  • Связанность (coupling) - степень, в которой один блок независим от других.
  • Невозможно добиться полного разделения (decoupling) без нарушения целостности (cohesion), и наоборот.
  • Старайтесь придерживаться принципа низкой связанности (low coupling) и высокого сцепления (high cohesion) на всех уровнях вашей кодовой базы.
  • Не попадайтесь в ловушку деструктивной развязки (destructive decoupling).

Почему предыдущие парадигмы до ООП не справлялись?

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

P.S. Вспомните про то, как осуществлялся переход от структурной парадигмы к ООП в лабораторной работе №2. Как выпиливались глобальные переменные и функции?

Почему предыдущие парадигмы до ООП не справлялись?

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

Пример функциональной и объектной декомпозиции:

center

Первый подход к снаряду (без ООП)

Использование библиотек и пространство имен!

С библиотеками мы уже знакомы. Давайте про пространство имен теперь узнаем!

Пространство имён (namespace) — это группа взаимосвязанных функций, переменных, констант, классов, объектов и других компонентов программы.

P.S. std:: и using namespace std; уже встречали?)

Сделаем грязь!

#include <iostream>

namespace lol {
    const char *cout() {
        return "Hello, World!";
    }
}

int main() {
    std::cout << lol::cout() << std::endl;
    return 0;
}

P.S. std::cout это объект ostream, lol::cout - это функция.

Глобальное пространство имен

Если идентификатор не объявлен явно в пространстве имен, он неявно считается входящим в глобальное пространство имен.

#include <iostream>

const char *privet() {
    return "Hello, World!";
}

int main() {
    std::cout << ::privet() << std::endl;
    return 0;
}

Директива using

Директива using позволяет использовать все имена в объекте namespace без имени пространства имен в качестве явного квалификатора.

Директиву using можно поместить в верхнюю часть CPP-файла (в области видимости файла) или внутрь определения класса или функции.

P.S. Стоит ли злоупотреблять using?

Осторожнее с using!

Без особой необходимости не размещайте директивы using в файлах заголовков (*.h), так как любой файл, содержащий этот заголовок, добавит все идентификаторы пространства имен в область видимости, что может вызвать скрытие или конфликты имен, которые очень трудно отлаживать. В файлах заголовков всегда используйте полные имена.

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

Вложенные пространства имен

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

Покажем эту особенность на примере.

Вложенные пространства имен

namespace ContosoDataServer
{
    void Foo();

    namespace Details
    {
        int CountImpl;
        void Ban() { return Foo(); }
    }

    int Bar(){...};
    int Baz(int i) { return Details::CountImpl; }
}

Встроенные пространства имен (C++ 11)

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

В следующем примере показано, как внешний код привязывается к встроенному пространству имен по умолчанию.

Встроенные пространства имен (C++ 11)

#include <string>
#include <iostream>

namespace Test {
    namespace old_ns {
        std::string Func() { return std::string("Hello from old"); }
    }

    inline namespace new_ns {
        std::string Func() { return std::string("Hello from new"); }
    }
}


int main() {
    using namespace Test;
    using namespace std;

    string s = Func();
    std::cout << s << std::endl; // "Hello from new"
    return 0;
}

Ну вроде же решили все проблемы?

  • Зачем нам ООП, если есть пространство имен?
  • Можно же группировать структуры по пространствам имен!
  • Да и механизмы доступа вроде имеются. Пусть программист сам контролирует к чему обращается!

Что говорил дядюшка Боб про парадигмы?

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

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

center

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

Проектирование (designing) — процесс определения архитектуры, компонентов, интерфейсов и других характеристик системы или её части (ISO 24765).

Процесс программирования (programming) состоит в последовательной или итеративной реализации компонент программной системы средствами конкретного языка.

P.S. Далее воспользуемся умной книгой.

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

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

Концептуальная база объектно-ориентированной программирования (парадигмы/стиля)

База = объектная модель, основывающаяся на четырех
главных принципах:

  1. абстрагирование (Всадник на белом коне)
  2. инкапсуляция (Всадник на рыжем коне)
  3. модульность (Всадник на вороном коне)
  4. иерархия (Всадник на бледном коне)

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

Снова про проектирование и программирование

  • объектно-ориентированное программирование = представление программной системы в виде объектов
  • объектно-ориентированного проектирования = проектирования на основе объектной модели

Бонусные принципы

Не являются обязательными, но полезны:

  1. типизация
  2. параллелизм
  3. сохраняемость

Но есть же другая классификация!

Неформально можно еще выделить "трёх китов" объектно-ориентированного программирования, таких как: инкапсуляция, полиморфизм и наследование.

center

P.S. Однако, что мы говорили про "классификации" и где их место 😜

Абстрагирование

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

Такие разные абстракции...

Можно выделить следующий спектр абстракций:

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

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

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

  • абстрагирование направлено на наблюдаемое поведение (контракт) объекта;
  • инкапсуляция обеспечивает сокрытие его реализации.

Интерфейс и реализация

  • Интерфейс отражает внешнее поведение абстракции, специфицируя поведение всех объектов данного класса.
  • Внутренняя реализация описывает представление этой абстракции и механизмы достижения желаемого поведения объекта.

Так что же такое инкапсуляция?

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

Современные объектно-ориентированные языки, имеют механизмы управления доступом к методам и данным объекта.

P.S. Широко известные в узких кругах являются ключевые слова public, protected и private.

Абстрагирование + инкапсуляция + ... ???

Разрабатываем систему на ООП → очень большое количество абстракций (классов и интерфейсов) → еще и связанность между абстракциями большая.

Зачем на это ООП тогда?

И снова здравствуй, декомпозиция!

Модульность — это свойство системы, которая была разложена на внутренне связные, но слабо связные между собой модули.

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

P.S. Использование модулей характерно не только для объектно-ориентированного программирования.

Абстрагирование + инкапсуляция + модульность + + ... ???

Еще один инструмент борьбы со сложностью - принцип иерархии.

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

Иерархия — это упорядочение абстракций путем расположения
их по уровням.

Виды иерархических структур

  • структуры классов (иерархические отношения "is a")

    Отношения реализуются с помощью наследования или генерализации.

  • структуры объектов (отношения вида "part of")

    Отношение определяет то, из каких классов может состоять рассматриваемый класс (вложенность, агрегат).

В результате получаем...

center

Типизация

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

Центральное место в типизации занимают механизмы согласования типов.

P.S. Типизация, как и инкапсуляция, больше относится к области объектно-ориентированного программирования, нежели к области объектно-ориентированного проектирования.

Вспоминаем про типизацию

На основе хорошей статьи, вспомним про типизацию.

Языки программирования по типизации принято делить на два больших лагеря:

  • типизированные: C, Python, Scala, PHP и Lua
  • нетипизированные (бестиповые): язык ассемблера, Forth и Brainfuck

Статическая / динамическая типизация

  • Статическая определяется тем, что конечные типы переменных и функций устанавливаются на этапе компиляции. Т.е. уже компилятор на 100% уверен, какой тип где находится.
  • В динамической типизации все типы выясняются уже во время выполнения программы.

Статическая: C, Java, C#;
Динамическая: Python, JavaScript, Ruby.

Сильная / слабая типизация (строгая / нестрогая)

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

Сильная: Java, Python, Haskell, Lisp;
Слабая: C, JavaScript, Visual Basic, PHP.

Явная / неявная типизация

  • Явно-типизированные языки отличаются тем, что тип новых переменных / функций / их аргументов нужно задавать явно.
  • Соответственно языки с неявной типизацией перекладывают эту задачу на компилятор / интерпретатор.

Явная: C++, D, C#
Неявная: PHP, Lua, JavaScript

Популярные языки и типизация

Язык 1 2 3
JavaScript Динамическая Слабая Неявная
Python Динамическая Сильная Неявная
Java Статическая Сильная Явная
PHP Динамическая Слабая Неявная
C Статическая Слабая Явная
C++ Стат./Динам. (C++17) Слаб./Сил. Явн./Неявн. (C++11)
C# Стат./Динам. (C# 4.0) Сильная Явн./Неявн.

Больше в этой статье.

Сильная (строгая) и статическая типизация

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

Внимание! Термины сильная и слабая типизация не являются однозначно трактуемыми и чаще всего используются для указания на достоинства и недостатки конкретного языка.

Сильная типизация - это зло?

"+": Сильная типизация заставляет разработчика соблюдать правила использования абстракций.

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

Как хранить множества объектов разных типов?

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

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

P.S.P.S. С std::variant (C++17) можно использовать безопасные объединения типов и хранить множество разных типов в одном объекте.

Параллелизм

Многопоточность ≠ параллелизм ≠ асинхронное программирование

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

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

Параллелизм дает возможность объектам действовать одновременно.

Параллелизм — это свойство, отличающее активные объекты от пассивных.

Сохраняемость

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

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

P.S. Первые три больше про языки, последние - про базы данных.

Опять про слонов забыли?

Опять кого-то забыли?

← Вот эта парочка выехала за нами.

Класс

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

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

Объект

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

Объекты ООП != Объекты реального мира

Объекты ООП - это наше представление об объектах реального мира.

Никто уже всерьез не упарывается экзистенциальным поиском Cвятого Грааля ООП.
Комментарий с habr.

Мы еще обсудим эту троицу

center

И еще кое-что...

В Турбо Паскале ООП реализовано начиная с версии 5.5. СЕРЬЕЗНО. А тут еще одно подтверждение.

center

Вопросы?

center

If not, just clap your hands!