После статьи «ООП не мертво. Вы просто пользуетесь им как молотком по клавиатуре» комментарии кипели

Кто-то звал Smalltalk, кто-то бросал в нас Haskell, кто-то доставал из-под кровати подшивку статей «ECS лучше всего» — и всё это с праведной уверенностью.

Что ж…

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

Code-Battle: MVP графического редактора

  • Задача: реализовать базовый графический редактор

  • Фигуры: точка, линия, круг, квадрат, прямоугольник, треугольник, ромб, овал

  • Функциональность: добавление фигур на канвас, отрисовка

Правила:

  • Один .cpp / .py / .exs / .c / .rs / .lisp / whatever файл

  • Язык и подход — любой

  • Главное: читаемость, понятность, архитектурная целостность

  • Пишите максимально просто, если надо - используйте псевдокод

  • MVP сейчас — фичи потом

  • Все решения оформляются одним Merge Request

  • В конце — разбор решений, сравнение подходов, и, как обычно: наказание невиновных и награждение непричастны

Для затравки: C++ / ООП реализация (v1)

В качестве точки отсчёта — наш базовый вариант.

ООП, без усложнений.
Всё в одном файле.

#include <iostream>
#include <string>
#include <vector>

class Shape {
public:
    virtual void draw() const = 0;
    virtual std::string name() const = 0;
    virtual ~Shape() {}
};

class Point : public Shape {
    int x, y;
public:
    Point(int x, int y) : x(x), y(y) {}
    void draw() const override {
        std::cout << «Drawing Point at (« << x << «, « << y << «)\\n»;
    }
    std::string name() const override { return «Point»; }
};

class Circle : public Shape {
    int x, y, r;
public:
    Circle(int x, int y, int r) : x(x), y(y), r(r) {}
    void draw() const override {
        std::cout << «Drawing Circle at (« << x << «, « << y << «), r = « << r << «\\n»;
    }
    std::string name() const override { return «Circle»; }
};

class Rectangle : public Shape {
    int x, y, w, h;
public:
    Rectangle(int x, int y, int w, int h) : x(x), y(y), w(w), h(h) {}
    void draw() const override {
        std::cout << «Drawing Rectangle at (« << x << «, « << y
                  << «), « << w << «x» << h << «\\n»;
    }
    std::string name() const override { return «Rectangle»; }
};

class Canvas {
    std::vector<Shape*> shapes;
public:
    void add(Shape* s) { shapes.push_back(s); }
    void render() const {
        for (auto s : shapes) s->draw();
    }
    ~Canvas() {
        for (auto s : shapes) delete s;
    }
};

int main() {
    Canvas canvas;
    canvas.add(new Point(1, 1));
    canvas.add(new Circle(5, 5, 3));
    canvas.add(new Rectangle(0, 0, 6, 3));

    canvas.render();
    return 0;
}

Теперь — вы.

Реализуйте то же самое:

  • На любом языке и в любой парадигме

  • В своей манере — функциональной, процедурной, декларативной, минималистской

  • Без фреймворков. Без магии. Только архитектура

Это только начало.

Следующая итерация уже в пути. Требования изменятся. Канвас расширится. Архитектура проявит себя.

А мы, разберём каждую реализацию — и, возможно, найдём ответ на вопрос:
«To OOP or not to OOP — вот в чем загвоздка”.

Репозитарий с правилами, шаблоном и инструкциями:

ООП Баттл Репозитарий

Комментарии (66)


  1. withkittens
    14.05.2025 12:29

    Горшочек, не вари.


  1. skovoroad
    14.05.2025 12:29

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

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

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

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


    1. Kahelman Автор
      14.05.2025 12:29

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

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


      1. skovoroad
        14.05.2025 12:29

        Ваш пример не убеждает, а разубеждает пользоваться ООП.

        Он не подчёркивает его сильные стороны (разделение кода на изолированные куски с хорошо описанной изолированной ответственностью)

        Но зато он отлично иллюстрирует аргументы критиков ООП: вы усложнили примитивную задачу.

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


        1. Kahelman Автор
          14.05.2025 12:29

          Это баттл- я не предлагаю реализацию. Я предлагаю сравнить подходы. Ссылка на репозитарий в конце. Сделайте вашу идеальную реализацию и пришлите MR. Потом можем осудить…


          1. skovoroad
            14.05.2025 12:29

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

            По сравнению с вашим у него масса достоинств, начиная с того, что он компилируется.

            struct Rectangle
            {
                int center[2] = {0, 0};
                unsigned size[2] = {0, 0};
            };
            
            struct Circle
            {
                int center[2] = {0, 0};
                int radius = 0;
            };
            
            void draw(const Rectangle& r)
            {
                // draw rectangle
            }
            
            void draw(const Circle& c)
            {
                // draw circle
            }
            
            int main()
            {
                Rectangle r{{1, 2}, {10, 20}};
                Circle c{{3, 4}, 5};
            
                draw(r);
                draw(c);
            }


            1. Kahelman Автор
              14.05.2025 12:29

              Не хватает: треугольника, квадрата, овала, ромба. :)


              1. Chamie
                14.05.2025 12:29

                У вас тоже

                К слову, знать бы ещё, что вы под ними подразумеваете, потому что в моей геометрии для рисования прямоугольника на плоскости нужно как минимум 5 чисел (крутит в руке телефон), а «овалов» там вообще гора разных.


            1. Kahelman Автор
              14.05.2025 12:29

              И не плохо бы было их в коллекцию запихнуть чтобы в main все не прописывать.


              1. skovoroad
                14.05.2025 12:29

                1. зачем в коллекцию? В вашем примере коллекция никак не используется

                2. но если есть какая-то ломовая необходимость в коллекции конкретно в этом микроскопическом примере, у вас есть стандартный std::vector<std::variant>

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


                1. Kahelman Автор
                  14.05.2025 12:29

                  Тогда возьмете на себя задачу реализовать на чистом процедурном подходе? std:vectorstd:variant это хак по меньшей мере. Стандартные процедурные языки не используют понятие темплейтов. Поправьте если я не прав. Соответственно в рамках чисто процедурного подхода эта задача просто не решается. Вам надо либо с void указателями работать, либо «изобретать» свой rtti. Что имеет место быть, но как бы добавляет аргументов в сторону сторонников ООП подхода. Со своей стороны могу на AWK реализовать :)


                  1. eao197
                    14.05.2025 12:29

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

                    Ada-83 как раз таким языком и была.


                  1. skovoroad
                    14.05.2025 12:29

                    Да господи, стандартная библиотека в 2025-м году от РХ у него хак. Как будто кто-то запрещает писать на плюсах в процедурном стиле. Ну возьмите раст, там enum из коробки - тот же вариант. Код ещё в три раза короче станет, кстати.

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


                    1. Comdiv
                      14.05.2025 12:29

                      что проблема не в ООП, а в вашей статье

                      Замените слово «статья» на «код» и окажется, что вы описали причину, почему ООП как общая парадигма для написания кода несостоятельна.


                      1. skovoroad
                        14.05.2025 12:29

                        Зачем я буду заменять слово "статья" на слово "код", если я имею а виду не код, а всю статью? Т.е. контекст использования этого кода.

                        Код, конечно, тоже плохой, но речь не об этом.


                      1. Comdiv
                        14.05.2025 12:29

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


                    1. Kahelman Автор
                      14.05.2025 12:29

                      Замените слово shape на Message, а Canvas на messageQueue. Получите задачу обработки входящих сообщений. Не ожидал что абстрактное мышление это отдельная фича :)


                      1. skovoroad
                        14.05.2025 12:29

                        Слушайте, если у вас всё надо заменить, чтобы вас правильно понять, может, вы уже признаете, что пример плохой? Задача организации очереди сообщений это другая задача и действительно служила бы лучше целям статьи.


  1. onets
    14.05.2025 12:29

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


    1. Kahelman Автор
      14.05.2025 12:29

      За данные и их согласованность должна отвечать БД. Правило это не модно и молодёжно. Правда никто пока не показал как обеспечить гарантированную согласованность данных в распределенной системе. :)


      1. askv
        14.05.2025 12:29

        Потому что таких гарантий не существует? )


        1. Kahelman Автор
          14.05.2025 12:29

          Потому что задача «византийских генералов» не решаема :(

          https://ru.m.wikipedia.org/wiki/Задача_византийских_генералов


  1. ermouth
    14.05.2025 12:29

    Редактор – это где редактировать можно. В примере какая-то надстройка для усложнения использования канваса, простите.


    1. Kahelman Автор
      14.05.2025 12:29

      Ага вы готовы за 20-30 минут сделать MVP редактора в вашем понимании?


      1. ermouth
        14.05.2025 12:29

        Зачем? По вашим условиям на любом языке получится примерно одно и то же. По вашим условиям по сути нужно через вызов add с кортежем {тегФигуры, ...параметры} положить его куда-то в стейт приложения, а потом по команде их все нарисовать.

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


        1. Kahelman Автор
          14.05.2025 12:29

          Согласен. Идея в том что дальше идут «требования на изменения» и «дополнительные фичи» идея посмотреть как будут развиваться процедурный/функциональный/ООП подходы


  1. eao197
    14.05.2025 12:29

    Как же печально видеть в 2025-ом году C++ный код с ручными new/delete и детскими ошибками, описанными еще в первых изданиях "Язык программирования C++" :(((


    1. Kahelman Автор
      14.05.2025 12:29

      Это минимальная реализация - максимально близко к «классикам». Жду вашего MR с «правильной реализацией». Поскольку тут Баттл- одними комментариями не отделаетесь. Нет MR - слив засчитан :)


      1. eao197
        14.05.2025 12:29

        Я хз что такое MR, но на слабо вы решили взять не того. В последние годы часть моей работы состоит как раз в том, чтобы бить по рукам за говнокод с ручными new/delete, возвратом std::string-ов по значению и т.п.

        Если бы вы свой Canvas определили хотя бы так:

        using ShapeUptr = std::unique_ptr<Shape>;
        class Canvas {
            std::vector<ShapeUptr> shapes;
        public:
            void add(ShapeUptr s) { shapes.push_back(std::move(s)); }
            void render() const {
                for (auto & s : shapes) s->draw();
            }
        };
        

        То у вас бы получилось и короче, и надежнее.

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

        https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#r3-a-raw-pointer-a-t-is-non-owning
        https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#r11-avoid-calling-new-and-delete-explicitly
        https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#r20-use-unique_ptr-or-shared_ptr-to-represent-ownership


        1. Kahelman Автор
          14.05.2025 12:29

          Первоначальный вариант был с unique_ptr. Потом выбросил, т.к. вопрос про архитектуру ООП vs XXX а не про современные подходы и best practice в C++.


          1. eao197
            14.05.2025 12:29

            Демонстрация "как в этом вашем C++ отстрелить себе ногу не прилагая никаких усилий" получилась наотлично.


        1. Kahelman Автор
          14.05.2025 12:29

          MR - merge request


        1. Cfyz
          14.05.2025 12:29

          using ShapeUptr = std::unique_ptr<Shape>;

          Использование алиасов почём зря (например в подобных тривиальных случаях) ухудшает читаемость кода. Не надо так.

          shapes.push_back(std::move(s))

          Скорее наверное emplace_back()?

          Я хз что такое MR

          Это довольно иронично, что вы в 2025 не знаете что такое MR =). Подсказка: это как PR, только gitlab.


          1. eao197
            14.05.2025 12:29

            Не надо так.

            Спасибо, но вредные советы (а это очень вредный совет) идут в сад. Желающие выписывать раз за разом std::unique_ptr<что-то-там> следуют туда же.

            Скорее наверное emplace_back()?

            Когда у нас на входе готовый unique_ptr, то разницы быть не должно.


            1. Cfyz
              14.05.2025 12:29

              Спасибо, но вредные советы (а это очень вредный совет) идут в сад.

              Ну так вы первый начали.

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

              Даже дюжина повторений в коде не стоит этого. Вы несколько раз экономите невероятные 10 символов, из которых половину все равно подставит IDE, а читаете это снова и снова и снова каждый раз, когда приходится вернуться к этому фрагменту кода.

              Одно дело, если тип фундаментален для проекта и используется в нем повсеместно. Но в единичных и/или простых случаях использование всяких FooPtr, FooMap и прочих одному автору понятных типов только лишь потому что видите ли лень пару лишних клавиш нажать -- это вредная практика.


              1. eao197
                14.05.2025 12:29

                Любой алиас это сокрытие конкретного типа

                Нет. Вы сразу ошибаетесь. А из ошибочных предпосылок происходят и ошибочные выводы.

                Все, что вы написали -- это ерунда. Особенно про "видите ли лень пару лишних клавиш нажать -- это вредная практика".

                Более того, когда в проекте есть устоявшиеся правила именования указателей (вроде ShapeUptr, ShapeShptr), то уже из названия типа видно, с каким указателем мы имеем дело и это отнимает гораздо меньше усилий, чем читать бесконечные std::unique_ptr. Особенно в сочетаниях типа std::shared_ptr<std::vector<std::unique_ptr<Shape>>>.

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

                ЗЫ. Минус на вашем комментарии не от меня. Я вообще на Хабре минусы не расставляю (за очень-очень-очень редким исключением).


                1. Cfyz
                  14.05.2025 12:29

                  Нет. Вы сразу ошибаетесь. Все, что вы написали -- это ерунда.

                  Вы меня конечно извините, но это аргументация уровня "нет ты".

                  когда в проекте есть устоявшиеся правила именования указателей (вроде ShapeUptr, ShapeShptr), то уже из названия типа видно

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

                  Закавыка в том, что все равно у каждого второго проекта будут свои собственные устоявшиеся правила. Вам может показаться что ShapreUptr это что-то совершенно очевидное, но это не так.

                  Особенно в сочетаниях типа std::shared_ptr<std::vector<std::unique_ptr<Shape>>>

                  По-моему это больше похоже на контр-пример.

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

                  Потому что постоянное жонглирование вложенностью-разыменованием и безымянными полями абстрактных контейнеров это форменная жесть с какой стороны ни посмотри.

                  Имя, которое невыносимо печатать из-за его длины -- это чаще всего признак проблемы, которую надо решать не алиасом.

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

                  Как например использование emplace_back() вместо push_back() даже если в данном конкретном случае компилятор оптимизирует лишний move в ноль? =)

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


                  1. eao197
                    14.05.2025 12:29

                    Вы меня конечно извините, но это аргументация уровня "нет ты".

                    Так если вы наговорили ерунды, то единственное, что можно сказать -- это назвать ерунду ерундой.

                    Я насмотрелся на программистов, которые не используют using-и. И еще больше насмотрелся на результаты их работы. Так, что больше не хочется. После этого любой персонаж, который мне начинает рассказывать про то, что "любой алиас -- это сокрытие типа", просто расписывается в своей профнепригодности. Пардон май френч.

                    Как например использование emplace_back() вместо push_back()

                    Для emplace_back есть очевидный сценарий применения -- это когда у нас на руках есть набор аргументов для конструирования нового объекта в векторе. Типа такого:

                    std::vector<std::string> lines;
                    lines.emplace_back(45uz, '\t');
                    

                    Для добавления же в конец вектора готового объекта предназначен push_back.

                    Все просто и очевидно. Использовать emplace_back как замену push_back -- ну такое себе, "сомнительно, но окай" (с)


                    1. Cfyz
                      14.05.2025 12:29

                      Я насмотрелся на программистов, которые не используют using-и.

                      Ну а я насмотрелся на тех, которые пихают typedef и using где надо и не надо.

                      Явное всегда лучше неявного.

                      Если у вас программисты пишут плохой код потому что они не используют using почем зря -- дело совершенно точно не в using.

                      Для emplace_back есть очевидный сценарий применения <...> Для добавления же в конец вектора готового объекта предназначен push_back.

                      Вот тут вынужден согласиться, в данном случае почем зря emplace приплел.


              1. skovoroad
                14.05.2025 12:29

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

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


                1. Cfyz
                  14.05.2025 12:29

                  Мне неважно, что он юник или шейред

                  Нюанс в том, что когда действительно неважно что это за тип, то как правило это будет typename T или auto =).

                  А вот что автор скрыл под Ptr -- unique, shared, QPointer, указатель из boost или вообще голый указатель -- это обычно существенно влияет на семантику владения, совместимость между типами и как с этим можно обращаться вообще помимо -> и *.

                  Да, внутри функции вам действительно часто все равно, что это за указатель. Ну так в названии переменной или поля тип и не указывается, shape и shape.


                  1. skovoroad
                    14.05.2025 12:29

                    Семантика владения и прочие детали важны при написании кода (и то в большинстве случаев компилятор не даст ошибиться).

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


              1. Dooez
                14.05.2025 12:29

                auto также скрывает тип, но повсеместен в современном C++. Хотя может вы из секты людей, которые всегда явно пишет тип переменных?

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

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

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


                1. Cfyz
                  14.05.2025 12:29

                  auto также скрывает тип, но повсеместен в современном C++. Хотя может вы из секты людей, которые всегда явно пишет тип переменных?

                  Фундаментальное отличие auto в том, что он скрывает тип в очень небольшом контексте. Грубо говоря вот видим объявление auto i = vec.begin(), и вот в пределах экрана эта переменная используется.

                  То же самое с локальным using в пределах области видимости.

                  Проблема с теми алиасами, которые определены где-то в совершенно другом месте и надо вспоминать/искать что это такое. IDE еще могут быстро подсказать, но при чтении/ревью например в web-интерфейсе это очень мешает.

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

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

                  Обобщенный алиас алиасу рознь. Одно дело using ElementType = T; в контейнере. И совсем другое это сокрытие семантики, как это часто происходит с указателями или многоэтажными контейнерами. Бездумное использование алиасов запросто может наоборот стать причиной неочевидной ошибки.

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

                  Вот казалось бы, std::unique_ptr<Shape> -- куда уж лучше. Указатель, уникальный, из стандартной библиотеки. Но нет, это слишком просто.


            1. simplepersonru
              14.05.2025 12:29

              Сделали в проекте такие глобальные юзинги (в условном common/core.h) :

              template <class T>
              using U = std::unique_ptr<T>;

              И также SH с шаредптр

              Читаемость не теряется и не нужно руками делать на каждый такой класс отдельный юзинг.

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


              1. eao197
                14.05.2025 12:29

                Минусы такого подхода:

                • однобуквенные, но значимые идентификаторы. Запоминать чем отличается U<T> от SH<T> от I<T> от P<T> и прочих одно-двух-трех-буквенных индентификаторов такое себе удовольствие. Еще хуже смотреть на код с такими вещами, особенно когда взгляд замыливается от усталости;

                • угловые скобки, которые никуда не деваются и о которые ты все равно спотыкаешься. Может показаться, что SH<std::vector<U<Shape>>> -- это сильно лучше, чем std::shared_ptr<std::vector<std::unique_ptr<Shape>>>, но это, как по мне, тот же самый фрагмент автопортрета Фаберже, только в профиль;

                • ну и главное, если со временем потребуется заменить тип за ShapeUptr на какой-то другой (вместо простого std::unique_ptr на какой-то хитрый собственный тип указателя), то имя ShapeUptr все равно остается на месте.


      1. Cfyz
        14.05.2025 12:29

        Претензия не к ООП или архитектуре.

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


        1. Kahelman Автор
          14.05.2025 12:29

          Я бы был менее категоричен: во-первых: «не очень хорошо знаете современный С++»,

          Во-вторых: в предыдущей статье я ссылался на книгу Гради Буча, которая вышла некоторое время назад. Примеры там написаны на C++/Java без использования новейших фич языков и даже без темплейтов. Так что я старался держаться одного стиля с оригиналом.


  1. MonkeyWatchingYou
    14.05.2025 12:29

    Чтоб повысить заход на эту статью надо было добавить теги #c, #assembler и #xml.
    И непонятно зачем и холиварно.
    Шутка.


  1. nin-jin
    14.05.2025 12:29

    1. Kahelman Автор
      14.05.2025 12:29

      Класс. Спасибо. Вечером смержу :)


    1. ermouth
      14.05.2025 12:29

      Ширина/высота прямоугольника и радиус – по-хорошему uint. Ширина и высота это к слову не dx, dy – которые и правда могли бы быть int. То, что у автора везде int – скорее проблемы автора.


      1. nin-jin
        14.05.2025 12:29

        А это и не ширина и высота. Поправил нейминг. Ну и тип радиуса тоже, спасибо.


      1. Kahelman Автор
        14.05.2025 12:29

        Как в анекдоте: «я знал что дискуссия будет только по последнему вопросы ..»


  1. Cfyz
    14.05.2025 12:29

    Следующая итерация уже в пути. Требования изменятся. Канвас расширится. Архитектура проявит себя.

    Не хватает: треугольника, квадрата, овала, ромба. :)

    Спойлер: в итоге не будет никаких Circle, Rectangle и прочих треугольников, останется только Shape с набором кривых, описывающих контур фигуры.


    1. Kahelman Автор
      14.05.2025 12:29

      Все гораздо проще. Есть план «развития» продукта. Что вполне вписывается в «тестовую» архитектуру. Задача посмотреть как разные подходы будут себя вести. Опять-таки, задача сохранить „time to market“. Ниже привели пример, который вроде как позволит все безгранично расширять. Но как MVP это в любом случае оверинжиниринг.


      1. Chamie
        14.05.2025 12:29

        Вот вам безгранично расширяемый MVP без оверинжиниринга (TypeScript):

        Скрытый текст
        const create = {
            point: (x: number, y: number) =>
                () => `Drawing a Point at (${x},${y})`,
            circle: (x: number, y: number, r: number) =>
                () => `Drawing a Circle R=${r} at (${x},${y})`,
            rectangle: (x1: number, y1: number, x2: number, y2: number, width: number) =>
                () => `Drawing a Rectangle (${x1}, ${y1}) to (${x2},${y2}) width=${width}`,
        };
        
        const getCanvas = () => {
            const shapes: Function[] = [];
            return {
                add: (shape: Function) => shapes.push(shape),
                render: () => shapes.forEach(shape => shape()),
            }
        }
        
        const main = () => {
            const canvas = getCanvas();
            canvas.add(create.circle(1, 2, 3));
            canvas.add(create.point(1, 2));
            canvas.add(create.rectangle(1, 2, 3, 4, 5));
        
            canvas.render();
        }

        Есть к нему какие-нибудь претензии?


    1. ermouth
      14.05.2025 12:29

      не будет никаких Circle

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


      1. Cfyz
        14.05.2025 12:29

        Ну значит возьмем B-сплайны. Какими кривыми аппроксимировать это детали реализации. В первом приближении можно вообще набором прямых обойтись.


        1. ermouth
          14.05.2025 12:29

          возьмем B-сплайны

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


  1. Jijiki
    14.05.2025 12:29

    Скрытый текст
    #include <print>
    #include <string>
    #include <vector>
    //include <vectormath>
    class Shape {
    public:
        virtual void draw() const = 0;
        virtual std::string name() const = 0;
        virtual ~Shape() {}
    };
    template<typename T>
    class Point : public Shape {
        T p[2];//vec2//[]
    public:
        Point(T x, T y) {
            p[0]=x;
            p[1]=y;
        }
        void draw() const override {
            std::println("Drawing {} at {} {}",name(),p[0],p[1]);
        }
        
        std::string name() const override { return std::string("Point"); }
    };
    template<typename T>
    class Circle : public Shape {
        T p[3];//x y r//vec3//[]
    public:
        Circle(T x, T y, T r) {
            p[0]=x; 
            p[1]=y; 
            p[2]=r;
        }
        void draw() const override {
          std::println("Drawing {} at {} {} {}",name(),p[0],p[1],p[2]);
        }
    
        std::string name() const override { return std::string("Circle"); }
    };
    template<typename T>
    class Rectangle : public Shape {
        T p[4]; //x, y, w, h;//vec4//[]
    public:
        Rectangle(T x, T y, T w, T h){ 
            p[0]=x; 
            p[1]=y; 
            p[2]=w; 
            p[3]=h; 
        }
        void draw() const override {
          std::println("Drawing {} at {} {} {} {}",name(),p[0],p[1],p[2],p[3]);
        }
    
        std::string name() const override { return std::string("Rectangle"); }
    };
    //maybe variadic для прохода текущих шейпов
    class Canvas {
        std::vector<Shape*> shapes;//или по T std::vector/std::array текущего шейпа
    public:
        void add(Shape* s) { shapes.push_back(s); }
        void render() const {
            for (auto s : shapes) s->draw();
        }
        ~Canvas() {
            for (auto s : shapes) delete s;
        }
    };
    
    int main() {
        Canvas canvas;
        canvas.add(new Point<int>(1, 1));
        canvas.add(new Circle<int>(5, 5, 3));//состоит из точек
        canvas.add(new Rectangle<int>(0, 0, 6, 3));//состоит из точек
    
        canvas.render();
        return 0;
    }
    
    clang++20 -std=c++26 main.cpp

    во как бы я сделал, но в идеале точки бы были из библиотеки матеши - это будет влиять на архитектуру кстати


    1. Kahelman Автор
      14.05.2025 12:29

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

      std::println("Drawing {} at {} {} {} {}",name(),p[0],p[1],p[2],p[3]);
      }

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


      1. Jijiki
        14.05.2025 12:29

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

        ну значит позиция в точке, и 2 переменные

        Скрытый текст
          template <typename T>
          class Vec2{
          private:
                
          public:
            T vec[2];
            Vec2(T a,T b){
              this->vec[0]=a;
              this->vec[1]=b;
            }
            T& operator [](int idx) {
              return vec[idx];
            }
            const T operator [](int idx)const {
              return vec[idx];
            }
            void qprint(){
              std::println("{} {}",vec[0],vec[1]);
            }
            const f32* data() const { return vec[0]; }
            f32* data() { return vec[0]; }    
          };
        
          using vec2 = Vec2<f32>;
          using ivec2 = Vec2<int>;

        у меня щас так, и мат и кватернион и двойной, перед отрисовкой даже span не нужен просто читаю из array/vector позиции точек и рисуется, значит у вас это тип только под позиции типо позиция картинки в 2д

        сам я впервые даже new пока не использовал

        так громоздко да, но удобно использовать

        int main(){
          vec3 v1(1.0f,2.0f,3.0f);
          vec3 v2(1.0f,2.0f,3.0f);
          vec3 v3=v1;
          v1.qprint();
          mat4 m(5.0f,1.0f,1.0f,0.0f,
        	 0.0f,9.0f,1.0f,1.0f,
        	 6.0f,1.0f,8.0f,0.0f,
        	 0.0f,0.0f,1.0f,2.0f);
          
          mat4 m1(1.0f);
          m.qprint();m1.qprint();
          mat4 m2=m1*m;
          m2.qprint();
        
        
          mat3 m3(5.0f,1.0f,1.0f,0.0f,9.0f,1.0f,6.0f,1.0f,8.0f);
          m3.qprint();
          m3=inverse(m3);
        
          m3.qprint();
          return 0;
        }


    1. Jijiki
      14.05.2025 12:29

      тоесть вам надо написать векторную математику (vec2-3-4, quaternion или rot, mat2-3-4)), а потом относительно её архитектуры накидывать архитектуру отрисовки и растеризации точек треугольников или позиций картинок, спасибо за минус - у меня кстати работает )


  1. Kelbon
    14.05.2025 12:29

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

    #include <anyany/anyany.hpp>
    
    struct draw_m {
      static void do_invoke(const auto& self) { self.draw(); }
    
      template <typename CRTP>
      struct plugin {
        void draw() const {
          auto& self = *static_cast<const CRTP*>(this);
          aa::invoke<Foo>(self);
        }
      };
    };
    
    struct name_m {
      static std::string do_invoke(const auto& self) { self.name(); }
    };
    
    using vec2d = ...;
    using shape = aa::any_with<draw_m, name_m>;
    
    struct point {
        vec2d pos;
    
        void draw() const;
        std::string name() const;
    };
    
    struct circle {
        vec2d center;
        int r = 0;
    
        void draw() const;
        std::string name() const;
    };
    
    struct rectangle {
        vec2d lefttop;
        vec2d sizes;
    
        void draw() const;
        std::string name() const;
    };
    
    struct canvas {
        std::vector<shape> shapes;
    public:
        void add(shape s) { shapes.push_back(std::move(s)); }
        void render() const {
            for (shape& s : shapes) s.draw();
        }
    };
    
    int main() {
        canvas c;
        c.add(point({1, 1}));
        c.add(circle({5, 5}, 3));
        c.add(rectangle({0, 0}, {6, 3}));
    
        c.render();
        return 0;
    }


  1. BenGunn
    14.05.2025 12:29

    Еще не вечер господа. Давайте лучше поглядим на эти танцы когда требования начнут долполняться/изменяться.