Ошибки в архитектуре ПО и как их избежать. Часть 2

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

В первой части мы уже рассмотрели кейсы о выборе технологий на основе личных преференций, об Event driven state machine, неправильной настройке ORM. Во второй части говорим о согласовании нефункциональных требований, использовании хайповых технологий, особенностях TypeORM и прочем.

Иллюстрация Алины Самолюк

Александр, Senior Embedded Engineer в Sirin Software

Использовали СУБД на встроенном устройстве

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

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

Рассмотрев эту задачу, мы с лидом решили, что лучше будет хранить все данные на сервере, а на клиентах только показывать. Потому что, во-первых, меню должно было выглядеть на всех девайсах одинаково. Во-вторых, на смену статусов были завязаны определенные действия. А в-третьих, MVC (именно так этот аргумент нашего лида и звучал). Других причин принять такое решение уже не помню, но все они казались важными, логичными и правильными.

Следующий вопрос, который встал перед нами: как хранить? В масштабе системы данных было потенциально много, они записывались и читались на десятках различных сервисов и клиентов. Следовательно, необходимо было управлять этим процессом и обеспечивать целостность информации.

Именно здесь и возникла архитектурная ошибка: для решения этой задачи мы подняли на встроенном устройстве реляционную СУБД. Просчет статусов дерева пришлось выполнять там же, а с клиентов — только читать готовые, потому что некоторые действия, в зависимости от статуса, должен был выполнять сам сервер (и, следовательно, знать эти статусы).

Быстро это, конечно, не работало. Цепочка действий выглядела так:

Сервис1 считывает данные и записывает в базу → сервис2 считывает данные и разрисовывает дерево → сервис2 записывает дерево обратно в базу → клиент загружает дерево из базы и показывает. Повторять как можно чаще. Сервис1 существовал в десятке экземпляров (для разных источников данных), процессор был одноядерный ARM с частотой примерно в 800 МГц, поэтому СУБД такое издевательство над собой переносила на удивление плохо. Задержка между переключением сенсора и изменением картинки GUI достигала секунды и более, что было абсолютно неприемлемо.

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

Гениальнейшим из всех принятых для повышения быстродействия решений я гордо считаю собственное предложение избавиться от сервиса2 (того, что раскрашивал дерево по конфигу), писать конфиг сразу в базу и там же проводить все расчеты на триггерах и SQL-функциях. В результате мы получили несколько приличных страниц сложного SQL, делающего истинную «черную магию с кровавыми жертвоприношениями» (например, парсер неравенств, ведь условия для статусов могли быть заданы выражениями, на regexp и табличном стеке), но таки работающего и выполняющего задачи достаточно быстро. А элементы, требующие наискорейшего отображения, мы вынесли на клиенты, чем замаскировали чрезвычайную медлительность базы, державшей процессор загруженным на 80% и более.

Это лишь один пример дальнейших проблем и их «гениальных» решений. Когда у нас все-таки появился веб-сервис, всю логику девайсов нужно было показывать на нем и оттуда конфигурировать, поэтому структура его базы повторяла структуру девайсов с пугающей точностью, и таблиц было где-то на полсотни больше. И хотя единообразие структуры было уже правильным (иначе синхронизировать было бы на порядок сложнее), оно означало вынужденное «заражение» той первой роковой ошибкой еще и нашего облака, что стало очередным блоком в монументе архитектурным фараонам.

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

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

Пирамида продолжала стоять.

Вывод. Самые болезненные архитектурные ошибки — те, на основе которых уже невозможно построить что-то стабильное. Отказаться от того, во что уже вложено много времени и усилий, всегда трудно, но обычно потери на переход «прямо сейчас» будут гораздо меньше, чем «потом». Заменить кирпич проще, когда пирамида на нем еще не стоит.

Олег Ботвенко, ex-CTO в Yellow Stone

Излишний оптимизм и легкомыслие по отношению к новым хайповым технологиям

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

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

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

Но MongoDB мы все же внедрили и стали использовать для хранилища логов (у нас был подход — логировать все, это позволяло быстро расследовать сложные кейсы в продакшене). И когда наша база логов выросла до порядка 600 Гб и нагрузка на запись заставила расшарить ее на четыре машины, мы столкнулись со множеством нюансов и проблем в работе MongoDB, о которых не имели представления на старте.

Оказалось, что MongoDB по надежности, предсказуемости и отлаженности даже близко не стояла рядом с MySQL. Среди проблем, которые всплыли, — нелинейная деградация производительности, много блокирующих операций, нестабильная работа в целом под нагрузкой.

В основном команда признала, что отказ от MongoDB в качестве основной операционной базы спас нас от космического объема геморроя.

Вывод. С тех пор я сформулировал для себя следующее правило внедрения новых технологий в продукте: в продакшен можно отдавать только те технологии, в отладку которых вложено время, чтобы достичь качества, сопоставимого с MySQL, PostgreSQL, Nginx, Apache и другими системами такого уровня. Кроме того, в команде должно быть не менее трех человек с опытом работы с этой системой в продакшене и с личной ответственностью за ее работоспособность.

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

Олег Боровик, Senior Solutions Architect в IntellectEU

Не учли и не согласовали нефункциональные требования

Любимое высказывание среди архитекторов — it depends. После этого обычно следует множество уточняющих вопросов. То, что отвечает условиям и ограничениям одного проекта, в другом может считаться неоптимальным или недостаточно безопасным решением. Именно поэтому для архитектора коммуникационные навыки столь же важны, как и технические.

Неучтенный constraint, неправильно приоритезированные quality attributes, несогласованные NFRs приводят к выбору неправильных подходов и технологий, а это — к потере времени и денег, а иногда и к закрытию проекта.

Одним из таких случаев в моем опыте был интеграционный проект в SOA-инфраструктуре клиента для регистрации в разных подсистемах (для линии продуктов): почте, календаре, хранилище документов и так далее. Мы использовали Event Sourcing/CQRS на базе CassandraDB — решение очень чувствительное к изменениям в domain model (как оказалось позже). Каждый входящий клиентский запрос генерировал цепочку immutable ивентов, которые делали процесс прозрачным и позволяли относительно легко восстановить нужное состояние с помощью функциональных конструкций. Но при этом создавалось множество новых объектов в исходном коде с не очень прозрачными связями.

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

Выводы

  1. Архитектор должен иметь полную картину того, что происходит и что может повлиять на проект.
  2. При выборе архитектурного подхода надо учитывать риски — как технические, так и организационные.
  3. Именно event sourcing можно выбирать только в случаях минимально возможных изменений бизнес-модели. Изменения после выхода в продакшн стоят очень дорого.

Андрей Андрийко, Engineering Manager в Uptech

Использовали дефолтные механизмы TypeORM для подтягивания реляций

В нашей работе мы часто пишем на TypeScript и используем одну из самых популярных ORM для Node.js — TypeORM.

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

@Entity({ name: 'whatever_item' })
export class WhateverItemEntity {
    @PrimaryColumn({ name: 'id' })
    id: string;
    // ...

    @OneToMany(type => PhotoEntity, photo => photo.listing)
    photos?: PhotoEntity[];
    // ...
}

Одна из базовых функций ORM — это работа с реляциями между сущностями. Реляции в TypeORM также описываются через декораторы, как показано в примере выше. Как и в других ORM, в TypeORM есть eager loading и lazy loading. Больше о том, как работают реляции, можно почитать тут. Но в этой документации нет одной важной детали: как именно ORM достает реляции из базы (JOIN, Sub-select, отдельные запросы). На эти грабли мы и напоролись.

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

Работая с TypeORM, мы решили не использовать lazy loading через промисы, так как в документации есть фраза «Note : ...This is non-standard technique and considered experimental in TypeORM». К тому же, если не использовать eager loading явно, реляции не будут подгружаться. Это позволяло нам самим указывать, в каких случаях подгрузить реляции, а в каких нет.

Базовый способ подтягивания реляций в TypeORM — repository.find({ relations: [’photos’] }). Изначально мы не обратили внимания, какой именно SQL-запрос/запросы составляет TypeORM, и начали использовать этот подход. Уже позже заметили, что на dev environment страница грузится достаточно долго — 1,5–2 с.

Немного разобравшись, локализировали проблему: это был запрос, который составляла наша ORM. Она подтягивала все реляции через LEFT JOIN. Сам по себе объект товара имел много свойств. Также к нему подтягивались его изображения через JOIN (~15 штук), и к каждому изображению фото в разных размерах (еще около 6). В итоге для страницы в 20 товаров из базы доставалось 1800 строк. Колонок в этом результате было слишком много. Большинство данных в колонках дублировались, так как таблица товаров была очень большая и к каждому товару подбиралось еще 90 строк.

Основные проблемы здесь очевидны:

  • Передаем по сети много «лишних» данных → долгий ответ от базы.
  • Много строк → TypeORM долго маппит результат в объекты.

Бонус: каждая ссылка на картинке подписывалась AWS-ключами для доступа из S3, что удлиняло ее в несколько раз.

В итоге JSON response на 1–2 MБ и ~2 c время загрузки данных. Такой результат нам, конечно же, не подходил.

Чтобы решить проблему, нужно было менять способ работы с реляциями. Lazy loading в том виде, в котором он был в TypeORM, нам не нравился. При этом делать отдельные запросы для реляций по всему коду не хотелось, ведь зачем нам тогда ORM? Нам было важно изолировать работу с базой данных в отдельной части приложения, но при этом сохранить гибкость и управлять реляциями, которые надо доставать.

Мы решили написать вспомогательный инструмент, который бы позволил улучшить работу с реляциями, и назвали его Preloader. Разрабатывая эту утилиту, исходили из следующих фактов:

  • запросы по PK (primary key) быстрые;
  • JOIN’ы для many-to-one реляций медленные;
  • если сгруппировать похожие запросы в один batch-запрос, будет быстрее (меньше сетевого оверхеда);
  • метаинформация Entity из TypeORM поможет не дублировать маппинги к таблицам.

Note: почему использовать Preloader, а не просто предложенные инструменты TypeORM, такие как query builder, и выполнять через него subselect/join? Потому что в таком случае надо писать отдельный запрос на каждый случай. Плюс, если подтягивать реляции одним запросом, SQL отдаст большую таблицу с большим количество дублирующихся данных, как описано выше. И тогда часть проблемы останется нерешенной.

Как работал Preloader

С помощью метаданных из TypeORM Entity Preloader находил поля-реляции и маппинг на колонки. При исполнении запроса он брал реляции, которые надо подтянуть, и доставал их отдельными запросами по PK. Через утилиту Dataloader похожие запросы группировались в один и выполнялись. Дальше реляции маппились к нужным родительским сущностям.

В коде это выглядело следующим образом:

const items = await itemsRepository.findByIds([ 'id1', 'id2' ]);
await new Preloader(ItemEntity, items).preload({ photos: [ 'sizedPhotos' ]);

В результате выполнялось три запроса:

SELECT * FROM items WHERE id IN (...)

SELECT * FROM photo WHERE id IN (...)

SELECT * FROM photos_sized WHERE id IN (...)

Вместо SELECT * FROM items LEFT JOIN photo … LEFT JOIN photos_sized ...

После всех оптимизаций (также мы убрали вставку ссылок на картинки и уменьшили количество размеров, которые отдаем для каждого фото) response time страницы уменьшился до 300–500 мс.

Выводы

  • Всегда нужно понимать, как работает ваш инструмент, будь то фреймворк, ORM или что-то другое. Это поможет использовать его с максимальной эффективностью.
  • Логирование и мониторинг — must have.
  • Больше запросов не означает худший перформанс.
  • Не стоит бояться расширить возможности инструмента самостоятельно, написав утилиту поверх него. Это не так долго и дорого, как может показаться на первый взгляд.

Отправка асинхронных задач на выполнение до коммита транзакций

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

  • сгенерировать номер заказа;
  • проверить наличие товара и забронировать товар;
  • провести оплату;
  • отправить чек покупателю на имейл и так далее.

Выполнять все эти действия синхронно в рамках запроса пользователя неэффективно. Так, сделав заказ, человек будет секунд 30–60 смотреть на экран загрузки. Чтобы избежать этой проблемы, часть работы можно выполнить асинхронно. Например, через event в message broker или просто с помощью внутренних механизмов вашего технологического стека.

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

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

Еще бывали ситуации, когда асинхронная задача начинала работу до того, как выполнялась основная транзакция. В этом случае она также пыталась обратиться к данным, которых еще нет в базе.

Мы выделили три основных способа решения.

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

Ввести idempotency key, то есть какой-то ID транзакции, который мы будем пересылать в ивентах и на основе которого будем проверять, выполнилась ли транзакция вообще и не выполнили ли мы эту операцию раньше (если будем делать retry). Таким образом можем получить идемпонтентные операции. Этот подход эффективен, но достаточно усложняет общую логику. К тому же необходимо хранить, везде передавать и проверять ключи идемпотентности. Мы решили, что на данном этапе это того не стоило.

Создать таблицу в базе для асинхронных задач. Для запуска иметь периодическую джобу, которая бы доставала задачи из базы раз в пару секунд и выполняла их. Здесь мы используем транзакцию основной операции, чтобы «запланировать» асинхронную задачу. То есть, если транзакция завершается успешно, асинхронные задачи сохраняются в базу, откуда будут вычитаны периодической джобой и поставлены на выполнение. После выполнения задача из базы удаляется. Если же транзакция роллбэкается, то асинхронные задачи в базу не попадают и не выполняются. Подход похож на паттерн Saga, только в рамках одного сервиса. Мы использовали именно этот метод.

В таблице с задачами достаточно сохранить «ключ» или название класса, который будет выполнять задачу, и параметры, которые будете туда передавать. Тут необходим механизм, который позволит в рантайме получить класс/функцию, которая будет выполнять задачу по ее имени. Таким механизмом может быть Dependecy Injection.

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

👍НравитсяПонравилось9
В избранноеВ избранном12
Подписаться на тему «архитектура»
LinkedIn



Підписуйтесь: Soundcloud | Google Podcast | YouTube


13 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

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

так если и используют — там столько всего допиливают в глубинах или вообще мимо ORMа, что бывает возникает вопрос — а зачем нам вообще этот ORM :)

сталкивался с этим и на Джаве, когда Хибернейт начинает путаться под ногами
и на пыхе, когда мат перемат на Doctrine

просто есть такое народное поверие, в книжках и у начинающих (которые самые активные в инетах, и поэтому создают иллюзию «мнения большинства») что ORM «спасает от вендор лок», «избавляет от необходимости знания БД», и прочая
вот и вешают подкову на шею, а крестик над порогом

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

Помню когда в Укрсибе работал, там тогда была практика что хороший манагер должен сам себе отчеты в SQL на прод базе генерить. И напишет вот чудо гений чето запустит — база вешается.. пока ты пытаешся убить его запрос, он в локальном чате скидывает его другим и грит «вот я написал, юзайте» и еще + 20 таких же запросов. И это была жопа.

По той же причине мне оч не нравяться транспайлеры кода. они очнь часто приводят к тем же проблемам.

я очень против всего неявного в коде

тоже все больше против.

ORMы меня раздражать стали уже со старта
всеми этими аннотациями упаришься описать схему оптимальную под типичные запросы, и с точки зрения конкретной БД
они все заточены на «правильную теорию» — объекты главные, поэтому надо их хранить в БД как они описаны в коде ООЯ. а то что основные БД такого хранения очень не любят — зачем знать?
да и, ах, тормозит, возьмем «монгу»!

По той же причине мне оч не нравяться транспайлеры кода.

да TS->JS вроде ничего

а так да, «ай, посмотрите какой классный генератор!»
ыть, надо в конечном коде одну строчечку чуть другую.
сцуко — как тебе, генератору объяснить то про эту строчечку!

не следил давно, но был период
в Magento 2 понапихали генераторов кода.
Потом, вроде выпиливали, потому что народ взвыл от радости саппорта нагенерированного

но начинающие — очень любят :)
«умные ж дяди все это придумали не зря! я на ютьюбе смотрел доклад. и главное — ни думать, ни знать не надо! оно само — раз и Чудо!»

Но MongoDB мы все же внедрили и стали использовать для хранилища логов (у нас был подход — логировать все, это позволяло быстро расследовать сложные кейсы в продакшене). И когда наша база логов выросла до порядка 600 Гб и нагрузка на запись заставила расшарить ее на четыре машины, мы столкнулись со множеством нюансов и проблем в работе MongoDB, о которых не имели представления на старте.

Логи писать в базу, причем судя по всему еще и без шардинга по времени, месту и прочему. жесть
Может для логов все же писать на диск + etl, или заюзать EKL или Splunk?
Пугают меня такие рекомендации...

Ну тогда и от меня в копилочку: типичные причины проблем в архитектуре десятилетней давности: dedic.ru/...​типичные-ошибки-клиентов

Спасибо, интересная подборка.

Следующий вопрос, который встал перед нами: как хранить? Именно здесь и возникла архитектурная ошибка: для решения этой задачи мы подняли на встроенном накопителе реляционную СУБД.

У меня на такие случаи правило:
хранить данные в raw виде как можно дольше в уровнях системы. А их обработка должна делаться в одном месте. Потому что — мы не можем предсказать точно — какая обработка понадобится в будущем, и какие еще виды raw данных появятся или как изменятся существующие.
Недостаток — можно попасть на большие объемы данных, перегоняемых между слоями, сервисами системы.

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

У этого моего правила есть философский синоним:
Не спешите трактовать происходящие события — мало ли что они значат.
Как в притче «Прекрасный конь»:
pritchi.ru/id_338

Архитектор должен иметь полную картину того, что происходит и что может повлиять на проект.

Он никогда не будет иметь полной картины. Это не здание и не самолет построить.

Ну то есть теоретически это возможно конечно. Но я бы очень не советовал на это надеяться :)

Но в этой документации нет одной важной детали: как именно ORM достает реляции из базы

Как я говорю — всегда смотри и думай на SQL вначале. А потом уже добейся этого от выбранного ORM

С помощью метаданных из TypeORM Entity Preloader находил поля-реляции и маппинг на колонки. При исполнении запроса он брал реляции, которые надо подтянуть, и доставал их отдельными запросами по PK.

Да, это типичное решение с любым серьезным ORMом. Вначале вытягиваем все что самое подчиненное, потом склеиваем, средствами БД или в приложении.
Кратко — от объектного представления данных переходим к реляционному. К bulk операциям а не навигации по данным:
А.B.C.D которая суть — дерево, а не наборы множеств и их пересечений.

Делать так сложно когда все по «DDD», переписывание domain model с инкапсулированным поведением может стать нерентабельным. потому что придется переписать — считайте всё.
Много проще — когда Anemic, там моделям все равно, как они собираются. Поведение всего равно вне них.

Здесь мы используем транзакцию основной операции, чтобы «запланировать» асинхронную задачу. То есть, если транзакция завершается успешно, асинхронные задачи сохраняются в базу, откуда будут вычитаны периодической джобой и поставлены на выполнение.

Наверное не понял в чем ноу хау. Это ж обычная фишка — доверить бизнес транзакцию — транзакции БД.
А если ваша БД не умеет транзакции — то выбрать такую БД которая это умеет. Недостаток — при большой нагрузке на запись можно упереться вертикально. Ну тогда все, кластер, мастер-слейв, Saga...

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

Делать так сложно когда все по «DDD», переписывание domain model с инкапсулированным поведением может стать нерентабельным. потому что придется переписать — считайте всё.
Много проще — когда Anemic, там моделям все равно, как они собираются. Поведение всего равно вне них.

В DDD, Application Layer можно писать в CQRS стиле, при сохранении данные будут обрабатываться бизнес логикой из Domain слоя; чтение данных будет происходить напрямую из бд (Presentation Layer -> Application Layer -> Infrastructure Layer), а таком случае, можно вообще отказаться от orm при чтении данных.
Если проект пишется с Anemic моделями, то он пишется либо в полу-процедурном стиле либо в функциональном. У процедурного стиля есть свои очевидные минусы. Проекты написанные на js/.net в функциональном стиле пока не видел.

В DDD, Application Layer можно писать в

можно конечно.

в CQRS стиле,

и в нем можно. и не в нем. и так можно, и сяк можно.

Компьютеру все равно, там в глубине все равно битовые операции.

чтение данных будет происходить напрямую из бд

а потом что мы с прочитанными данными делаем?
в этом и вопрос :)

при сохранении данные будут обрабатываться бизнес логикой из Domain слоя

а для того чтобы обработать бизнес-логикой — как правило надо прочитать дополнительные данные.
мы их прочитали напрямую из БД.
И? как будем — обрабатывать? где и по каким архитектурным принципам будет находиться этот код.
В этом же и вопрос.

А не в том что — хочется, берем и читаем напрямую. Конечно можно прочитать напрямую. ORM как правило слоеная штука, и можно заюзать его DBAL уровень. А можно и ниже, сам драйвер БД вытянуть и работать через него.

В самом чтении — какой прок?
Зачем, с какой целью мы прочитали, что мы собираемся делать с этим прочитанным — вот в чем вопрос.

Если проект пишется с Anemic моделями, то он пишется либо в полу-процедурном стиле либо в функциональном.

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

У процедурного стиля есть свои очевидные минусы

у всего есть минусы.

Проекты написанные на js/.net в функциональном стиле пока не видел

и вряд ли увидите.
функциональный стиль — не был и не станет мейнстримом.

Компьютеру все равно, там в глубине все равно битовые операции.

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

а для того чтобы обработать бизнес-логикой — как правило надо прочитать дополнительные данные.
мы их прочитали напрямую из БД.
И? как будем — обрабатывать? где и по каким архитектурным принципам будет находиться этот код.
В этом же и вопрос.

В CQRS подходе код разделяется на queries & commands. Query надо писать так что бы потом не понадобилось обрабатывать данные с бизнес логикой, иначе это будет command. У CQRS тоже есть свои ограничения, напр. плохо работает когда много сложной бизнес логики msdn.
Queries нужны что бы показать какие-то данные пользователю, напр. показать все сообщения которые отправил пользователь.
select * from messages ... where userid = @userid @userid подставляем из контекста http запроса, `@userid` мы получили когда пользователь залогинился и положили в сессию. Так что другой пользователь не сможет посмотреть чужие сообщения.
Валидацию пользовательских данных, логгирование можно навесить с помощью cross-cutting-concern. Как пример реализации CQRS+DDD мне нравится этот проект github (C#, .net)

В DDD можно оптимизировать код, вынесением логики напр. по доставанию части данных из «тяжелого» агрегат рута в бизнес сервис. Там надо находить баланс между чистыми моделями, производительностью и красотой код ссылка. Если заранее не известно какие какие части нужно оптимизировать, то лучше делать максимально большие агрегат руты, в рамках разумного, и по возможности бизнес логику писать в Value Object-ах, потом будет легче оптимизировать/рефакторить код.

функциональный стиль — не был и не станет мейнстримом.

Языки стремятся к тому что бы поддерживать разные парадигмы программирования. Функциональный подход — это часть ООП. В классах можно писать функции, которые не изменяют входные данные и это хороший тон написания кода. Такой код проще тестировать и поддерживать, нет зависимости от входных параметров. В принципе, все кроме доменной области(из DDD) и кода который взаимодействует с вводом/выводом данных, можно легко писать в функциональном стиле. Но, напр., в C# это пока еще сложно реализовать, так что бы код написанный в функциональном стиле так же легко читался и так же быстро работал как в процедурном стиле, но с каждый релизом функциональных фишек в нем появляется все больше и больше.

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

приятно встретить — понимающего. (я без тролинга, и без подъё)
дальше просто не читал еще. но факт, хуева туча наших колег не понимает это. но да,ессно — декларирует. зазубрили....

В CQRS подходе код разделяется на queries & commands.

да. или «ну да». или, «а мужики то и не знали» :)

Query надо писать так что бы потом не понадобилось обрабатывать данные с бизнес логикой, иначе это будет command.

скидка такая-то дается купившему товару, в этом месяце на сумму Х, без учета акционных товаров и товаров купленных по акциям Y и W, но кроме товаров купленных по промо W, а также от поставщиков P и M (P и M это поставщики которые имели обороты за последние полгода не меньше A, или обороты за три месяца, хоть когда, не меньше B)

Queries нужны что бы показать какие-то данные пользователю

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

В DDD можно оптимизировать код

Извините, но DDD — сОООООвсем про другое. Если, конечно, вы поняли детский тролинг выше.
Если не поняли — то да, про — «оптимизацию кода»

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

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

НЕТ. ЯП стремятся к — «скорости разработки». И даже Haskell — об том же. На задачах которые со всей очевидностью быстрее получится реализовыть — в фукнциональной парадигме.

Такой код проще тестировать и поддерживать,

Проблемы индейцев шерифа не волнуют

это хороший тон написания кода.

Энциклика, или хотя бы булла Папы Римского есть? ну, про хороший тон? Пес с ним, как его звать, папу этого.
или какая-то Опра интервью взя0ла, а потом начались бития головой оп пол о королекской семье?

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

в нем появляется все больше и больше.

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

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

Если не в курсе, локальных проблем
.... .... Последние дни все не на шутку взбудоражены данными образовательного теста PISA, который в этом году впервые проводился в Украине. Этот тест оценивает знания 15-летних школьников в чтении, математике и естественных науках. Полученные результаты потом сводятся вместе по всем странам участникам.
К удивлению многих, оказалось, что украинские школьники на фоне общемировых трендов находятся где-то ближе к плинтусу. Собственно, учителя и ученые знали об этом уже давным-давно, просто в коллективном бессознательном до сих пор очень силен миф о том, что наше школьное образование чуть ли не лучшее в мире. Может оно таким и было 30 лет назад, но это давно в прошлом. Сейчас мы находимся на уровне стран «второго мира» с устойчивой тенденцией к скатыванию в третий.
site.ua/...​vanie-analiz-dannyh-pisa

Если вы уверены что это только у нас. — вопрос только один — сколько часов вы учили детей-подростков азам программирования?
(подсказка — джунов обучение, в конкретных командах я и обозначил T/T. сорькаю за демагогию — ad hominem)

Ключеовое слово, вашего первого предложения — Peopleware. ну от де Марко.
Не нравится, ну ок:
Design and programming are human activities; forget that and all is lost.
Bjarne Stroustrup

да, контекст, это я почти каждую третью букву перебивал. пьян. пару часов с лушим другом по вайберу его ДР отмечали :) Он попросился — попрощаться. Ну, по паре областей Украины — главный по ойти. ну там, локальные магазины продажи, проблемы всякие с зависшими кассами, или хренью что от этих касс в ЦБ поступает.
а я, если чё, стейкхолдерам покрупнее отмажусь. Мне легче :)

хотите обсудить по трезвому? ;)

Толковая заметка, спасибо большое !

Вторая часть очевидно веселее первой. Спасибо написавшим!)

Подписаться на комментарии