Комментарии 124
Горшочек, не вари.
Это очень плохой пример, потому что для игрушечных задач ООП не нужен. Он нужен для сложных абстракций, для разделения когнитивной нагрузки между группами разработчиков. У вас никогда не будет отдельной группы разработчиков для кружочков, а отдельной - для треугольничков.
Давайте попробуем вернуться в реальность и представим, что эти квадратики - что-то более-менее сложное. Очевидно, функции draw() должны обращаться к каким-то связанным с рисованием зависимостям (как минимум, тянуть заголовочные файлы). Но сами прямоугольники могут использоваться в контекстах, которые не подразумевают никакой связи с рисованием. Например, я не знаю, библиотека, которая вычисляет пересечения прямоугольников. На кой ляд ей draw() и все её зависимости?
А значит, фигура у нас разбивается на две части: Rectangle и RectangleDrawer (если предположить, что у рисовальщика есть состояние, например, кэш, и просто функции недостаточно, то понадобится класс), а RectangleDrawer станет частью своей иерархии. Они будут связываться, к примеру, как визиторы, и тут...
И тут в помещение вбегают критики ООП и говорят, что вот наворотили дикий оверинжиниринг! И они правы, потому что для этой примера достаточно POD-структур, для их хранения - несколько векторов, а для рисования - перегруженных (и даже это необязательно) свободных функций. Плохой пример, негодный. Надо придумать пример реального размера, в котором ООП действительно нужно.
Я с вами не согласен. ООП это подход к разработке. Демонстрация подходов и их применимости делается на простых примерах. Графический редактор это простейший приме, позволяющий выявить плюсы и минусы подходов. В частности, наследование квадрата от прямоугольника -классическая ошибка в ООП дизайне. Здесь вы можете доходчиво объяснить, почему так не следует делать.
Ваша критика похожа на заявление что Ньютоновская механика не подходит, поскольку не работает в масштабах Вселенной.
Ваш пример не убеждает, а разубеждает пользоваться ООП.
Он не подчёркивает его сильные стороны (разделение кода на изолированные куски с хорошо описанной изолированной ответственностью)
Но зато он отлично иллюстрирует аргументы критиков ООП: вы усложнили примитивную задачу.
(ну и плюс задача решена некорректно, рисование не является частью прямоугольника, как я уже выше и написал)
Это баттл- я не предлагаю реализацию. Я предлагаю сравнить подходы. Ссылка на репозитарий в конце. Сделайте вашу идеальную реализацию и пришлите MR. Потом можем осудить…
Да тут даже мр не нужен, вот вам код, выполняющий совершенно ту же задачу, что и ваш пример.
По сравнению с вашим у него масса достоинств, начиная с того, что он компилируется.
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);
}
Не хватает: треугольника, квадрата, овала, ромба. :)
И не плохо бы было их в коллекцию запихнуть чтобы в main все не прописывать.
зачем в коллекцию? В вашем примере коллекция никак не используется
но если есть какая-то ломовая необходимость в коллекции конкретно в этом микроскопическом примере, у вас есть стандартный std::vector<std::variant>
Понимаете, вы сейчас начнёте накручивать требования, которых нет в примере. Моё замечание было не про ООП (это мощный и полезный подход во множестве случаев), а про ваш пример. Он не иллюстрирует сильные стороны ООП. Поэтому идея соревнования в решении этой задачи в разных парадигмах не имеет смысла.
Тогда возьмете на себя задачу реализовать на чистом процедурном подходе? std:vectorstd:variant это хак по меньшей мере. Стандартные процедурные языки не используют понятие темплейтов. Поправьте если я не прав. Соответственно в рамках чисто процедурного подхода эта задача просто не решается. Вам надо либо с void указателями работать, либо «изобретать» свой rtti. Что имеет место быть, но как бы добавляет аргументов в сторону сторонников ООП подхода. Со своей стороны могу на AWK реализовать :)
Стандартные процедурные языки не используют понятие темплейтов.
Ada-83 как раз таким языком и была.
Да господи, стандартная библиотека в 2025-м году от РХ у него хак. Как будто кто-то запрещает писать на плюсах в процедурном стиле. Ну возьмите раст, там enum из коробки - тот же вариант. Код ещё в три раза короче станет, кстати.
Вы по-прежнему не туда воюете, я уже сороковой комментарий подряд пытаюсь вам объяснить, что проблема не в ООП, а в вашей статье, в которой выбран неудачный подход демонстрации достоинств ООП. Ладно, это бесполезно.
что проблема не в ООП, а в вашей статье
Замените слово «статья» на «код» и окажется, что вы описали причину, почему ООП как общая парадигма для написания кода несостоятельна.
Зачем я буду заменять слово "статья" на слово "код", если я имею а виду не код, а всю статью? Т.е. контекст использования этого кода.
Код, конечно, тоже плохой, но речь не об этом.
Замените слово shape на Message, а Canvas на messageQueue. Получите задачу обработки входящих сообщений. Не ожидал что абстрактное мышление это отдельная фича :)
Слушайте, если у вас всё надо заменить, чтобы вас правильно понять, может, вы уже признаете, что пример плохой? Задача организации очереди сообщений это другая задача и действительно служила бы лучше целям статьи.
Организация очереди сообщений на ООП? — Это новое слово в разработке.
Вот пример очереди на ООП. Но новое слово тут не в том, что ООП, а в том, что wait-free.
И правда, при чём тут ООП, если всё сделано на объектах.. Наверно это ФП такой.
Тест с 1000 потоками уже давно есть. А нежданчик не случится благодаря барьерам памяти.
Вы считываете значение и обновляете его за двумя разными барьерами.
А ООП ни при чём, потому что не используется. Замените объекты на структуры — и ничего не изменится.
Это новое слово в разработке.
Как пример для статьи про ООП на хабре? Почему, собственно, нет? Речь не идёт про промышленную универсальную реализацию, вы даже не знаете, какие требования будут к такой очереди, но уже лезете в бутылку. Вполне можно навертеть ООП-абстракций для примера, начиная от иерархии самих типов сообщений и заканчивая какими-нибудь, я не знаю, подписчиками. Это иллюстративный материал для статьи, он не обязан летать на утюгах.
Ах да, я вспомнил, вы тот самый любитель общественного внимания, который обращается к собеседнику "тупой дегенерат", когда его самого макают в, назовём это так, чрезмерную широту обобщений на фоне ограниченнго кругозора.
Тогда вопросов не имею.
Графический редактор это простейший пример, позволяющий выявить плюсы и минусы подходов.
Смешно. На КОБОЛе написаны тонны кода, который работает десятилетиями, и у него — прикиньте — нет графических/десктопных примитивов.
При это КОБОЛ на момент создания был гигантским прыжком вперед, почти революцией. А вот десктопов не было, упс.
Кроме того, никому в здравом уме не придёт в голову писать графический редактор без использования библиотек в 2025 году. А еще на джаваскрипте это будет короче, понятнее, и быстрее. Упс².
О написании графического редактора без библиотек и вообще о написании графического редактора речи не идёт. Речь о классическом примере реализации приложения на на ООП. Можно было бы последовать примеру товарища Буча и взять его вариант с Гидропонной схемой. Но тут у народ в 5 примитивах разбираться не может и ТЗ из трех строчек до конца прочитать. Вы кстати на Коболе писать собираетесь? Причём тут что на нем тонны кода написаны? На bash написано больше чем не COBOL, и продолжают писать и что?
Я не собираюсь писать вообще, я свободное время трачу на OSS, а не на взнуздание сферических коней в вакууме. Но если бы собирался, написал бы на насквозь функциональном эликсире.
Реализация полиморфизма, который здесь нужен, в ООП — самая кривая.
для игрушечных задач ООП не нужен. Он нужен для сложных абстракций, для разделения когнитивной нагрузки между группами разработчиков
И для этого он тоже не нужен.
Снизить межкомандную когнитивную нагрузку можно, сведя общий API к компактному и редко меняющемуся ядру, а частные вариации вынеся в простые адаптеры.
А мы пишем класс в ООП потому что ожидаем что его будет отдельная команда разрабатывать? Эвона чо.
Мы пишем класс (и прочими способами упрощаем поддержку кода), потому что писатель кода и читатель могут различаться, да (это может быть и один человек, но в разное время). Это действительно причина
Код, в котором никому никогда не надо будет разбираться и поддерживать, всё равно, в какой парадигме писать
Эвона чо
для игрушечных задач ООП не нужен
В точку. А для неигрушечных — он не работает, к сожалению.
Однопоточные request response комфортно живут в ООП
Игрушечные? Ну, может...
У вас linux kernel и android перестали работать?)
Linux kernel на чистом Си же.
Это не мешает там использовать ООП.
https://lwn.net/Articles/444910/
https://matheustavares.dev/assets/oop_git_and_kernel.pdf
И он не на чистом С, а на GNU расширениях.
Хорошо, что выбрали графический редактор - на нем можно показать плюсы ооп, вместо бизнес приложения, где центром являются данные и их согласованность
За данные и их согласованность должна отвечать БД. Правило это не модно и молодёжно. Правда никто пока не показал как обеспечить гарантированную согласованность данных в распределенной системе. :)
Потому что таких гарантий не существует? )
Потому что задача «византийских генералов» не решаема :(
https://ru.m.wikipedia.org/wiki/Задача_византийских_генералов
Редактор – это где редактировать можно. В примере какая-то надстройка для усложнения использования канваса, простите.
Ага вы готовы за 20-30 минут сделать MVP редактора в вашем понимании?
Зачем? По вашим условиям на любом языке получится примерно одно и то же. По вашим условиям по сути нужно через вызов add с кортежем {тегФигуры, ...параметры} положить его куда-то в стейт приложения, а потом по команде их все нарисовать.
В вашем примере ООП подход ничего вообще не добавляет, ну кроме километра ритуальной по сути писанины.
Как же печально видеть в 2025-ом году C++ный код с ручными new/delete и детскими ошибками, описанными еще в первых изданиях "Язык программирования C++" :(((
Это минимальная реализация - максимально близко к «классикам». Жду вашего MR с «правильной реализацией». Поскольку тут Баттл- одними комментариями не отделаетесь. Нет MR - слив засчитан :)
Я хз что такое 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
Первоначальный вариант был с unique_ptr. Потом выбросил, т.к. вопрос про архитектуру ООП vs XXX а не про современные подходы и best practice в C++.
MR - merge request
using
ShapeUptr = std::unique_ptr<Shape>;
Использование алиасов почём зря (например в подобных тривиальных случаях) ухудшает читаемость кода. Не надо так.
shapes.push_back(std::move(s))
Скорее наверное emplace_back()?
Я хз что такое MR
Это довольно иронично, что вы в 2025 не знаете что такое MR =). Подсказка: это как PR, только gitlab.
Не надо так.
Спасибо, но вредные советы (а это очень вредный совет) идут в сад. Желающие выписывать раз за разом std::unique_ptr<что-то-там>
следуют туда же.
Скорее наверное emplace_back()?
Когда у нас на входе готовый unique_ptr, то разницы быть не должно.
Спасибо, но вредные советы (а это очень вредный совет) идут в сад.
Ну так вы первый начали.
Любой алиас это сокрытие конкретного типа и небольшая, но дополнительная когнитивная нагрузка на удержание в памяти что это такое на самом деле и/или постоянные сверки то ли это, что ты думаешь.
Даже дюжина повторений в коде не стоит этого. Вы несколько раз экономите невероятные 10 символов, из которых половину все равно подставит IDE, а читаете это снова и снова и снова каждый раз, когда приходится вернуться к этому фрагменту кода.
Одно дело, если тип фундаментален для проекта и используется в нем повсеместно. Но в единичных и/или простых случаях использование всяких FooPtr, FooMap и прочих одному автору понятных типов только лишь потому что видите ли лень пару лишних клавиш нажать -- это вредная практика.
Любой алиас это сокрытие конкретного типа
Нет. Вы сразу ошибаетесь. А из ошибочных предпосылок происходят и ошибочные выводы.
Все, что вы написали -- это ерунда. Особенно про "видите ли лень пару лишних клавиш нажать -- это вредная практика".
Более того, когда в проекте есть устоявшиеся правила именования указателей (вроде ShapeUptr, ShapeShptr), то уже из названия типа видно, с каким указателем мы имеем дело и это отнимает гораздо меньше усилий, чем читать бесконечные std::unique_ptr. Особенно в сочетаниях типа std::shared_ptr<std::vector<std::unique_ptr<Shape>>>
.
Ну и главное, хорошие привычки нужно вырабатывать сразу, на простых ученических задачках.
ЗЫ. Минус на вашем комментарии не от меня. Я вообще на Хабре минусы не расставляю (за очень-очень-очень редким исключением).
Нет. Вы сразу ошибаетесь. Все, что вы написали -- это ерунда.
Вы меня конечно извините, но это аргументация уровня "нет ты".
когда в проекте есть устоявшиеся правила именования указателей (вроде ShapeUptr, ShapeShptr), то уже из названия типа видно
Это можно отнести к упомянутому мной частному случаю, когда типы настолько общеупотребимы в проекте, что без их знания все равно никуда. Кроме того, если в проекте исторически сложилось использование таких сокращений, то бессмысленно спорить. Даже если используется какая-нибудь венгерская нотация, все равно придется писать как уже написано.
Закавыка в том, что все равно у каждого второго проекта будут свои собственные устоявшиеся правила. Вам может показаться что ShapreUptr это что-то совершенно очевидное, но это не так.
Особенно в сочетаниях типа
std::shared_ptr<std::vector<std::unique_ptr<Shape>>>
По-моему это больше похоже на контр-пример.
Пользоваться такими типами очень тяжело и легко приводит к ошибкам, с алиасом или без. Если в коде постоянно нужно использовать и передавать что-то подобное, то вместо тяп-ляп спрятать имя под алиасом, по-хорошему надо задуматься о выделении соответствующей сущности в отдельный тип.
Потому что постоянное жонглирование вложенностью-разыменованием и безымянными полями абстрактных контейнеров это форменная жесть с какой стороны ни посмотри.
Имя, которое невыносимо печатать из-за его длины -- это чаще всего признак проблемы, которую надо решать не алиасом.
Ну и главное, хорошие привычки нужно вырабатывать сразу, на простых ученических задачках.
Как например использование emplace_back() вместо push_back() даже если в данном конкретном случае компилятор оптимизирует лишний move в ноль? =)
Но суть моей придирки к алиасам в том, что вы фактически производите небольшую обфускацию и называете это хорошей привычкой.
Вы меня конечно извините, но это аргументация уровня "нет ты".
Так если вы наговорили ерунды, то единственное, что можно сказать -- это назвать ерунду ерундой.
Я насмотрелся на программистов, которые не используют using-и. И еще больше насмотрелся на результаты их работы. Так, что больше не хочется. После этого любой персонаж, который мне начинает рассказывать про то, что "любой алиас -- это сокрытие типа", просто расписывается в своей профнепригодности. Пардон май френч.
Как например использование emplace_back() вместо push_back()
Для emplace_back есть очевидный сценарий применения -- это когда у нас на руках есть набор аргументов для конструирования нового объекта в векторе. Типа такого:
std::vector<std::string> lines;
lines.emplace_back(45uz, '\t');
Для добавления же в конец вектора готового объекта предназначен push_back.
Все просто и очевидно. Использовать emplace_back как замену push_back -- ну такое себе, "сомнительно, но окай" (с)
Я насмотрелся на программистов, которые не используют using-и.
Ну а я насмотрелся на тех, которые пихают typedef и using где надо и не надо.
Явное всегда лучше неявного.
Если у вас программисты пишут плохой код потому что они не используют using почем зря -- дело совершенно точно не в using.
Для emplace_back есть очевидный сценарий применения <...> Для добавления же в конец вектора готового объекта предназначен push_back.
Вот тут вынужден согласиться, в данном случае почем зря emplace приплел.
Ну а я насмотрелся на тех, которые пихают typedef и using где надо и не надо.
Примеры можно? Что-то мне сложно представить, как using-ами можно код испортить.
Может из OpenSource что-нибудь?
Если у вас программисты пишут плохой код потому что они не используют using почем зря -- дело совершенно точно не в using.
В том-то и дело, что не у меня. Почитаешь профильные ресурсы, все профи просто высшего разряда. Как придешь какой-нибудь проект консультировать, так просто в шоке -- где все те монстры от программирования, которые себя пяткой в грудь в комментариях бьют. И код корявый получается не потому, что тупые неумехи его пишут, а потому, что просто вовремя не научили, как можно себе жизнь облегчить, а код -- упростить.
Примеры можно? Что-то мне сложно представить, как using-ами можно код испортить.
А примеры как испортили код отсутствием using там, где в этом нет явной на то необходимости? Сотый раз говорю, у алиасов конечно же есть применение. Иногда допустипо или даже надо их применять. Просто не надо пихать их там, где они лишь обфусцируют написанное.
Вообще вся дискуссия выглядит довольно сюрно:
std::unique_ptr<Shape> -- это ужас, прямо-таки признак профнепригодности. А вот ShapeUniquePtr -- это совсем другое дело!
Остается переименовать int в Signed, std::string в String и т. п. и вот тогда все станет просто и понятно, ведь
именованные типы (даже в виде алиасов) служат как дополнительный слой абстракции и скрывают лишние детали
Можно еще using namespace std полирнуть, потому что
уже из названия типа видно, с каким
указателемтипом мы имеем дело и это отнимает гораздо меньше усилий, чем читать бесконечные std::
Да-да, reductio ad absurdum, но я уже не знаю как еще реагировать.
и главное, если со временем потребуется заменить тип за ShapeUptr на какой-то другой, то имя ShapeUptr все равно остается на месте
Или вот это. Давайте просто втихаря подменим реализацию.
Это кстати прекрасная иллюстрация одной из причин большей когнитивной нагрузки от излишнего применения алиасов: всегда надо быть начеку. Даже если ты читал этот код вчера и запомнил что есть что, сегодня кто-нибудь мог все поменять.
std::shared_ptr<std::vector<std::unique_ptr<Shape>>>
Или вот это. Давайте просто спрячем эту жесть под коврик и сделаем вид что так и надо.
А что пользоваться этим невозможно, так кому какое дело.
Что любопытно, действительно можно вообразить такие ситуации, когда ваши аргументы будут оправданы. Стандартная библиотека вон полна алиасов, например. Но какое это имеет отношение к 95% тривиальнейших случаев по типу std::unique_ptr<Shape> в примере выше?
Что, конечно же, вас ничуточку не переубедит. Поэтому здесь мои полномочия видимо все, окончены =(.
А примеры как испортили код отсутствием using там, где в этом нет явной на то необходимости?
Легко. Во-первых, уже был пример с std::shared_ptr<std::vector<std::unique_ptr<Shape>>>
. Вместо которого был бы ShapeContainerShptr
.
Во-вторых, давайте исходный пример чуть расширим и добавим в Canvas методы extract, insert и replace. Без алиаса получим (для простоты не расставлял [[nodiscard]]
):
class Canvas {
std::vector<std::unique_ptr<Shape>> _shapes;
...
public:
void add(std::unique_ptr<Shape> s);
std::unique_ptr<Shape> extract(std::size_t index);
std::unique_ptr<Shape> replace(std::size_t index, std::unique_ptr<Shape> s);
void insert(std::size_t index, std::unique_ptr<Shape> s);
...
};
и тоже самое с алисом:
class Canvas {
std::vector<ShapeUptr> _shapes;
...
public:
void add(ShapeUptr s);
ShapeUptr extract(std::size_t index);
ShapeUptr replace(std::size_t index, ShapeUptr s);
void insert(std::size_t index, ShapeUptr s);
...
};
Любой желающий может сам оценить какой из вариантов больше замусоривает смысл более низкоуровневыми деталями реализации. И какой более приспособлен к будущим изменениям. Например, если нам потребуется сменить обычный unique_ptr на unique_ptr с кастомным делетером.
Теперь ваши примеры ситуаций, когда алиасы мешают.
Остается переименовать int в Signed
Вам смешно, а я вот сейчас работаю с проектом, в котором для индексации задействовали int-ы, а не какой-либо из вариантов strong typedef. Поменять это задешево уже не получается и приходится разгребаться с предупреждениями о неявных конвертациях size_t в int, а иногда и double в int (промежуточно через size_t). Был бы изначально введен некий ItemIndex, пусть даже в виде простого using-а, сейчас стало бы гораздо проще.
А что пользоваться этим невозможно, так кому какое дело.
Интересно почему этим невозможно пользоваться?
Что, конечно же, вас ничуточку не переубедит
Есть немаленькая вероятность, что я программирую дольше, чем вы живете на свете. И есть еще большая вероятность, что говнокода пришлось разгрести тоже побольше. Так что да, не убеждают. А вот ощущение, что вы сперва сказали ерунду, а потом ее старательно защищаете, только усиливается.
Кстати говоря, профнепригодность относилась к совету не использовать алиасы в простых случаях. А не то, что вы написали выше.
Казалось бы, наоборот? Если я знаю, что FooPtr это некий уместный в данном контексте тип указателя, то я не думаю о нижележащем типе и когнитивная нагрузка падает?
Мне неважно, что он юник или шейред, мне важно, что он указывает на Foo и используется в заданной сигнатуре. Остаётся только нужная информация. Нагрузка падает.
Мне неважно, что он юник или шейред
Нюанс в том, что когда действительно неважно что это за тип, то как правило это будет typename T или auto =).
А вот что автор скрыл под Ptr -- unique, shared, QPointer, указатель из boost или вообще голый указатель -- это обычно существенно влияет на семантику владения, совместимость между типами и как с этим можно обращаться вообще помимо -> и *.
Да, внутри функции вам действительно часто все равно, что это за указатель. Ну так в названии переменной или поля тип и не указывается, shape и shape.
Семантика владения и прочие детали важны при написании кода (и то в большинстве случаев компилятор не даст ошибиться).
Но код читается кратно больше, чем пишется. И когнитивная нагрузка - это про чтение, а не про написание. Здесь алиасы работают, как хорошее именование переменных и даже как конструирование типов - упрощает код.
Но код читается кратно больше, чем пишется. И когнитивная нагрузка - это про чтение, а не про написание.
Именно. И поэтому аргумент мол не хочется выписывать раз за разом полное имя -- плохой.
Здесь алиасы работают, как хорошее именование переменных
Именно. И поэтому сокращение имен или использование нестандартных наименований без существенной на то причины -- усложняет чтение.
Семантика владения и прочие детали важны при написании кода
И при попытке понять что происходит в данном фрагменте кода. Особенно когда неочевидно это оно специально так написано или случайно получилось и возможно тут ошибка.
Но опять-таки, не все алиасы плохие. Дискуссия началась с использования using ShapeUptr = std::unique_ptr<Shape>. Это, положа руку на ногу, просто бесполезная перестановка символов местами, которая выигрывает несколько символов ценой замены явного, всем и каждому понятного типа на локальную историю, которую надо будет найти и запомнить (и сразу же забыть, потому что в каждом случае она разная). Зачем? Просто потому что.
Это, положа руку на ногу, просто бесполезная перестановка символов местами, которая выигрывает несколько символов
Блин, еще раз повторяю: экономия символов здесь не при чем от слова совсем.
Зачем?
Затем, что именованные типы (даже в виде алиасов) служат как дополнительный слой абстракции и скрывают лишние детали. Когда вам потребуется узнать что за ShapeUptr -- вы посмотрите. А пока это не потребовалось, то лучше использовать одно имя вместо std::unique_ptr.
Просто потому что.
Если вы чего-то не понимаете, то это не значит, что в этом нет смысла. Аргументов вам здесь уже привели в ассортименте. Причем разные люди.
auto
также скрывает тип, но повсеместен в современном C++. Хотя может вы из секты людей, которые всегда явно пишет тип переменных?
Хороший алиас позволяет в первую очередь меньше читать. Если он ограничен областью видимости, то когнитивная нагрузка вполне вероятно будет меньше чем при его отсутствии.
Алиасы значительно упрощают рефакторинг и позволяют избежать багов. Обобщенный код без алиасов я даже представлять не хочу.
Если говорить про границы API то конечно лучше иметь сигнатуры со стандартными именами. Но в реализациях функций и классов не вижу проблемы при грамотно выбранных именах.
auto
также скрывает тип, но повсеместен в современном C++. Хотя может вы из секты людей, которые всегда явно пишет тип переменных?
Фундаментальное отличие auto в том, что он скрывает тип в очень небольшом контексте. Грубо говоря вот видим объявление auto i = vec.begin(), и вот в пределах экрана эта переменная используется.
То же самое с локальным using в пределах области видимости.
Проблема с теми алиасами, которые определены где-то в совершенно другом месте и надо вспоминать/искать что это такое. IDE еще могут быстро подсказать, но при чтении/ревью например в web-интерфейсе это очень мешает.
Заметьте, что я не агитирую за полный отказ от using вообще. Только там, где без этого можно легко обойтись.
Алиасы значительно упрощают рефакторинг и позволяют избежать багов. Обобщенный код без алиасов я даже представлять не хочу.
Обобщенный алиас алиасу рознь. Одно дело using ElementType = T; в контейнере. И совсем другое это сокрытие семантики, как это часто происходит с указателями или многоэтажными контейнерами. Бездумное использование алиасов запросто может наоборот стать причиной неочевидной ошибки.
Но в реализациях функций и классов не вижу проблемы при грамотно выбранных именах.
Вот казалось бы, std::unique_ptr<Shape> -- куда уж лучше. Указатель, уникальный, из стандартной библиотеки. Но нет, это слишком просто.
Сделали в проекте такие глобальные юзинги (в условном common/core.h) :
template <class T>
using U = std::unique_ptr<T>;
И также SH с шаредптр
Читаемость не теряется и не нужно руками делать на каждый такой класс отдельный юзинг.
Плюс такого подхода, что мы всегда видим оригинальный класс и например поиск по символу нужно делать не для каждого такого юзинга, а только для оригинального класса
Минусы такого подхода:
однобуквенные, но значимые идентификаторы. Запоминать чем отличается
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 все равно остается на месте.
Однобуквенность. Вопрос привычки, такая практика. Их всего 2 таких алиаса, как самые часто-используемые, понятно что если бы их было 10, это уже совсем другой разговор, но эти даже интуитивно понятные
В примере с ShapeUptr угловых скобок было бы на одну пару меньше, они не то чтобы куда-то испарятся все. Но вместе с тем появился и дополнительный символ, его нужно написать. Не сделать forward decl с оригинальным классом, чтобы использовать этот юзинг (т.к. он как правило рядом с определением класса), а в моем примере можно в U<MyClass> пихать такую декларацию при определённых условиях. Ну и то что говорил про поиск по символу
Про замену типа указателя. Для меня выглядит как исключительное событие и думаю писал полный тип хитрого указателя по месту, чтобы не вводить в заблуждение пользователя этого символа на предмет что под капотом. Вообще пример странный, не могу представить себе такое.
Приведу шутошную аналогию, это не аргумент и не всерьез:
Давайте в проекте определим и будем использовать
using Int = int;
Мало ли нам понадобится массово подменить целые числа, а имя Int все равно останется на месте
Не сделать forward decl с оригинальным классом, чтобы использовать этот юзинг (т.к. он как правило рядом с определением класса)
Бесконтрольные forward decl, к сожалению, прямой путь к хоть и мелкому, но геморрою. Такие определения следует держать в одном месте, а тогда и using-и не проблема от слова совсем.
Для меня выглядит как исключительное событие
Редкое, но не то, чтобы исключительное. Например, для unique_ptr -- был простой, стал unique_ptr с кастомным deleter-ом. Для shared_ptr -- был обычный std::shared_ptr, поменяли на кастомный без поддержки weak-references и с простым счетчиком ссылок место атомарного (по типу Rc в Rust, который отличается от Arc). Или же был обычный std::shared_ptr, а стал каким-нибудь boost::intrusive_ptr.
using Int = int;
А давайте сделаем вашу попытку пошутить более серьезной и возьмем такой пример:
using AxisX = int;
using AxisY = int;
using Width = int;
using Height = int;
using Radius = int;
...
ShapeUptr makeRectangle(AxisX x, AxisY y, Width cx, Height cx);
ShapeUptr makeCircle(AxisX x, AxisY y, Radius r);
Очевиднее ли это будет, чем обычные int-ы?
Для быстрого прототипирования сойдет. А потом можно будет в using-ах int-ы заменить на какой-то из вариантов strong typedef и компилятор еще и сам по рукам разработчикам бить начнет, когда они радиус с шириной начнут путать по недосмотру.
не ну геморой, но у меня удалось добавить дабл кватернион с решением зависимости(он содержит 2 кватерниона и его методы тянут парочку функций) через ввод парочки в функций(так просто проще по итогу и я их переименовал просто дописал 1 в конец, более красивого решение чтобы замкнуть типы в неймспейсе и решить зависимость я не нашел покачто) прям в неймспейс, другие моменты с именованием еще геморнее я вчера смотрел(мне было проще продублировать внутрь парочку функций зато сохранил стиль либы так скажем)
у меня тоже именование, но оно закрывает неймспейс в неймспейсе только типы закрыты, получилось прикольно
namespace NameLib{
template<typename T>
class V{
};
template<typename T>
class V1{
};
}
using v=NameLib::V<float>;
template<typename T>
T fucntion(T l,T r){
if constexpr(std::is_samev<T,v>){
return l+r;
}
}
Не знал, что неиспользование гитлаба — смертный грех, караемый остракизмом.
Я, например, подавляющее большинство написанного кода открываю в OSS, у нас это неповоротливое говнище не в чести.
Автор исходного комментария посетовал (справедливо) мол как можно так писать на C++ в 2025. А еще он сказал что не знает, что такое MR.
Я поиронизировал мол как можно в 2025 не знать что такое pull/merge request, особенно если приходится часто ревьють код.
Я поиронизировал мол как можно в 2025 не знать что такое pull/merge request
Давайте будем точными: речь шла про MR, а не про PR.
И да, непонятно, если я не пользуюсь gitlab-ом от слова совсем, то чем это в 2025-ом отличается от 2015-го?
Есть такая штука, называется "здравый смысл". Необязательно все доводить до абсурда =/.
К нынешнему моменту наверное 99% опенсурса хостятся на двух платформах: github и gitlab. Сложно найти разработчика, который с ними не сталкивался.
Я конечно догадался что вы не признали термин merge request, потому и написал мол это как pull request, только gitlab.
И разумеется это мало чем отличается от 2015, вот как будто непонятно откуда в моем комментарии взялся 2025.
Претензия не к ООП или архитектуре.
Претензия к использованию устаревших и не идиоматических конструкций. Как будто вы не очень хорошо знаете C++, но тогда возникает вопрос зачем вы пишете пример именно на нем?
Я бы был менее категоричен: во-первых: «не очень хорошо знаете современный С++»,
Во-вторых: в предыдущей статье я ссылался на книгу Гради Буча, которая вышла некоторое время назад. Примеры там написаны на C++/Java без использования новейших фич языков и даже без темплейтов. Так что я старался держаться одного стиля с оригиналом.
Чтоб повысить заход на эту статью надо было добавить теги #c, #assembler и #xml.
И непонятно зачем и холиварно.
Шутка.
Следующая итерация уже в пути. Требования изменятся. Канвас расширится. Архитектура проявит себя.
Не хватает: треугольника, квадрата, овала, ромба. :)
Спойлер: в итоге не будет никаких Circle, Rectangle и прочих треугольников, останется только Shape с набором кривых, описывающих контур фигуры.
Все гораздо проще. Есть план «развития» продукта. Что вполне вписывается в «тестовую» архитектуру. Задача посмотреть как разные подходы будут себя вести. Опять-таки, задача сохранить „time to market“. Ниже привели пример, который вроде как позволит все безгранично расширять. Но как MVP это в любом случае оверинжиниринг.
Вот вам безгранично расширяемый 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();
}
Есть к нему какие-нибудь претензии?
Занятный подход
Есть, конечно
Рисование пропало внутри create тут и конструктор и рисование
Значит, при добавлении или модификации нового метода будет всё сломано.
Сделай рисование красным. Сделай вычисление площадей
Рисование пропало внутри create тут и конструктор и рисование
Ничего не пропало, фабричные функции (в терминах ООП) создают объекты (сущности) с требуемой от них функциональностью. Посколько требовалось пока только делать "render", то из одной этой функции сущности и состоят.
Значит, при добавлении или модификации нового метода будет всё сломано.
Да с чего бы сломано? Как там вообще что-то сделать, чтобы сломать?
Сделай рисование красным.
По хорошему, покраска уже после отрисовки формы должна быть. Типа такого:
type Color = "Black" | "Red" | "Green" | "Blue" | "Magenta" | "Salmon" | "Gold";
const getCanvas = () => {
const shapes: Shape[] = [];
return {
add: (shape: Shape) => shapes.push(shape),
render: (color?: Color) => shapes.map(shape => shape() + (color ? ` ${color}` : "")),
}
}
Сделай вычисление площадей
Тоже работы на 3 минуты.
Скрытый текст
type Shape = {
render: () => string,
getArea: () => number,
}
type Color = "Black" | "Red" | "Green" | "Blue" | "Magenta" | "Salmon" | "Gold";
const create = {
point: (x: number, y: number): Shape => ({
render: () => `Drawing a Point at (${x},${y})`,
getArea: () => 0,
}),
circle: (x: number, y: number, r: number): Shape => ({
render: () => `Drawing a Circle R=${r} at (${x},${y})`,
getArea: () => r * r * Math.PI,
}),
rectangle: (x1: number, y1: number, x2: number, y2: number, width: number): Shape => ({
render: () => `Drawing a Rectangle (${x1}, ${y1}) to (${x2},${y2}) width=${width}`,
getArea: () => Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) * width,
})
};
const getCanvas = () => {
const shapes: Shape[] = [];
return {
add: (shape: Shape) => shapes.push(shape),
render: (color?: Color) => shapes.map(shape => shape.render() + (color ? ` ${color}` : "")),
getTotalArea: () => shapes.reduce((sum, cur) => cur.getArea() + sum, 0),
}
}
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();
canvas.render("Gold");
}
Хотя, можно, конечно, и поупарываться
Скрытый текст
type ShapeType = "circle" | "point" | "rectangle";
type Shape = [ShapeType, ...number[]];
type Color = "Black" | "Red" | "Green" | "Blue" | "Magenta" | "Salmon" | "Gold";
const create = {
circle: (x: number, y: number, r: number): Shape => ["circle", x, y, r],
point: (x: number, y: number): Shape => ["point", x, y],
rectangle: (x1: number, y1: number, x2: number, y2: number, width: number): Shape => ["rectangle", x1, y1, x2, y2, width],
};
const renderers: Record<ShapeType, (shape: Shape) => string> = {
circle: ([_, x, y, r]) => `Drawing a Circle R=${r} at (${x},${y})`,
point: ([_, x, y]) => `Drawing a Point at (${x},${y})`,
rectangle: ([_, x1, y1, x2, y2, width]) => `Drawing a Rectangle (${x1}, ${y1}) to (${x2},${y2}) width=${width}`,
}
const render = (shape: Shape, color: Color = "Black") =>
(renderers[shape[0]] || (() => ""))(shape) + (color ? ` ${color}` : "");
const areaCalculators: Record<ShapeType, (shape: Shape) => number> = {
circle: ([_, x, y, r]) => r * r * Math.PI,
point: () => 0,
rectangle: ([_, x1, y1, x2, y2, width]) => Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) * width,
}
const getArea = (shape: Shape) =>
(areaCalculators[shape[0]] || (() => NaN))(shape);
const getCanvas = () => {
const shapes: Shape[] = [];
return {
add: (shape: Shape) => shapes.push(shape),
render: (color?: Color) => shapes.map(shape => render(shape, color)),
getTotalArea: () => shapes.reduce((sum, cur) => getArea(cur) + sum, 0),
}
}
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();
canvas.render("Gold");
canvas.getTotalArea();
}
circle: (x: number, y: number, r: number): Shape => ({
render: () => `Drawing a Circle R=${r} at (${x},${y})`,
getArea: () => r * r * Math.PI,
}),
Нарушение принципа единственной ответственности, разные вещи а одном месте и разделить нельзя. То есть если один функционал пришел один человек, а другой другой, то они будут мешать друг другу. Даже если один, то вам придется постоянно переключать контекст внимания
не будет никаких Circle
Окружность не представима кривыми Безье. Так что может и будут, аппроксимация не всегда приемлема.
Ну значит возьмем B-сплайны. Какими кривыми аппроксимировать это детали реализации. В первом приближении можно вообще набором прямых обойтись.
возьмем B-сплайны
Окружность точно не представима никакими сплайнами, хотя аппроксимируют почти всегда через них. Точно берут там, где важна гомогенность пространства, это типа чтобы когда вы вал в отверстии виртуально поворачиваете, его не заклинивало на узлах соотв производных из-за того, что мы аппроксимацию взяли вместо окружности.
у безье если она опишет круг на сколько я понял когда вникал(пока не глубоко), надо будет определять попадает точка в растеризаторе или не попадает(это я увидел в видео по шрифту), если попадает, то вставляем точку, но тут шейп это ББ скорее всего, соотв круг имеет радиус, потомучто я сомневаюсь что задача написать растеризатор, потомучто проще картинку с теми же точками грузануть, если
{
T P1; // Point 1
T C1; // Control 1
T P2; // Point 2
T C2; // Control 2
}х2 соотв проверить можно в блендере, кароче есть туториал там ИК он крепит к ходу по безье и там можно создать круг канал забыл как называется polyfjord (Animate a Character in 15 Minutes in Blender)
ну и в конце надо по гаусу размыть край по сигме 1 или 2
ну и не знаю если это 100% круг можно и стандартной функцией пройтись с радиусом, а в конце гаусом
Скрытый текст

Скрытый текст
#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
во как бы я сделал, но в идеале точки бы были из библиотеки матеши - это будет влиять на архитектуру кстати
На мой неискушенный взгляд вы тут слегка огород нагородили. Вместо понятного синтаксиса: тип: переменная вы все закинули в массив с неясной структурой. Далее, у вас все элементы массива оказались одного и того же типа. Что может быть нежелательным ограничением.
std::println("Drawing {} at {} {} {} {}",name(),p[0],p[1],p[2],p[3]);
}
Я бы не хотел такой код поддерживать, когда автор давно работу поменял, а мне понять надо что там и где используется.
потомучто в вашем примере не было векторной математики, вывод переведён в 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;
}
тоесть вам надо написать векторную математику (vec2-3-4, quaternion или rot, mat2-3-4)), а потом относительно её архитектуры накидывать архитектуру отрисовки и растеризации точек треугольников или позиций картинок, спасибо за минус - у меня кстати работает )
Не стал менять архитектуру (почти), ведь задачи как таковой нет и тут нечего архитектурить, просто переписал на новые технологии
#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;
}
Еще не вечер господа. Давайте лучше поглядим на эти танцы когда требования начнут долполняться/изменяться.
Суета какая-то
{-# LANGUAGE GADTs #-}
-- Определение типа Shape
data Shape where
Point :: Int -> Int -> Shape
Circle :: Int -> Int -> Int -> Shape
Rectangle :: Int -> Int -> Int -> Int -> Shape
-- Функция для рисования
draw :: Shape -> String
draw (Point x y) = "Drawing Point at (" ++ show x ++ ", " ++ show y ++ ")"
draw (Circle x y r) = "Drawing Circle at (" ++ show x ++ ", " ++ show y ++ "), r = " ++ show r
draw (Rectangle x y w h) = "Drawing Rectangle at (" ++ show x ++ ", " ++ show y ++ "), " ++ show w ++ "x" ++ show h
-- Определение типа Canvas
data Canvas = Canvas [Shape]
-- Функция для добавления фигуры
add :: Shape -> Canvas -> Canvas
add shape (Canvas shapes) = Canvas (shape : shapes)
-- Функция для рендеринга
render :: Canvas -> [String]
render (Canvas shapes) = map draw shapes
-- Пример использования
main :: IO ()
main = do
let canvas = add (Point 1 1) $ add (Circle 5 5 3) $ add (Rectangle 0 0 6 3) (Canvas [])
mapM_ putStrLn (render canvas)
Товарищи, пожалуйста, читайте ТЗ до конца:
Задача: реализовать базовый графический редактор
Фигуры: точка, линия, круг, квадрат, прямоугольник, треугольник, ромб, овал
Функциональность: добавление фигур на канвас, отрисовка
С таким подходом мы собеседование в FAAG ( или как его там, FB, Amazon,Apple,Google..) не пройдем :)
Все в детали углубились а базовый функционал никто не реализовал.
Я просто переписал твой код. А ТЗ куда-то затерялся.
Faang. N netflix
{-# LANGUAGE GADTs #-}
newtype Angle = Degrees Double deriving (Eq, Show)
data Shape where
Point :: Double -> Double -> Shape -- x, y
Line :: Double -> Double -> Double -> Double -> Shape -- x1,y1,x2,y2
Triangle :: Double -> Double -> Double -> Double -> Double -> Double -> Shape -- x1,y1,x2,y2,x3,y3
Circle :: Double -> Double -> Double -> Shape -- centerX,centerY,radius
Rectangle :: Double -> Double -> Double -> Double -> Angle -> Shape -- centerX,centerY,width,height,angle
Square :: Double -> Double -> Double -> Angle -> Shape -- centerX,centerY,side,angle
Rhombus :: Double -> Double -> Double -> Double -> Angle -> Shape -- centerX,centerY,diag1,diag2,angle
Oval :: Double -> Double -> Double -> Double -> Angle -> Shape -- centerX,centerY,radiusX,radiusY,angle
draw :: Shape -> String
draw (Point x y) = "Drawing Point at (" ++ show x ++ ", " ++ show y ++ ")"
draw (Line x1 y1 x2 y2) = "Drawing Line from (" ++ show x1 ++ ", " ++ show y1 ++ ") to (" ++ show x2 ++ ", " ++ show y2 ++ ")"
draw (Triangle x1 y1 x2 y2 x3 y3) =
"Drawing Triangle with vertices ("++show x1++","++show y1++"), ("++show x2++","++show y2++"), ("++show x3++","++show y3++")"
draw (Circle x y r) =
"Drawing Circle at ("++show x++","++show y++"), r = "++show r
draw (Rectangle x y w h angle) =
"Drawing Rectangle at ("++show x++","++show y++"), "++show w++"x"++show h ++ rotate angle
draw (Square x y side angle) =
"Drawing Square at ("++show x++","++show y++"), side = "++show side ++ rotate angle
draw (Rhombus x y d1 d2 angle) =
"Drawing Rhombus at ("++show x++","++show y++") with diagonals "++show d1++","++show d2 ++ rotate angle
draw (Oval x y rx ry angle) =
"Drawing Oval at ("++show x++","++show y++") with radii "++show rx++","++show ry ++ rotate angle
rotate :: Angle -> String
rotate (Degrees 0.0) = ""
rotate (Degrees a) = ", rotated by " ++ show a ++ " degrees around its center"
data Canvas = Canvas [Shape]
add :: Shape -> Canvas -> Canvas
add shape (Canvas shapes) = Canvas (shape:shapes)
render :: Canvas -> [String]
render (Canvas shapes) = map draw (reverse shapes)
main :: IO ()
main = do
let emptyCanvas = Canvas []
let canvas =
add (Point 0 0) $
add (Line 0 0 5 5) $
add (Triangle 0 0 3 0 0 4) $
add (Circle 10 10 5) $
add (Rectangle 20 20 10 5 (Degrees 30)) $
add (Square 15 15 8 (Degrees 45.1)) $
add (Rhombus 25 25 14 10 (Degrees 60.760)) $
add (Oval 40 40 12 7 (Degrees 75.0876)) emptyCanvas
mapM_ putStrLn (render canvas)
Пока всё идёт нормально
F# более компактный, читаемый, тоже чисто функциональный подход
И заменил на точки вместо пар чисел
// Определяем типы для углов и точек
type Angle = Degrees of float
type Point2D = { X: float; Y: float }
// Определяем тип Shape как discriminated union
type Shape =
| Point of p: Point2D
| Line of p1: Point2D * p2: Point2D
| Triangle of p1: Point2D * p2: Point2D * p3: Point2D
| Circle of center: Point2D * radius: float
| Rectangle of center: Point2D * width: float * height: float * angle: Angle
| Square of center: Point2D * side: float * angle: Angle
| Rhombus of center: Point2D * diag1: float * diag2: float * angle: Angle
| Oval of center: Point2D * radiusX: float * radiusY: float * angle: Angle
// Вспомогательная функция для отображения точки
let showPoint (p: Point2D) = sprintf "(%g, %g)" p.X p.Y
// Функция для строки поворота
let rotate angle =
match angle with
| Degrees 0.0 -> ""
| Degrees a -> sprintf ", rotated by %g degrees around its center" a
// Функция "рисования" фигуры
let draw shape =
match shape with
| Point { p = p } -> sprintf "Drawing Point at %s" (showPoint p)
| Line ({ p1 = p1; p2 = p2 }) -> sprintf "Drawing Line from %s to %s" (showPoint p1) (showPoint p2)
| Triangle ({ p1 = p1; p2 = p2; p3 = p3 }) ->
sprintf "Drawing Triangle with vertices %s, %s, %s" (showPoint p1) (showPoint p2) (showPoint p3)
| Circle ({ center = center; radius = r }) ->
sprintf "Drawing Circle at %s, r = %g" (showPoint center) r
| Rectangle ({ center = center; width = w; height = h; angle = angle }) ->
sprintf "Drawing Rectangle at %s, %gx%g%s" (showPoint center) w h (rotate angle)
| Square ({ center = center; side = side; angle = angle }) ->
sprintf "Drawing Square at %s, side = %g%s" (showPoint center) side (rotate angle)
| Rhombus ({ center = center; diag1 = d1; diag2 = d2; angle = angle }) ->
sprintf "Drawing Rhombus at %s with diagonals %g,%g%s" (showPoint center) d1 d2 (rotate angle)
| Oval ({ center = center; radiusX = rx; radiusY = ry; angle = angle }) ->
sprintf "Drawing Oval at %s with radii %g,%g%s" (showPoint center) rx ry (rotate angle)
// Тип для холста (список фигур)
type Canvas = Shape list
// Добавление фигуры на холст (в начало списка)
let add shape (canvas: Canvas) = shape :: canvas
// Рендеринг холста: применяет draw к каждой фигуре в обратном порядке добавления
let render (canvas: Canvas) =
canvas |> List.rev |> List.map draw
// Главная функция
[<EntryPoint>]
let main argv =
let emptyCanvas: Canvas = []
// Создаем точки для удобства
let pt x y = { X = float x; Y = float y }
let canvas =
emptyCanvas
|> add (Oval { center = pt 40 40; radiusX = 12.0; radiusY = 7.0; angle = Degrees 75.0876 })
|> add (Rhombus { center = pt 25 25; diag1 = 14.0; diag2 = 10.0; angle = Degrees 60.760 })
|> add (Square { center = pt 15 15; side = 8.0; angle = Degrees 45.1 })
|> add (Rectangle { center = pt 20 20; width = 10.0; height = 5.0; angle = Degrees 30.0 })
|> add (Circle { center = pt 10 10; radius = 5.0 })
|> add (Triangle { p1 = pt 0 0; p2 = pt 3 0; p3 = pt 0 4 })
|> add (Line { p1 = pt 0 0; p2 = pt 5 5 })
|> add (Point { p = pt 0 0 })
render canvas |> List.iter (printfn "%s")
0 // Возвращаем код выхода
Ну, побежали дальше. Что мы там должны развивать?
Зачем тут GADT расширение языка используется?
Что-то уже было в предыдущем поколении. И споры. И даже с кодом: http://softcraft.ru/paradigm/dhp/
Дежавю, однако...
Для коллекции. Код на Си. Ну, почти чистом Си (http://softcraft.ru/ppp/ppc/).
#include <stdio.h>
#include <stdlib.h>
typedef struct Point {int x, y;} Point;
void PointInit(Point* p, int x, int y) {
p->x = x; p->y = y;
}
void PointDraw(Point* p) {
printf("Drawing Point at (%d, %d)\n", p->x, p->y);
}
char* PointName() {
return "Point";
}
typedef struct Circle {int x, y, r;} Circle;
void CircleInit(Circle* c, int x, int y, int r) {
c->x = x; c->y = y; c->r = r;
}
void CircleDraw(Circle* c) {
printf("Drawing Circle at (%d, %d), r = %d\n", c->x, c->y, c->r);
}
char* CircleName() { return "Circle"; }
typedef struct Rectangle {int x, y, w, h;} Rectangle;
void RectangleInit(Rectangle* r, int x, int y, int w, int h) {
r->x = x; r->y = y; r->w = w; r->h = h;
}
void RectangleDraw(Rectangle* r) {
printf("Drawing Rectangle at (%d, %d), %d*%d\n",
r->x, r->y, r->w, r->h);
}
char* RectangleName() { return "Rectangle"; }
typedef struct Shape {}<> Shape;
Shape + <Point;>;
Shape + <Circle;>;
Shape + <Rectangle;>;
void Draw<Shape* s>() = 0;
void Draw<Shape.Point* s>() { PointDraw(&(s->@)); }
void Draw<Shape.Circle* s>() { CircleDraw(&(s->@)); }
void Draw<Shape.Rectangle* s>() { RectangleDraw(&(s->@)); }
char* Name<Shape* s>() {return NULL;} // = 0;
char* Name<Shape.Point* s>() { return PointName(); }
char* Name<Shape.Circle* s>() { return CircleName(); }
char* Name<Shape.Rectangle* s>() { return RectangleName(); }
Shape* CreateShapeAsPoint(int x, int y) {
struct Shape.Point* s = create_spec(Shape.Point);
PointInit(&(s->@), x, y);
return (Shape*)s;
}
Shape* CreateShapeAsCircle(int x, int y, int r) {
struct Shape.Circle* s = create_spec(Shape.Circle);
CircleInit(&(s->@), x, y, r);
return (Shape*)s;
}
Shape* CreateShapeAsRectangle(int x, int y, int w, int h) {
struct Shape.Rectangle* s = create_spec(Shape.Rectangle);
RectangleInit(&(s->@), x, y, w, h);
return (Shape*)s;
}
void DeleteShape(Shape* s) {free(s);}
typedef struct Canvas {
int len;
Shape* shapes[100];
} Canvas;
void CanvasInit(Canvas* c) {c->len = 0;}
void CanvasClear(Canvas* c) {
for(int i = 0; i < c->len; ++i) {
DeleteShape(c->shapes[i]);
}
c->len = 0;
}
void CanvasAdd(Canvas* c, Shape* s) {
c->shapes[c->len++] = s;
}
void CanvasRender(Canvas* c) {
for(int i = 0; i < c->len; ++i) {
Draw<c->shapes[i]>();
}
}
int main() {
Canvas canvas;
CanvasInit(&canvas);
CanvasAdd(&canvas, CreateShapeAsPoint(1, 1));
CanvasAdd(&canvas, CreateShapeAsCircle(5, 5, 3));
CanvasAdd(&canvas, CreateShapeAsRectangle(10, 10, 10, 10));
CanvasRender(&canvas);
CanvasClear(&canvas);
return 0;
}
Если интерес касается разных эволюций кода, то можно посмотреть здесь:
https://github.com/kreofil/evo-situations/tree/main/evolution
Старое описание ситуаций можно почитать отсюда:
http://softcraft.ru/ppp/simplesituations/
Из последнего: http://softcraft.ru/ppp/
это графический редактор надо добавлять шейп в растр в нужную точку(тоесть есть полотно формата и есть известный его размер)[w*h*4] - просто так шейпы собирать не надо в массивах, и по save сохранять в картинку, если это отрисовка в через массив шейпов это картинки шейпов с (pos left corner,w/h) для интерфейсов или функционал кисточки тоже с нужным офсетом на полотне
ООП или не ООП — вот в чём ревью