Катастрофа ООП и многомерные модели данных
От: velkin Удмуртия http://blogs.rsdn.org/effective/
Дата: 21.10.21 12:05
Оценка: 72 (1) +1 -4 :)

Катастрофа ООП


Перевод статьи: Объектно-ориентированное программирование — катастрофа на триллион долларов

C++ — ужасный объектно-ориентированный язык. Ограничение вашего проекта до C означает, что люди не напортачат ни с какой идиотской «объектной моделью».
Линус Торвальдс, создатель Linux
...
Методы или свойства, которые обеспечивают доступ к определённым полям, не лучше, чем непосредственное изменение значения поля. Не имеет значения, изменяете ли вы состояние объекта с помощью необычного свойства или метода, результат один и тот же — изменённое состояние.
...
Падение четырёх столпов:
1) Абстракция.
2) Наследование.
3) Инкапсуляция.
4) Полиморфизм.

http://files.rsdn.org/99832/oop_real.png

Модель данных

В классической теории, модель данных это формальная теория представления и обработки данных.


Структуры и классы


Для начала создам структуру и класс на языке C++ в обобщённой парадигме программирования. В общем и целом можно считать, что я создаю массивы разнородных элементов или разнородные массивы (гетерогенные), в противовес однородным (гомогенным). Учитывая неизменность количества элементов их можно считать статическими в противовес динамическим.

// структура прямоугольника
template <typename TValue>
struct SRectangle
{
    TValue x = 0;
    TValue y = 0;
    TValue width = 0;
    TValue height = 0;
};

// класс прямоугольника
template <typename TValue>
class CRectangle
{
public:
    TValue x = 0;
    TValue y = 0;
    TValue width = 0;
    TValue height = 0;
};

void print_srectangle(const SRectangle<double>& srectangle)
{
    qDebug() << "x = " << srectangle.x
             << "y = " << srectangle.y
             << "width = " << srectangle.width
             << "height = " << srectangle.height;
}

void print_crectangle(const CRectangle<double>& crectangle)
{
    qDebug() << "x = " << crectangle.x
             << "y = " << crectangle.y
             << "width = " << crectangle.width
             << "height = " << crectangle.height;
}

void using_rectangle()
{
    SRectangle<double> srectangle;
    CRectangle<double> crectangle;
    print_srectangle(srectangle);
    print_crectangle(crectangle);
}

/*
Вывод using_rectangle:
x =  0 y =  0 width =  0 height =  0
x =  0 y =  0 width =  0 height =  0
*/


Код работает, но только потому, что я добавил в класс модификатор доступа public, то есть открытый доступ. А по хорошему я не должен дописывать ничего подобного, чтобы разграничить подходы, где структура похожа на структуру Си, пусть и в парадигме обобщённого программирования C++, а класс это тот самый новый ООП подход при переходе от Си к C++.

// класс прямоугольника
template <typename TValue>
class CRectangle
{
    TValue x = 0;
    TValue y = 0;
    TValue width = 0;
    TValue height = 0;
};

void print_crectangle(const CRectangle<double>& crectangle)
{
    qDebug() << "x = " << crectangle.x // ошибка: 'x' is a private member of 'CRectangle<double>'
             << "y = " << crectangle.y // ошибка: 'y' is a private member of 'CRectangle<double>'
             << "width = " << crectangle.width // ошибка: 'width' is a private member of 'CRectangle<double>'
             << "height = " << crectangle.height; // ошибка: 'height' is a private member of 'CRectangle<double>'
}


Инкапсуляция и свойства


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

Инкапсуляция (англ. encapsulation, от лат. in capsula) — в информатике размещение в одном компоненте данных и методов, которые с ними работают. В реализации большинства языков программирования (C++, C#, Java и другие), обеспечивает механизм сокрытия, позволяющий разграничивать доступ к различным компонентам программы.


Но что если мне нужен доступ к закрытым полям данных. На помощь приходит шаблон проектирование "Свойство". Одни и те же шаблоны проектирования можно реализовывать по разному. Воспользуюсь самым примитивным известным мне способом.

// структура прямоугольника
template <typename TValue>
struct SRectangle
{
    TValue x = 0;
    TValue y = 0;
    TValue width = 0;
    TValue height = 0;
};

// класс прямоугольника
template <typename TValue>
class CRectangle
{
    TValue x = 0;
    TValue y = 0;
    TValue width = 0;
    TValue height = 0;
public:
    inline const TValue& get_x() const { return x; }
    inline const TValue& get_y() const { return y; }
    inline const TValue& get_width() const { return width; }
    inline const TValue& get_height() const { return height; }

    inline void set_x(const TValue& x) { this->x = x; }
    inline void set_y(const TValue& y) { this->y = y; }
    inline void set_width(const TValue& width) { this->width = width; }
    inline void set_height(const TValue& height) { this->height = height; }
};

void print_srectangle(const SRectangle<double>& srectangle)
{
    qDebug() << "x = " << srectangle.x
             << "y = " << srectangle.y
             << "width = " << srectangle.width
             << "height = " << srectangle.height;
}

void print_crectangle(const CRectangle<double>& crectangle)
{
    qDebug() << "x = " << crectangle.get_x()
             << "y = " << crectangle.get_y()
             << "width = " << crectangle.get_width()
             << "height = " << crectangle.get_height();
}

void using_rectangle()
{
    SRectangle<double> srectangle;
    CRectangle<double> crectangle;
    print_srectangle(srectangle);
    print_crectangle(crectangle);
    srectangle.x = 1;
    srectangle.y = 2;
    srectangle.width = 3;
    srectangle.height = 4;
    print_srectangle(srectangle);
    crectangle.set_x(5);
    crectangle.set_y(6);
    crectangle.set_width(7);
    crectangle.set_height(8);
    print_crectangle(crectangle);
}

/*
Вывод using_rectangle:
x =  0 y =  0 width =  0 height =  0
x =  0 y =  0 width =  0 height =  0
x =  1 y =  2 width =  3 height =  4
x =  5 y =  6 width =  7 height =  8
*/


Пример стал чуть ближе к классическому классу, можно было бы даже заменить print_crectangle на CRectangle.print(). Тем не менее разница в количестве кода по сравнению со структурой на лицо, а ведь здесь ещё нет ни конструктора с деструктором, ни много чего ещё.

Вычисление площади прямоугольника


Для начала вычислю площадь прямоугольника внешней функцией.

// площадь прямоугольника
template <typename TValue>
TValue rectangle_area(const TValue& width, const TValue& height)
{
    return width * height;
}

void using_rectangle()
{
    SRectangle<double> srectangle;
    CRectangle<double> crectangle;
    print_srectangle(srectangle);
    print_crectangle(crectangle);
    srectangle.x = 1;
    srectangle.y = 2;
    srectangle.width = 3;
    srectangle.height = 4;
    print_srectangle(srectangle);
    crectangle.set_x(5);
    crectangle.set_y(6);
    crectangle.set_width(7);
    crectangle.set_height(8);
    print_crectangle(crectangle);

    qDebug() << "srectangle_area = " << rectangle_area(
                    srectangle.width, srectangle.height);
    qDebug() << "crectangle_area = " << rectangle_area(
                    crectangle.get_width(), crectangle.get_height());
}

/*
Вывод using_rectangle:
x =  0 y =  0 width =  0 height =  0
x =  0 y =  0 width =  0 height =  0
x =  1 y =  2 width =  3 height =  4
x =  5 y =  6 width =  7 height =  8
srectangle_area =  12
crectangle_area =  56
*/


Метод класса


А теперь встрою функцию в класс, в общей терминологии сделаю её методом, в терминологии C++ функцией-членом класса.

// структура прямоугольника
template <typename TValue>
struct SRectangle
{
    TValue x = 0;
    TValue y = 0;
    TValue width = 0;
    TValue height = 0;
};

// класс прямоугольника
template <typename TValue>
class CRectangle
{
    TValue x = 0;
    TValue y = 0;
    TValue width = 0;
    TValue height = 0;
public:
    inline const TValue& get_x() const { return x; }
    inline const TValue& get_y() const { return y; }
    inline const TValue& get_width() const { return width; }
    inline const TValue& get_height() const { return height; }

    inline void set_x(const TValue& x) { this->x = x; }
    inline void set_y(const TValue& y) { this->y = y; }
    inline void set_width(const TValue& width) { this->width = width; }
    inline void set_height(const TValue& height) { this->height = height; }

    TValue rectangle_area() const { return width * height; }
};

void print_srectangle(const SRectangle<double>& srectangle)
{
    qDebug() << "x = " << srectangle.x
             << "y = " << srectangle.y
             << "width = " << srectangle.width
             << "height = " << srectangle.height;
}

void print_crectangle(const CRectangle<double>& crectangle)
{
    qDebug() << "x = " << crectangle.get_x()
             << "y = " << crectangle.get_y()
             << "width = " << crectangle.get_width()
             << "height = " << crectangle.get_height();
}

// площадь прямоугольника
template <typename TValue>
TValue rectangle_area(const TValue& width, const TValue& height)
{
    return width * height;
}

void using_rectangle()
{
    SRectangle<double> srectangle;
    CRectangle<double> crectangle;
    print_srectangle(srectangle);
    print_crectangle(crectangle);
    srectangle.x = 1;
    srectangle.y = 2;
    srectangle.width = 3;
    srectangle.height = 4;
    print_srectangle(srectangle);
    crectangle.set_x(5);
    crectangle.set_y(6);
    crectangle.set_width(7);
    crectangle.set_height(8);
    print_crectangle(crectangle);

    qDebug() << "srectangle_area = " << rectangle_area(
                    srectangle.width, srectangle.height);
    qDebug() << "crectangle_area = " << crectangle.rectangle_area();
}

/*
Вывод using_rectangle:
x =  0 y =  0 width =  0 height =  0
x =  0 y =  0 width =  0 height =  0
x =  1 y =  2 width =  3 height =  4
x =  5 y =  6 width =  7 height =  8
srectangle_area =  12
crectangle_area =  56
*/


Повторное использование функций


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

Вот две алгоритмически абсолютно одинаковые функции.
// площадь прямоугольника
template <typename TValue>
TValue rectangle_area(const TValue& width, const TValue& height)
{
    return width * height;
}

// что-то там
template <typename TValue>
TValue something_there(const TValue& some_value, const TValue& another_value)
{
    return some_value * another_value;
}


Но сделав функцию методом, а потом и прибив её к внутреннему состоянию класса мы сделали алгоритм не повторно используемым.

class CRectangle
{
    // начало блока описывающего внутреннее состояние класса
    TValue x = 0;
    TValue y = 0;
    TValue width = 0;
    TValue height = 0;
    // конец блока описывающего внутреннее состояние класса
public:
    inline const TValue& get_x() const { return x; }
    inline const TValue& get_y() const { return y; }
    inline const TValue& get_width() const { return width; }
    inline const TValue& get_height() const { return height; }

    inline void set_x(const TValue& x) { this->x = x; }
    inline void set_y(const TValue& y) { this->y = y; }
    inline void set_width(const TValue& width) { this->width = width; }
    inline void set_height(const TValue& height) { this->height = height; }

    // использование внутреннего состояния класса вне свойств
    TValue rectangle_area() { return width * height; }
};


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

Наследование и делегирование


А ещё я с самого начала специально создал структуру и класс прямоугольника и поместил в них все поля скопом.

Хотя разумно было бы разделить поля прямоугольника на:
1) Двухмерная координата (она же точка).
2) Двухмерный размер.

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

// структура двухмерной координаты
template <typename TValue>
struct SCartesianPoint2D
{
    TValue x = 0;
    TValue y = 0;
};

// структура двухмерного размера
template <typename TValue>
struct SSize2D
{
    TValue width = 0;
    TValue height = 0;
};

// структура прямоугольника
template <typename TValue>
struct SRectangle
{
    SCartesianPoint2D<TValue> cartesian_point2d;
    SSize2D<TValue> size2d;
};

// класс двухмерной координаты
template <typename TValue>
class CCartesianPoint2D
{
    TValue x = 0;
    TValue y = 0;
public:
    inline const TValue& get_x() const { return x; }
    inline const TValue& get_y() const { return y; }

    inline void set_x(const TValue& x) { this->x = x; }
    inline void set_y(const TValue& y) { this->y = y; }
};

// класс двухмерного размера
template <typename TValue>
class CSize2D
{
    TValue width = 0;
    TValue height = 0;
public:
    inline const TValue& get_width() const { return width; }
    inline const TValue& get_height() const { return height; }

    inline void set_width(const TValue& width) { this->width = width; }
    inline void set_height(const TValue& height) { this->height = height; }

    // площадь прямоугольника в классе размер
    TValue rectangle_area() const { return width * height; }
};

// класс прямоугольника
template <typename TValue>
class CRectangle
{
    // начало блока описывающего внутреннее состояние класса
    CCartesianPoint2D<TValue> cartesian_point2d;
    CSize2D<TValue> size2d;
    // конец блока описывающего внутреннее состояние класса
public:
    inline const CCartesianPoint2D<TValue>& get_cartesian_point2d() const
    { return cartesian_point2d; }
    inline const CSize2D<TValue>& get_size2d() const { return size2d; }

    inline void set_cartesian_point2d(const TValue& cartesian_point2d)
    { this->cartesian_point2d = cartesian_point2d; }
    inline void set_size2d(const TValue& size2d) { this->size2d = size2d; }

    // лишнее делегирование
    inline const TValue& get_x() const { return cartesian_point2d.get_x(); }
    inline const TValue& get_y() const { return cartesian_point2d.get_y(); }
    inline const TValue& get_width() const { return size2d.get_width(); }
    inline const TValue& get_height() const { return size2d.get_height(); }

    inline void set_x(const TValue& x) { cartesian_point2d.set_x(x); }
    inline void set_y(const TValue& y) { cartesian_point2d.set_y(y); }
    inline void set_width(const TValue& width) { size2d.set_width(width); }
    inline void set_height(const TValue& height) { size2d.set_height(height); }

    // площадь прямоугольника в классе прямоугольник
    TValue rectangle_area() const { return size2d.get_width() * size2d.get_height(); }
};

void print_srectangle(const SRectangle<double>& srectangle)
{
    qDebug() << "x = " << srectangle.cartesian_point2d.x
             << "y = " << srectangle.cartesian_point2d.y
             << "width = " << srectangle.size2d.width
             << "height = " << srectangle.size2d.height;
}

void print_crectangle(const CRectangle<double>& crectangle)
{
    qDebug() << "x = " << crectangle.get_x()
             << "y = " << crectangle.get_y()
             << "width = " << crectangle.get_width()
             << "height = " << crectangle.get_height();
}

// площадь прямоугольника
template <typename TValue>
TValue rectangle_area(const TValue& width, const TValue& height)
{
    return width * height;
}

// что-то там
template <typename TValue>
TValue something_there(const TValue& some_value, const TValue& another_value)
{
    return some_value * another_value;
}

void using_rectangle()
{
    SRectangle<double> srectangle;
    CRectangle<double> crectangle;
    print_srectangle(srectangle);
    print_crectangle(crectangle);
    srectangle.cartesian_point2d.x = 1;
    srectangle.cartesian_point2d.y = 2;
    srectangle.size2d.width = 3;
    srectangle.size2d.height = 4;
    print_srectangle(srectangle);
    crectangle.set_x(5);
    crectangle.set_y(6);
    crectangle.set_width(7);
    crectangle.set_height(8);
    print_crectangle(crectangle);

    qDebug() << "srectangle_area = " << rectangle_area(
                    srectangle.size2d.width, srectangle.size2d.height);
    qDebug() << "crectangle_area = " << crectangle.rectangle_area();
    qDebug() << "crectangle_area = " << crectangle.get_size2d().rectangle_area();
}

/*
Вывод using_rectangle:
x =  0 y =  0 width =  0 height =  0
x =  0 y =  0 width =  0 height =  0
x =  1 y =  2 width =  3 height =  4
x =  6 y =  0 width =  7 height =  8
srectangle_area =  12
crectangle_area =  56
crectangle_area =  56
*/


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

Так же я получил дубликаты вычисления площади прямоугольника, один метод находится в классе размера, другой в классе прямоугольника, причём делегирование не используется. А я ещё мог бы в классе размера назвать этот медод size_area или просто area, так же как и в классе CRectangle.

Но проблема не просто в том, что код дублируется многократно. Казалось бы не дублируй и всё. Но кто в наше время пишет свои велосипеды, разве что для обучения. А по факту имеем фрейморки, и классы прямоугольников от разных поставщиков:
1) вендор VasjaPupkin: VPRectangle.
2) вендор Qt: QRect.
И так далее.

Многомерные модели данных


Существуют другие способы работать с моделями данных.

OLAP (англ. online analytical processing, интерактивная аналитическая обработка) — технология обработки данных, заключающаяся в подготовке суммарной (агрегированной) информации на основе больших массивов данных, структурированных по многомерному принципу.


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

OLAP-куб — (On-Line Analytical Processing — интерактивный анализ данных) многомерный массив данных, как правило, разреженный и долговременно хранимый, используемый в OLAP. Может быть реализован на основе универсальных реляционных СУБД или специализированным программным обеспечением.


Структура OLAP-куба

https://intuit.ru/EDI/14_06_16_2/1465856498-31733/tutorial/596/objects/1/files/01_06.jpg

Хранилища данных Основы технологии хранилищ данных

https://present5.com/presentation/659639_176290058/image-106.jpg

Данные как гиперкуб


https://upload.wikimedia.org/wikipedia/commons/d/d7/8-cell.gif

Тессера́кт (от др.-греч. τέσσαρες ἀκτῖνες — «четыре луча») — четырёхмерный гиперкуб, аналог обычного трёхмерного куба в четырёхмерном пространстве.

Гиперку́б — обобщение куба на случай с произвольным числом измерений.


Количество измерений:
1) последовательность
2) квардрат (прямоугольник)
3) куб (параллепипед)
4) тессеракт
n) гиперкуб

Количество элементов вдоль одного измерения:
0) нет элементов.
1) фиксированное 1 элемент.
n) фиксированное больше одного элемента.
∞) изменяемое бесконечное количество элементов.

Диапазоны количества элеметов в последовательности:
1) минимальное 0, 1, n
2) максимальное 1, n, ∞


кол-во элементов однородные типы разнородные типы
0..1
0..n
0..∞ динамический массив
1..1 значение вариант
1..n
1..∞
n..n статический массив структура
n..∞
одномерное двухмерное трёхмерное четырёхмерное ... n-мерное
#..#
#..# #..#
#..# #..# #..#
#..# #..# #..# #..#
#..# #..# #..# #..# #..#
#..# #..# #..# #..# #..# #..#
По сути битомолкам хватило бы n-мерных структур вложенных друг в друга, да и не только битомолкам.

Итоги


Всё это были просто размышлизмы с простейшими примерами. На основе этого рано делать какие-либо выводы. Но можно обсудить кто что думает.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.