После статьи «ООП не мертво. Вы просто пользуетесь им как молотком по клавиатуре» комментарии кипели
Кто-то звал 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)
skovoroad
14.05.2025 12:29Это очень плохой пример, потому что для игрушечных задач ООП не нужен. Он нужен для сложных абстракций, для разделения когнитивной нагрузки между группами разработчиков. У вас никогда не будет отдельной группы разработчиков для кружочков, а отдельной - для треугольничков.
Давайте попробуем вернуться в реальность и представим, что эти квадратики - что-то более-менее сложное. Очевидно, функции draw() должны обращаться к каким-то связанным с рисованием зависимостям (как минимум, тянуть заголовочные файлы). Но сами прямоугольники могут использоваться в контекстах, которые не подразумевают никакой связи с рисованием. Например, я не знаю, библиотека, которая вычисляет пересечения прямоугольников. На кой ляд ей draw() и все её зависимости?
А значит, фигура у нас разбивается на две части: Rectangle и RectangleDrawer (если предположить, что у рисовальщика есть состояние, например, кэш, и просто функции недостаточно, то понадобится класс), а RectangleDrawer станет частью своей иерархии. Они будут связываться, к примеру, как визиторы, и тут...
И тут в помещение вбегают критики ООП и говорят, что вот наворотили дикий оверинжиниринг! И они правы, потому что для этой примера достаточно POD-структур, для их хранения - несколько векторов, а для рисования - перегруженных (и даже это необязательно) свободных функций. Плохой пример, негодный. Надо придумать пример реального размера, в котором ООП действительно нужно.
Kahelman Автор
14.05.2025 12:29Я с вами не согласен. ООП это подход к разработке. Демонстрация подходов и их применимости делается на простых примерах. Графический редактор это простейший приме, позволяющий выявить плюсы и минусы подходов. В частности, наследование квадрата от прямоугольника -классическая ошибка в ООП дизайне. Здесь вы можете доходчиво объяснить, почему так не следует делать.
Ваша критика похожа на заявление что Ньютоновская механика не подходит, поскольку не работает в масштабах Вселенной.
skovoroad
14.05.2025 12:29Ваш пример не убеждает, а разубеждает пользоваться ООП.
Он не подчёркивает его сильные стороны (разделение кода на изолированные куски с хорошо описанной изолированной ответственностью)
Но зато он отлично иллюстрирует аргументы критиков ООП: вы усложнили примитивную задачу.
(ну и плюс задача решена некорректно, рисование не является частью прямоугольника, как я уже выше и написал)
Kahelman Автор
14.05.2025 12:29Это баттл- я не предлагаю реализацию. Я предлагаю сравнить подходы. Ссылка на репозитарий в конце. Сделайте вашу идеальную реализацию и пришлите MR. Потом можем осудить…
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); }
Kahelman Автор
14.05.2025 12:29Не хватает: треугольника, квадрата, овала, ромба. :)
Chamie
14.05.2025 12:29У вас тоже
К слову, знать бы ещё, что вы под ними подразумеваете, потому что в моей геометрии для рисования прямоугольника на плоскости нужно как минимум 5 чисел (крутит в руке телефон), а «овалов» там вообще гора разных.
Kahelman Автор
14.05.2025 12:29И не плохо бы было их в коллекцию запихнуть чтобы в main все не прописывать.
skovoroad
14.05.2025 12:29зачем в коллекцию? В вашем примере коллекция никак не используется
но если есть какая-то ломовая необходимость в коллекции конкретно в этом микроскопическом примере, у вас есть стандартный std::vector<std::variant>
Понимаете, вы сейчас начнёте накручивать требования, которых нет в примере. Моё замечание было не про ООП (это мощный и полезный подход во множестве случаев), а про ваш пример. Он не иллюстрирует сильные стороны ООП. Поэтому идея соревнования в решении этой задачи в разных парадигмах не имеет смысла.
Kahelman Автор
14.05.2025 12:29Тогда возьмете на себя задачу реализовать на чистом процедурном подходе? std:vectorstd:variant это хак по меньшей мере. Стандартные процедурные языки не используют понятие темплейтов. Поправьте если я не прав. Соответственно в рамках чисто процедурного подхода эта задача просто не решается. Вам надо либо с void указателями работать, либо «изобретать» свой rtti. Что имеет место быть, но как бы добавляет аргументов в сторону сторонников ООП подхода. Со своей стороны могу на AWK реализовать :)
eao197
14.05.2025 12:29Стандартные процедурные языки не используют понятие темплейтов.
Ada-83 как раз таким языком и была.
skovoroad
14.05.2025 12:29Да господи, стандартная библиотека в 2025-м году от РХ у него хак. Как будто кто-то запрещает писать на плюсах в процедурном стиле. Ну возьмите раст, там enum из коробки - тот же вариант. Код ещё в три раза короче станет, кстати.
Вы по-прежнему не туда воюете, я уже сороковой комментарий подряд пытаюсь вам объяснить, что проблема не в ООП, а в вашей статье, в которой выбран неудачный подход демонстрации достоинств ООП. Ладно, это бесполезно.
Comdiv
14.05.2025 12:29что проблема не в ООП, а в вашей статье
Замените слово «статья» на «код» и окажется, что вы описали причину, почему ООП как общая парадигма для написания кода несостоятельна.
skovoroad
14.05.2025 12:29Зачем я буду заменять слово "статья" на слово "код", если я имею а виду не код, а всю статью? Т.е. контекст использования этого кода.
Код, конечно, тоже плохой, но речь не об этом.
Comdiv
14.05.2025 12:29Затем, что статья в данном случае олицетворяет код на ООП, который опять не поняли и неправильно применили. Речь ведь не о коде из статьи.
Kahelman Автор
14.05.2025 12:29Замените слово shape на Message, а Canvas на messageQueue. Получите задачу обработки входящих сообщений. Не ожидал что абстрактное мышление это отдельная фича :)
skovoroad
14.05.2025 12:29Слушайте, если у вас всё надо заменить, чтобы вас правильно понять, может, вы уже признаете, что пример плохой? Задача организации очереди сообщений это другая задача и действительно служила бы лучше целям статьи.
onets
14.05.2025 12:29Хорошо, что выбрали графический редактор - на нем можно показать плюсы ооп, вместо бизнес приложения, где центром являются данные и их согласованность
Kahelman Автор
14.05.2025 12:29За данные и их согласованность должна отвечать БД. Правило это не модно и молодёжно. Правда никто пока не показал как обеспечить гарантированную согласованность данных в распределенной системе. :)
askv
14.05.2025 12:29Потому что таких гарантий не существует? )
Kahelman Автор
14.05.2025 12:29Потому что задача «византийских генералов» не решаема :(
https://ru.m.wikipedia.org/wiki/Задача_византийских_генералов
ermouth
14.05.2025 12:29Редактор – это где редактировать можно. В примере какая-то надстройка для усложнения использования канваса, простите.
Kahelman Автор
14.05.2025 12:29Ага вы готовы за 20-30 минут сделать MVP редактора в вашем понимании?
ermouth
14.05.2025 12:29Зачем? По вашим условиям на любом языке получится примерно одно и то же. По вашим условиям по сути нужно через вызов add с кортежем {тегФигуры, ...параметры} положить его куда-то в стейт приложения, а потом по команде их все нарисовать.
В вашем примере ООП подход ничего вообще не добавляет, ну кроме километра ритуальной по сути писанины.
Kahelman Автор
14.05.2025 12:29Согласен. Идея в том что дальше идут «требования на изменения» и «дополнительные фичи» идея посмотреть как будут развиваться процедурный/функциональный/ООП подходы
eao197
14.05.2025 12:29Как же печально видеть в 2025-ом году C++ный код с ручными new/delete и детскими ошибками, описанными еще в первых изданиях "Язык программирования C++" :(((
Kahelman Автор
14.05.2025 12:29Это минимальная реализация - максимально близко к «классикам». Жду вашего MR с «правильной реализацией». Поскольку тут Баттл- одними комментариями не отделаетесь. Нет MR - слив засчитан :)
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-ownershipKahelman Автор
14.05.2025 12:29Первоначальный вариант был с unique_ptr. Потом выбросил, т.к. вопрос про архитектуру ООП vs XXX а не про современные подходы и best practice в C++.
eao197
14.05.2025 12:29Демонстрация "как в этом вашем C++ отстрелить себе ногу не прилагая никаких усилий" получилась наотлично.
Cfyz
14.05.2025 12:29using
ShapeUptr = std::unique_ptr<Shape>;
Использование алиасов почём зря (например в подобных тривиальных случаях) ухудшает читаемость кода. Не надо так.
shapes.push_back(std::move(s))
Скорее наверное emplace_back()?
Я хз что такое MR
Это довольно иронично, что вы в 2025 не знаете что такое MR =). Подсказка: это как PR, только gitlab.
eao197
14.05.2025 12:29Не надо так.
Спасибо, но вредные советы (а это очень вредный совет) идут в сад. Желающие выписывать раз за разом
std::unique_ptr<что-то-там>
следуют туда же.Скорее наверное emplace_back()?
Когда у нас на входе готовый unique_ptr, то разницы быть не должно.
Cfyz
14.05.2025 12:29Спасибо, но вредные советы (а это очень вредный совет) идут в сад.
Ну так вы первый начали.
Любой алиас это сокрытие конкретного типа и небольшая, но дополнительная когнитивная нагрузка на удержание в памяти что это такое на самом деле и/или постоянные сверки то ли это, что ты думаешь.
Даже дюжина повторений в коде не стоит этого. Вы несколько раз экономите невероятные 10 символов, из которых половину все равно подставит IDE, а читаете это снова и снова и снова каждый раз, когда приходится вернуться к этому фрагменту кода.
Одно дело, если тип фундаментален для проекта и используется в нем повсеместно. Но в единичных и/или простых случаях использование всяких FooPtr, FooMap и прочих одному автору понятных типов только лишь потому что видите ли лень пару лишних клавиш нажать -- это вредная практика.
eao197
14.05.2025 12:29Любой алиас это сокрытие конкретного типа
Нет. Вы сразу ошибаетесь. А из ошибочных предпосылок происходят и ошибочные выводы.
Все, что вы написали -- это ерунда. Особенно про "видите ли лень пару лишних клавиш нажать -- это вредная практика".
Более того, когда в проекте есть устоявшиеся правила именования указателей (вроде ShapeUptr, ShapeShptr), то уже из названия типа видно, с каким указателем мы имеем дело и это отнимает гораздо меньше усилий, чем читать бесконечные std::unique_ptr. Особенно в сочетаниях типа
std::shared_ptr<std::vector<std::unique_ptr<Shape>>>
.Ну и главное, хорошие привычки нужно вырабатывать сразу, на простых ученических задачках.
ЗЫ. Минус на вашем комментарии не от меня. Я вообще на Хабре минусы не расставляю (за очень-очень-очень редким исключением).
Cfyz
14.05.2025 12:29Нет. Вы сразу ошибаетесь. Все, что вы написали -- это ерунда.
Вы меня конечно извините, но это аргументация уровня "нет ты".
когда в проекте есть устоявшиеся правила именования указателей (вроде ShapeUptr, ShapeShptr), то уже из названия типа видно
Это можно отнести к упомянутому мной частному случаю, когда типы настолько общеупотребимы в проекте, что без их знания все равно никуда. Кроме того, если в проекте исторически сложилось использование таких сокращений, то бессмысленно спорить. Даже если используется какая-нибудь венгерская нотация, все равно придется писать как уже написано.
Закавыка в том, что все равно у каждого второго проекта будут свои собственные устоявшиеся правила. Вам может показаться что ShapreUptr это что-то совершенно очевидное, но это не так.
Особенно в сочетаниях типа
std::shared_ptr<std::vector<std::unique_ptr<Shape>>>
По-моему это больше похоже на контр-пример.
Пользоваться такими типами очень тяжело и легко приводит к ошибкам, с алиасом или без. Если в коде постоянно нужно использовать и передавать что-то подобное, то вместо тяп-ляп спрятать имя под алиасом, по-хорошему надо задуматься о выделении соответствующей сущности в отдельный тип.
Потому что постоянное жонглирование вложенностью-разыменованием и безымянными полями абстрактных контейнеров это форменная жесть с какой стороны ни посмотри.
Имя, которое невыносимо печатать из-за его длины -- это чаще всего признак проблемы, которую надо решать не алиасом.
Ну и главное, хорошие привычки нужно вырабатывать сразу, на простых ученических задачках.
Как например использование emplace_back() вместо push_back() даже если в данном конкретном случае компилятор оптимизирует лишний move в ноль? =)
Но суть моей придирки к алиасам в том, что вы фактически производите небольшую обфускацию и называете это хорошей привычкой.
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 -- ну такое себе, "сомнительно, но окай" (с)
Cfyz
14.05.2025 12:29Я насмотрелся на программистов, которые не используют using-и.
Ну а я насмотрелся на тех, которые пихают typedef и using где надо и не надо.
Явное всегда лучше неявного.
Если у вас программисты пишут плохой код потому что они не используют using почем зря -- дело совершенно точно не в using.
Для emplace_back есть очевидный сценарий применения <...> Для добавления же в конец вектора готового объекта предназначен push_back.
Вот тут вынужден согласиться, в данном случае почем зря emplace приплел.
skovoroad
14.05.2025 12:29Казалось бы, наоборот? Если я знаю, что FooPtr это некий уместный в данном контексте тип указателя, то я не думаю о нижележащем типе и когнитивная нагрузка падает?
Мне неважно, что он юник или шейред, мне важно, что он указывает на Foo и используется в заданной сигнатуре. Остаётся только нужная информация. Нагрузка падает.
Cfyz
14.05.2025 12:29Мне неважно, что он юник или шейред
Нюанс в том, что когда действительно неважно что это за тип, то как правило это будет typename T или auto =).
А вот что автор скрыл под Ptr -- unique, shared, QPointer, указатель из boost или вообще голый указатель -- это обычно существенно влияет на семантику владения, совместимость между типами и как с этим можно обращаться вообще помимо -> и *.
Да, внутри функции вам действительно часто все равно, что это за указатель. Ну так в названии переменной или поля тип и не указывается, shape и shape.
skovoroad
14.05.2025 12:29Семантика владения и прочие детали важны при написании кода (и то в большинстве случаев компилятор не даст ошибиться).
Но код читается кратно больше, чем пишется. И когнитивная нагрузка - это про чтение, а не про написание. Здесь алиасы работают, как хорошее именование переменных и даже как конструирование типов - упрощает код.
Dooez
14.05.2025 12:29auto
также скрывает тип, но повсеместен в современном C++. Хотя может вы из секты людей, которые всегда явно пишет тип переменных?Хороший алиас позволяет в первую очередь меньше читать. Если он ограничен областью видимости, то когнитивная нагрузка вполне вероятно будет меньше чем при его отсутствии.
Алиасы значительно упрощают рефакторинг и позволяют избежать багов. Обобщенный код без алиасов я даже представлять не хочу.
Если говорить про границы API то конечно лучше иметь сигнатуры со стандартными именами. Но в реализациях функций и классов не вижу проблемы при грамотно выбранных именах.
Cfyz
14.05.2025 12:29auto
также скрывает тип, но повсеместен в современном C++. Хотя может вы из секты людей, которые всегда явно пишет тип переменных?Фундаментальное отличие auto в том, что он скрывает тип в очень небольшом контексте. Грубо говоря вот видим объявление auto i = vec.begin(), и вот в пределах экрана эта переменная используется.
То же самое с локальным using в пределах области видимости.
Проблема с теми алиасами, которые определены где-то в совершенно другом месте и надо вспоминать/искать что это такое. IDE еще могут быстро подсказать, но при чтении/ревью например в web-интерфейсе это очень мешает.
Заметьте, что я не агитирую за полный отказ от using вообще. Только там, где без этого можно легко обойтись.
Алиасы значительно упрощают рефакторинг и позволяют избежать багов. Обобщенный код без алиасов я даже представлять не хочу.
Обобщенный алиас алиасу рознь. Одно дело using ElementType = T; в контейнере. И совсем другое это сокрытие семантики, как это часто происходит с указателями или многоэтажными контейнерами. Бездумное использование алиасов запросто может наоборот стать причиной неочевидной ошибки.
Но в реализациях функций и классов не вижу проблемы при грамотно выбранных именах.
Вот казалось бы, std::unique_ptr<Shape> -- куда уж лучше. Указатель, уникальный, из стандартной библиотеки. Но нет, это слишком просто.
simplepersonru
14.05.2025 12:29Сделали в проекте такие глобальные юзинги (в условном common/core.h) :
template <class T> using U = std::unique_ptr<T>;
И также SH с шаредптр
Читаемость не теряется и не нужно руками делать на каждый такой класс отдельный юзинг.
Плюс такого подхода, что мы всегда видим оригинальный класс и например поиск по символу нужно делать не для каждого такого юзинга, а только для оригинального класса
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 все равно остается на месте.
Cfyz
14.05.2025 12:29Претензия не к ООП или архитектуре.
Претензия к использованию устаревших и не идиоматических конструкций. Как будто вы не очень хорошо знаете C++, но тогда возникает вопрос зачем вы пишете пример именно на нем?
Kahelman Автор
14.05.2025 12:29Я бы был менее категоричен: во-первых: «не очень хорошо знаете современный С++»,
Во-вторых: в предыдущей статье я ссылался на книгу Гради Буча, которая вышла некоторое время назад. Примеры там написаны на C++/Java без использования новейших фич языков и даже без темплейтов. Так что я старался держаться одного стиля с оригиналом.
MonkeyWatchingYou
14.05.2025 12:29Чтоб повысить заход на эту статью надо было добавить теги #c, #assembler и #xml.
И непонятно зачем и холиварно.
Шутка.
nin-jin
14.05.2025 12:29ermouth
14.05.2025 12:29Ширина/высота прямоугольника и радиус – по-хорошему uint. Ширина и высота это к слову не dx, dy – которые и правда могли бы быть int. То, что у автора везде int – скорее проблемы автора.
nin-jin
14.05.2025 12:29А это и не ширина и высота. Поправил нейминг. Ну и тип радиуса тоже, спасибо.
Kahelman Автор
14.05.2025 12:29Как в анекдоте: «я знал что дискуссия будет только по последнему вопросы ..»
Cfyz
14.05.2025 12:29Следующая итерация уже в пути. Требования изменятся. Канвас расширится. Архитектура проявит себя.
Не хватает: треугольника, квадрата, овала, ромба. :)
Спойлер: в итоге не будет никаких Circle, Rectangle и прочих треугольников, останется только Shape с набором кривых, описывающих контур фигуры.
Kahelman Автор
14.05.2025 12:29Все гораздо проще. Есть план «развития» продукта. Что вполне вписывается в «тестовую» архитектуру. Задача посмотреть как разные подходы будут себя вести. Опять-таки, задача сохранить „time to market“. Ниже привели пример, который вроде как позволит все безгранично расширять. Но как MVP это в любом случае оверинжиниринг.
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(); }
Есть к нему какие-нибудь претензии?
ermouth
14.05.2025 12:29не будет никаких Circle
Окружность не представима кривыми Безье. Так что может и будут, аппроксимация не всегда приемлема.
Cfyz
14.05.2025 12:29Ну значит возьмем B-сплайны. Какими кривыми аппроксимировать это детали реализации. В первом приближении можно вообще набором прямых обойтись.
ermouth
14.05.2025 12:29возьмем B-сплайны
Окружность точно не представима никакими сплайнами, хотя аппроксимируют почти всегда через них. Точно берут там, где важна гомогенность пространства, это типа чтобы когда вы вал в отверстии виртуально поворачиваете, его не заклинивало на узлах соотв производных из-за того, что мы аппроксимацию взяли вместо окружности.
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
во как бы я сделал, но в идеале точки бы были из библиотеки матеши - это будет влиять на архитектуру кстати
Kahelman Автор
14.05.2025 12:29На мой неискушенный взгляд вы тут слегка огород нагородили. Вместо понятного синтаксиса: тип: переменная вы все закинули в массив с неясной структурой. Далее, у вас все элементы массива оказались одного и того же типа. Что может быть нежелательным ограничением.
std::println("Drawing {} at {} {} {} {}",name(),p[0],p[1],p[2],p[3]);
}Я бы не хотел такой код поддерживать, когда автор давно работу поменял, а мне понять надо что там и где используется.
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; }
Jijiki
14.05.2025 12:29тоесть вам надо написать векторную математику (vec2-3-4, quaternion или rot, mat2-3-4)), а потом относительно её архитектуры накидывать архитектуру отрисовки и растеризации точек треугольников или позиций картинок, спасибо за минус - у меня кстати работает )
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; }
BenGunn
14.05.2025 12:29Еще не вечер господа. Давайте лучше поглядим на эти танцы когда требования начнут долполняться/изменяться.
withkittens
Горшочек, не вари.