День 1631. #TipsAndTricks #Frontend
Как Обнаружить Ненужную Отрисовку Элементов DOM в Веб-Приложении
Давненько я не писал ничего про фронтенд. А ведь я фуллстек разработчик всё-таки. Поэтому сегодня небольшой лайфхак про фронт.
Ненужные перерисовки в веб-приложении могут оказать критическое влияние на производительность. Обычно наши машины для разработки достаточно мощные, поэтому мы можем даже не замечать некоторых вещей, которые делает браузер. Не говоря уже про то, что зачастую наша база данных или API для разработки находятся на той же машине, и отвечают почти мгновенно.
Очень простой пример, когда при нажатии на кнопку мы должны отобразить какой-то список результатов. Это приложение на ангуляре, но суть не в этом. Каждый раз, когда пользователь нажимает на кнопку, совершается запрос на сервер, и выводится список результатов. Но что, если пользователь нажмёт на кнопку много раз? По странице этого, возможно, не будет заметно, но на самом деле, каждый раз будет отправляться новый запрос на сервер, и результаты будут перерисовываться (хотя это будут одни и те же результаты).
Как это исправить – отдельный разговор, и, наверное, решение будет своё в каждом случае. Но как это обнаружить?
На помощь придут инструменты разработчика Chrome (F12). Нажмите на гамбургер-меню (три вертикальные точки) > More tools (Другие инструменты) > Rendering (Отрисовка) > Paint flashing (Индикатор замены отображения).
Теперь каждая перерисовка элементов DOM на странице будет выделяться зелёным (заметьте, что это не работает на примере по ссылке выше, из-за эмуляции страницы результатов, но будет работать в реальном приложении). Браузер будет подсвечивать все перерисовки страницы, включая прокрутки, изменения при наведении мыши, анимации и т.п. Так вы можете легко определить элементы, которые перерисовываются, хотя не должны этого делать.
В разделе Rendering (Отрисовка) инструментов разработчика есть и другие полезные инструменты. В частности, мне пригодился Scrolling performance issues (Проблемы производительности при прокрутке).
Источник: https://dev.to/maxime1992/how-to-detect-unnecessary-renderings-of-dom-elements-in-your-web-app-to-improve-performances-13jd
Как Обнаружить Ненужную Отрисовку Элементов DOM в Веб-Приложении
Давненько я не писал ничего про фронтенд. А ведь я фуллстек разработчик всё-таки. Поэтому сегодня небольшой лайфхак про фронт.
Ненужные перерисовки в веб-приложении могут оказать критическое влияние на производительность. Обычно наши машины для разработки достаточно мощные, поэтому мы можем даже не замечать некоторых вещей, которые делает браузер. Не говоря уже про то, что зачастую наша база данных или API для разработки находятся на той же машине, и отвечают почти мгновенно.
Очень простой пример, когда при нажатии на кнопку мы должны отобразить какой-то список результатов. Это приложение на ангуляре, но суть не в этом. Каждый раз, когда пользователь нажимает на кнопку, совершается запрос на сервер, и выводится список результатов. Но что, если пользователь нажмёт на кнопку много раз? По странице этого, возможно, не будет заметно, но на самом деле, каждый раз будет отправляться новый запрос на сервер, и результаты будут перерисовываться (хотя это будут одни и те же результаты).
Как это исправить – отдельный разговор, и, наверное, решение будет своё в каждом случае. Но как это обнаружить?
На помощь придут инструменты разработчика Chrome (F12). Нажмите на гамбургер-меню (три вертикальные точки) > More tools (Другие инструменты) > Rendering (Отрисовка) > Paint flashing (Индикатор замены отображения).
Теперь каждая перерисовка элементов DOM на странице будет выделяться зелёным (заметьте, что это не работает на примере по ссылке выше, из-за эмуляции страницы результатов, но будет работать в реальном приложении). Браузер будет подсвечивать все перерисовки страницы, включая прокрутки, изменения при наведении мыши, анимации и т.п. Так вы можете легко определить элементы, которые перерисовываются, хотя не должны этого делать.
В разделе Rendering (Отрисовка) инструментов разработчика есть и другие полезные инструменты. В частности, мне пригодился Scrolling performance issues (Проблемы производительности при прокрутке).
Источник: https://dev.to/maxime1992/how-to-detect-unnecessary-renderings-of-dom-elements-in-your-web-app-to-improve-performances-13jd
👍10
День 1632. #ЗаметкиНаПолях
Разработка API для Людей.
Часть 3. Принципы разработки. Начало
Часть 1
Часть 2
API интересны тем, насколько они могут улучшить или сломать продукт. Если у вас потрясающий продукт, но неполный API, с которым сложно работать или который откровенно сбивает с толку, это означает, что подавляющее большинство ваших пользователей никогда не узнают, насколько хорош ваш продукт на самом деле. Если разработчик не может добиться существенного прогресса в течение первых 30 минут, вы можете потерять его навсегда. С другой стороны, посредственные продукты с отличным API, скорее всего, будут лучше. Причина довольно проста: API является частью продукта, поэтому мы должны относиться к нему с таким же вниманием и оттачивать до совершенства.
При проектировании API следует помнить о трёх вещах:
- Кто является целевой аудиторией?
- Насколько настраиваемым должен быть API?
- От какой доступности вы готовы отказаться ради настраиваемости?
1. Целевая аудитория
Как правило, API предназначены для опытных пользователей. Нужны ли вам новички? Можно сделать API слишком простым для взаимодействия и позволить пользователю уверенно принимать многие решения самостоятельно.
Если ваш продукт нишевый и используется только подмножеством разработчиков (например, только разработчиками корпоративных приложений на определённой платформе) полагаетесь ли вы на объектные модели и языковые конструкции, которые они используют?
2. Настраиваемость
Мощный API предоставляет множество возможностей конечным пользователям, например, возможность настраивать различные факторы запросов. Это происходит в ущерб доступности, поскольку чем больше опций, тем сложнее становится API. Сложный API означает, что вы, скорее всего, столкнётесь с проблемами при попытке построить свою интеграцию: придётся принимать сложные решения, которые могут иметь последствия для будущих версий вашего проекта, возможно, не имея достаточного опыта.
Простые API предоставляют один маршрут «счастливого пути» и могут быть изучены за считанные минуты, но они ограничены. Мощные API, где пользователь имеет полный контроль над каждым аспектом, подстраивая API под свои нужды, сопряжены с крутой кривой обучения и уязвимы для любых будущих изменений в API. Важно быть где-то посередине, возможно, склоняясь к менее сложному API. В идеале «счастливый путь» — это путь интеграции, который охватывает почти все варианты использования ваших пользователей, предоставляя некоторое пространство для манёвра тем опытным пользователям, у которых есть специфические потребности.
3. От какой доступности отказаться ради настраиваемости?
Это во многом будет зависеть от вопроса, кто является целевой аудиторией. Вы можете превратить простой API в мощный и сложный, однако почти невозможно сделать обратное, не начав с нуля. Начните с простого, а затем наращивайте сложность, но старайтесь не загонять себя в угол, из которого потом будет трудно выбраться.
Окончание следует…
Источник: https://dev.to/stripe/designing-apis-for-humans-design-patterns-5847
Разработка API для Людей.
Часть 3. Принципы разработки. Начало
Часть 1
Часть 2
API интересны тем, насколько они могут улучшить или сломать продукт. Если у вас потрясающий продукт, но неполный API, с которым сложно работать или который откровенно сбивает с толку, это означает, что подавляющее большинство ваших пользователей никогда не узнают, насколько хорош ваш продукт на самом деле. Если разработчик не может добиться существенного прогресса в течение первых 30 минут, вы можете потерять его навсегда. С другой стороны, посредственные продукты с отличным API, скорее всего, будут лучше. Причина довольно проста: API является частью продукта, поэтому мы должны относиться к нему с таким же вниманием и оттачивать до совершенства.
При проектировании API следует помнить о трёх вещах:
- Кто является целевой аудиторией?
- Насколько настраиваемым должен быть API?
- От какой доступности вы готовы отказаться ради настраиваемости?
1. Целевая аудитория
Как правило, API предназначены для опытных пользователей. Нужны ли вам новички? Можно сделать API слишком простым для взаимодействия и позволить пользователю уверенно принимать многие решения самостоятельно.
Если ваш продукт нишевый и используется только подмножеством разработчиков (например, только разработчиками корпоративных приложений на определённой платформе) полагаетесь ли вы на объектные модели и языковые конструкции, которые они используют?
2. Настраиваемость
Мощный API предоставляет множество возможностей конечным пользователям, например, возможность настраивать различные факторы запросов. Это происходит в ущерб доступности, поскольку чем больше опций, тем сложнее становится API. Сложный API означает, что вы, скорее всего, столкнётесь с проблемами при попытке построить свою интеграцию: придётся принимать сложные решения, которые могут иметь последствия для будущих версий вашего проекта, возможно, не имея достаточного опыта.
Простые API предоставляют один маршрут «счастливого пути» и могут быть изучены за считанные минуты, но они ограничены. Мощные API, где пользователь имеет полный контроль над каждым аспектом, подстраивая API под свои нужды, сопряжены с крутой кривой обучения и уязвимы для любых будущих изменений в API. Важно быть где-то посередине, возможно, склоняясь к менее сложному API. В идеале «счастливый путь» — это путь интеграции, который охватывает почти все варианты использования ваших пользователей, предоставляя некоторое пространство для манёвра тем опытным пользователям, у которых есть специфические потребности.
3. От какой доступности отказаться ради настраиваемости?
Это во многом будет зависеть от вопроса, кто является целевой аудиторией. Вы можете превратить простой API в мощный и сложный, однако почти невозможно сделать обратное, не начав с нуля. Начните с простого, а затем наращивайте сложность, но старайтесь не загонять себя в угол, из которого потом будет трудно выбраться.
Окончание следует…
Источник: https://dev.to/stripe/designing-apis-for-humans-design-patterns-5847
👍5
День 1633. #ЗаметкиНаПолях
Разработка API для Людей.
Часть 3. Принципы разработки. Окончание
Начало
Часть 1
Часть 2
Раннее определение принципов разработки открывает ряд огромных преимуществ, которые помогут увеличить долговечность и скорость разработки вашего API.
1. Будьте последовательны
Наиболее распространёнными операциями для API являются запрос на получение ресурса (GET) и на создание или обновление ресурса (POST).
2. Будьте интуитивно понятны
Предположим, у нас есть объект ресурса Customer, который принимает 3 поля: имя, email и адрес. Сначала мы создаём клиента:
API преуспевают благодаря своей интуитивности. Операции, подобные описанным выше, должны «просто работать» на основе общего предположения, а не на склонности компьютера делать именно так, как ему было сказано.
Источник: https://dev.to/stripe/designing-apis-for-humans-design-patterns-5847
Разработка API для Людей.
Часть 3. Принципы разработки. Окончание
Начало
Часть 1
Часть 2
Раннее определение принципов разработки открывает ряд огромных преимуществ, которые помогут увеличить долговечность и скорость разработки вашего API.
1. Будьте последовательны
Наиболее распространёнными операциями для API являются запрос на получение ресурса (GET) и на создание или обновление ресурса (POST).
// Создание продуктаВ примере выше маршрут для получения продукта позволяет получать только один продукт за раз. Это довольно неудобно. Добавление конечной точки для получения списка продуктов предоставит пользователю гибкость и возможность сократить количество запросов к API:
POST /v1/products
// Обновление продукта
POST /v1/products/:id
// Получение продукта
GET /v1/products/:id
// Получаем список продуктовТо же касается других ресурсов, таких как клиенты (Customers). Как разработчик, вы хотите, чтобы ваш API был максимально предсказуемым, позволяя пользователям угадывать, какая комбинация HTTP-команд и конечных точек сработает, основываясь на предыдущем опыте работы. Будьте строго последовательны: новые маршруты должны работать во многом так же, как и существующие. Это не только позволит пользователям быстрее освоить API, но и даст ощущение хорошо продуманного, интуитивно понятного интерфейса.
GET /v1/products
2. Будьте интуитивно понятны
Предположим, у нас есть объект ресурса Customer, который принимает 3 поля: имя, email и адрес. Сначала мы создаём клиента:
// ЗапросЗатем мы решили обновить клиента:
POST /v1/customers
{ "name": "John Watson",
"email": "[email protected]",
"address": "221B Baker Street" }
// Ответ
{
"id": "cus_123",
"object": "customer",
"name": "John Watson",
"email": "[email protected]",
"address": "221B Baker Street"
}
// ЗапросВ этом запросе мы просто обновляем существующий адреса клиента. Но куда делись значения для имени и email? API сделал именно то, что ему было сказано. Он обновил значение адреса, но, поскольку значения имени и email отсутствовали в запросе на обновление, он интерпретировал их отсутствие как запрос на обнуление. Формально это правильно, но является чётким показателем того, что этот API не был разработан для людей. Вместо того, чтобы проверять, был ли передан параметр, разработчики этого API обновляют объект значением атрибута, независимо от того, был он предоставлен или нет.
POST /v1/customers/cus_123
{ "address": "London SW1A 1AA" }
// Ответ
{
"id": "cus_123",
"object": "customer",
"name": undefined,
"email": undefined,
"address": "London SW1A 1AA"
}
API преуспевают благодаря своей интуитивности. Операции, подобные описанным выше, должны «просто работать» на основе общего предположения, а не на склонности компьютера делать именно так, как ему было сказано.
Источник: https://dev.to/stripe/designing-apis-for-humans-design-patterns-5847
👍13
День 1635. #ЧтоНовенького
Метрики в .NET 8
Метрики — это измерения, которые собираются с течением времени и чаще всего используются для мониторинга работоспособности приложения и создания предупреждений. Например, счётчик, сообщающий о неудачных HTTP-запросах, может отображаться на информационных панелях или генерировать оповещения, когда количество сбоев превышает пороговое значение.
В превью .NET 8 в ASP.NET Core добавлены:
- новые виды измерений со счетчиками, датчиками и гистограммами;
- мощные отчёты с многомерными значениями;
- интеграция в более широкую облачную экосистему за счёт соответствия стандартам OpenTelemetry;
- добавлены метрики для хостинга ASP.NET Core, Kestrel и SignalR.
Также ASP.NET Core теперь использует API IMeterFactory для создания и получения счётчиков. Это позволяет использовать внедрение зависимостей, что упрощает изоляцию и сбор метрик. IMeterFactory особенно полезен для юнит-тестов, когда несколько тестов могут выполняться параллельно и собирать данные только для своего теста:
-
-
-
- Различные новые счётчики промежуточного ПО ограничений позволяют отслеживать количество прошедших запросов, запросов в очереди, продолжительность очереди и многое другое.
Если вам интересно попробовать метрики, разработчики .NET собрали панели мониторинга Grafana, которые сообщают о метриках ASP.NET Core, собранных Prometheus в репозитории aspnetcore-grafana
Источники:
- https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-6/
- https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-4/
Метрики в .NET 8
Метрики — это измерения, которые собираются с течением времени и чаще всего используются для мониторинга работоспособности приложения и создания предупреждений. Например, счётчик, сообщающий о неудачных HTTP-запросах, может отображаться на информационных панелях или генерировать оповещения, когда количество сбоев превышает пороговое значение.
В превью .NET 8 в ASP.NET Core добавлены:
- новые виды измерений со счетчиками, датчиками и гистограммами;
- мощные отчёты с многомерными значениями;
- интеграция в более широкую облачную экосистему за счёт соответствия стандартам OpenTelemetry;
- добавлены метрики для хостинга ASP.NET Core, Kestrel и SignalR.
Также ASP.NET Core теперь использует API IMeterFactory для создания и получения счётчиков. Это позволяет использовать внедрение зависимостей, что упрощает изоляцию и сбор метрик. IMeterFactory особенно полезен для юнит-тестов, когда несколько тестов могут выполняться параллельно и собирать данные только для своего теста:
public class BasicTests :Новые счетчики в превью 6
IClassFixture<WebApplicationFactory<Program>>
{
private WebApplicationFactory<Program> factory;
public BasicTests(
WebApplicationFactory<Program> f)
=> factory = f;
[Fact]
public async Task GetRequest()
{
var mf = factory.Services
.GetRequiredService<IMeterFactory>();
var col = new MetricCollector<double>(
mf,
"Microsoft.AspNetCore.Hosting",
"http-server-request-duration"
);
var cl = factory.CreateClient();
var resp = await cl.GetAsync("/");
Assert.Equal("Hello World!",
await resp.Content.ReadAsStringAsync());
await col.WaitForMeasurementsAsync(
minCount: 1);
Assert.Collection(col.GetMeasurementSnapshot(),
m => {
Assert.Equal("http", m.Tags["scheme"]);
Assert.Equal("GET", m.Tags["method"]);
Assert.Equal("/", m.Tags["route"]);
});
}
}
-
routing-match-success
и routing-match-failure
сообщают, как запрос был перенаправлен в приложении. Если запрос успешно соответствует маршруту, счётчик включает информацию о шаблоне маршрута и о том, был ли это резервный маршрут.-
diagnostics-handler-exception
добавляется к ПО обработки исключений и сообщает, как оно обрабатывает необработанные исключения, включая информацию об имени исключения и результате обработки.-
http-server-unhandled-requests
— сообщает, когда HTTP-запрос достигает конца конвейера промежуточного ПО и не обрабатывается приложением.- Различные новые счётчики промежуточного ПО ограничений позволяют отслеживать количество прошедших запросов, запросов в очереди, продолжительность очереди и многое другое.
Если вам интересно попробовать метрики, разработчики .NET собрали панели мониторинга Grafana, которые сообщают о метриках ASP.NET Core, собранных Prometheus в репозитории aspnetcore-grafana
Источники:
- https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-6/
- https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-4/
👍14
День 1636. #DDD
Использование Доменных Событий для Построения Слабосвязанных Систем. Начало
В программной инженерии «связанность» означает, насколько разные части программной системы зависят друг от друга. Если они тесно связаны, изменения в одной части могут повлиять на другие. Если слабо, изменения в одной части не вызовут больших проблем в остальной системе. Доменные события — это тактический шаблон DDD, который можно использовать для создания слабосвязанных систем.
Событие представляет собой произошедший в домене факт. Другие компоненты в системе могут подписаться на это событие и соответствующим образом его обработать. События домена позволяют явно выражать побочные эффекты и обеспечивают лучшее разделение проблем в домене. Это идеальный способ вызвать побочные эффекты для нескольких агрегатов внутри домена.
Вы несёте ответственность за то, чтобы публикация события предметной области была транзакционной (об этом позже).
Особенности
- Неизменяемость — доменные события являются фактами и должны быть неизменными.
- Толстые и Тонкие события — сколько информации вам нужно передавать?
- Используйте прошедшее время для именования событий.
Доменные события и интеграционные события
Семантически это одно и то же: представление чего-то, что произошло в прошлом. Однако намерения у них разные.
События домена:
- публикуются и используется в пределах одного домена;
- отправляются с помощью шины сообщений в памяти;
- могут обрабатываться синхронно или асинхронно.
Интеграционные события:
- потребляются другими подсистемами (микросервисы, ограниченные контексты);
- отправляются брокером сообщений через очередь;
- обрабатываются только асинхронно.
Доменные события можно использовать для создания интеграционных событий, выходящих за пределы домена.
Реализация
Реализовать доменные события можно через создание абстракции IDomainEvent и реализацию INotification в MediatR. Так вы можете использовать поддержку публикации-подписки в MediatR для публикации уведомления одному или нескольким обработчикам.
Только сущности могут вызывать доменные события, поэтому создадим базовый класс Entity, сделав метод RaiseDomainEvent защищённым. Доменные события хранятся во внутренней коллекции, чтобы никто не мог получить к ним доступ. GetDomainEvents предназначен для получения моментального снимка коллекции, ClearDomainEvents — для очистки.
Источник: https://www.milanjovanovic.tech/blog/using-domain-events-to-build-loosely-coupled-systems
Использование Доменных Событий для Построения Слабосвязанных Систем. Начало
В программной инженерии «связанность» означает, насколько разные части программной системы зависят друг от друга. Если они тесно связаны, изменения в одной части могут повлиять на другие. Если слабо, изменения в одной части не вызовут больших проблем в остальной системе. Доменные события — это тактический шаблон DDD, который можно использовать для создания слабосвязанных систем.
Событие представляет собой произошедший в домене факт. Другие компоненты в системе могут подписаться на это событие и соответствующим образом его обработать. События домена позволяют явно выражать побочные эффекты и обеспечивают лучшее разделение проблем в домене. Это идеальный способ вызвать побочные эффекты для нескольких агрегатов внутри домена.
Вы несёте ответственность за то, чтобы публикация события предметной области была транзакционной (об этом позже).
Особенности
- Неизменяемость — доменные события являются фактами и должны быть неизменными.
- Толстые и Тонкие события — сколько информации вам нужно передавать?
- Используйте прошедшее время для именования событий.
Доменные события и интеграционные события
Семантически это одно и то же: представление чего-то, что произошло в прошлом. Однако намерения у них разные.
События домена:
- публикуются и используется в пределах одного домена;
- отправляются с помощью шины сообщений в памяти;
- могут обрабатываться синхронно или асинхронно.
Интеграционные события:
- потребляются другими подсистемами (микросервисы, ограниченные контексты);
- отправляются брокером сообщений через очередь;
- обрабатываются только асинхронно.
Доменные события можно использовать для создания интеграционных событий, выходящих за пределы домена.
Реализация
Реализовать доменные события можно через создание абстракции IDomainEvent и реализацию INotification в MediatR. Так вы можете использовать поддержку публикации-подписки в MediatR для публикации уведомления одному или нескольким обработчикам.
using MediatR;Вызов
public interface IDomainEvent : INotification
{
}
public class CourseCompleted : IDomainEvent
{
public Guid CourseId { get; init; }
}
Только сущности могут вызывать доменные события, поэтому создадим базовый класс Entity, сделав метод RaiseDomainEvent защищённым. Доменные события хранятся во внутренней коллекции, чтобы никто не мог получить к ним доступ. GetDomainEvents предназначен для получения моментального снимка коллекции, ClearDomainEvents — для очистки.
public abstract class Entity : IEntityТеперь сущности могут наследовать от Entity и вызывать доменные события:
{
private readonly List<IDomainEvent>
_events = new();
public IReadOnlyList<IDomainEvent>
GetDomainEvents() => _events.ToList();
public void ClearDomainEvents()
=> _events.Clear();
protected void RaiseDomainEvent(
IDomainEvent domainEvent)
=> _events.Add(domainEvent);
}
public class Course : EntityОкончание следует…
{
public Guid Id { get; private set; }
public CourseStatus Status { get; private set; }
public DateTime? Completed { get; private set; }
public void Complete()
{
Status = CourseStatus.Completed;
Completed = DateTime.UtcNow;
RaiseDomainEvent(
new CourseCompleted {
CourseId = this.Id
});
}
}
Источник: https://www.milanjovanovic.tech/blog/using-domain-events-to-build-loosely-coupled-systems
👍16
День 1637. #DDD
Использование Доменных Событий для Построения Слабосвязанных Систем. Окончание
Начало
Публикация с помощью EF Core
Поскольку EF Core использует паттерн Единица Работы, вы можете использовать его для сбора всех доменных событий в текущей транзакции и их публикации.
Можно либо переопределить метод SaveChangesAsync, либо использовать перехватчик:
До:
- события являются частью той же транзакции,
- немедленная согласованность данных.
После:
- события – отдельная транзакция,
- конечная согласованность, т.к. сообщения обрабатываются после исходной транзакции,
- риск несогласованности БД, т.к. обработка события может привести к сбою.
Можно решить эту проблему с помощью паттерна исходящих сообщений (Outbox), когда изменения в БД и изменения, сделанные в доменном событии сохраняются (в виде исходящих сообщений) в одной транзакции.
Метода PublishDomainEventsAsync:
Нужно определить класс, реализующий INotificationHandler<T>, где T – тип доменного события. Обработчик доменного события CourseCompleted ниже публикует CourseCompletedIntegrationEvent для уведомления других систем.
- Доменные события могут помочь построить слабосвязанную систему, отделяя основную логику домена от побочных эффектов, которые можно обрабатывать асинхронно.
- Для реализации доменных событий, можно использовать библиотеки EF Core и MediatR.
- Нужно решить, когда публиковать доменные события: до или после сохранения изменений в БД.
- Публикация доменных событий после сохранения изменений в БД и паттерн Outbox для добавления транзакционных гарантий обеспечивают окончательную согласованность, но также бОльшую надёжность.
Источник: https://www.milanjovanovic.tech/blog/using-domain-events-to-build-loosely-coupled-systems
Использование Доменных Событий для Построения Слабосвязанных Систем. Окончание
Начало
Публикация с помощью EF Core
Поскольку EF Core использует паттерн Единица Работы, вы можете использовать его для сбора всех доменных событий в текущей транзакции и их публикации.
Можно либо переопределить метод SaveChangesAsync, либо использовать перехватчик:
public class ApplicationDbContext : DbContextВажное решение: публиковать доменные события до или после вызова SaveChangesAsync (сохранения данных в БД)?
{
public override async Task<int> SaveChangesAsync(
CancellationToken ct = default)
{
var result = await
base.SaveChangesAsync(ct);
await PublishDomainEventsAsync();
return result;
}
}
До:
- события являются частью той же транзакции,
- немедленная согласованность данных.
После:
- события – отдельная транзакция,
- конечная согласованность, т.к. сообщения обрабатываются после исходной транзакции,
- риск несогласованности БД, т.к. обработка события может привести к сбою.
Можно решить эту проблему с помощью паттерна исходящих сообщений (Outbox), когда изменения в БД и изменения, сделанные в доменном событии сохраняются (в виде исходящих сообщений) в одной транзакции.
Метода PublishDomainEventsAsync:
private async Task PublishDomainEventsAsync()Обработка
{
var events = ChangeTracker
.Entries<Entity>()
.Select(e => e.Entity)
.SelectMany(ent =>
{
var evnts = ent.GetDomainEvents();
ent.ClearDomainEvents();
return evnts;
})
.ToList();
foreach (var ev in events)
await _mediator.Publish(ev);
}
Нужно определить класс, реализующий INotificationHandler<T>, где T – тип доменного события. Обработчик доменного события CourseCompleted ниже публикует CourseCompletedIntegrationEvent для уведомления других систем.
public class CourseCompletedDomainEventHandlerИтого
: INotificationHandler<CourseCompleted>
{
private readonly IBus _bus;
public CourseCompletedDomainEventHandler(IBus bus)
{
_bus = bus;
}
public async Task Handle(
CourseCompleted de,
CancellationToken ct)
{
await _bus.Publish(
new CourseCompletedIntegrationEvent(de.CourseId),
ct);
}
}
- Доменные события могут помочь построить слабосвязанную систему, отделяя основную логику домена от побочных эффектов, которые можно обрабатывать асинхронно.
- Для реализации доменных событий, можно использовать библиотеки EF Core и MediatR.
- Нужно решить, когда публиковать доменные события: до или после сохранения изменений в БД.
- Публикация доменных событий после сохранения изменений в БД и паттерн Outbox для добавления транзакционных гарантий обеспечивают окончательную согласованность, но также бОльшую надёжность.
Источник: https://www.milanjovanovic.tech/blog/using-domain-events-to-build-loosely-coupled-systems
👍12
День 1638. #PostgresTips
Советы по Postgres для Начинающих
Давно я не писал ничего про базы данных. А тут наткнулся на советы по PostgreSQL и решил пройтись по ним, а заодно повторить для себя некоторые моменты, поскольку скоро мне предстоит переход на эту СУБД. Это будет серия постов с тегом #PostgresTips, которые периодически будут появляться на канале.
1. Кортежи — это физические версии строк
Одним из основополагающих аспектов PostgreSQL, который удивляет многих новичков, является концепция кортежей. Попросту говоря, кортеж в Postgres — это физическая версия строки данных. Это означает, что при изменении данных в строке вместо изменения существующих данных Postgres добавляет новую версию этой строки, кортеж. Эта система управления версиями называется MVCC (Multiversion Concurrency Control), и важно понимать её для разработки высокопроизводительных систем.
Вот что происходит во время различных операций записи:
- Когда вы выполняете команду DELETE, она не сразу освобождает место на диске. Вместо этого старый кортеж помечается как мёртвый, но остаётся в БД до тех пор, пока VACUUM не удалит его. Если эти мёртвые кортежи накапливаются и удаляются при очистке больших объёмов, это приводит к раздуванию таблиц и индексов (они занимают гораздо больше места на диске, чем содержат данных).
- Точно так же, когда вы обновляете строку, Postgres не изменяет существующий кортеж. Вместо этого он создаёт новую версию этой строки (новый кортеж) и помечает старую как мёртвую.
- Даже отменённый INSERT создаёт мёртвый кортеж, что может удивить многих. Это означает, что, если вы попытаетесь вставить запись, а затем откатите это действие, кортеж, который должен был быть вставлен, помечается как мёртвый.
Чтобы помочь понять эти концепции, каждая таблица в Postgres имеет скрытые столбцы, которые вы можете посмотреть: ctid, xmin и xmax. ctid представляет расположение кортежа (номер страницы + смещение внутри неё), а xmin и xmax можно рассматривать как «дату (номер транзакции) рождения» и «дату смерти» для кортежей.
Поняв это поведение на раннем этапе, вы будете лучше подготовлены к решению проблем, связанных с дисковым пространством, раздуванием и процессами автоочистки, которые направлены на удаление этих мёртвых кортежей. Вот базовый пример, тривиальный, но очень важный:
Источник: https://postgres.ai/blog/20230722-10-postgres-tips-for-beginners
Советы по Postgres для Начинающих
Давно я не писал ничего про базы данных. А тут наткнулся на советы по PostgreSQL и решил пройтись по ним, а заодно повторить для себя некоторые моменты, поскольку скоро мне предстоит переход на эту СУБД. Это будет серия постов с тегом #PostgresTips, которые периодически будут появляться на канале.
1. Кортежи — это физические версии строк
Одним из основополагающих аспектов PostgreSQL, который удивляет многих новичков, является концепция кортежей. Попросту говоря, кортеж в Postgres — это физическая версия строки данных. Это означает, что при изменении данных в строке вместо изменения существующих данных Postgres добавляет новую версию этой строки, кортеж. Эта система управления версиями называется MVCC (Multiversion Concurrency Control), и важно понимать её для разработки высокопроизводительных систем.
Вот что происходит во время различных операций записи:
- Когда вы выполняете команду DELETE, она не сразу освобождает место на диске. Вместо этого старый кортеж помечается как мёртвый, но остаётся в БД до тех пор, пока VACUUM не удалит его. Если эти мёртвые кортежи накапливаются и удаляются при очистке больших объёмов, это приводит к раздуванию таблиц и индексов (они занимают гораздо больше места на диске, чем содержат данных).
- Точно так же, когда вы обновляете строку, Postgres не изменяет существующий кортеж. Вместо этого он создаёт новую версию этой строки (новый кортеж) и помечает старую как мёртвую.
- Даже отменённый INSERT создаёт мёртвый кортеж, что может удивить многих. Это означает, что, если вы попытаетесь вставить запись, а затем откатите это действие, кортеж, который должен был быть вставлен, помечается как мёртвый.
Чтобы помочь понять эти концепции, каждая таблица в Postgres имеет скрытые столбцы, которые вы можете посмотреть: ctid, xmin и xmax. ctid представляет расположение кортежа (номер страницы + смещение внутри неё), а xmin и xmax можно рассматривать как «дату (номер транзакции) рождения» и «дату смерти» для кортежей.
Поняв это поведение на раннем этапе, вы будете лучше подготовлены к решению проблем, связанных с дисковым пространством, раздуванием и процессами автоочистки, которые направлены на удаление этих мёртвых кортежей. Вот базовый пример, тривиальный, но очень важный:
pg=# create table t1 as select 1 as id;Мы создали таблицу с одной строкой, проверили расположение живого кортежа этой строки (ctid), а затем сделали обновление, которое логически ничего не делает, т.е. не меняет значение. Но местоположение изменилось с (0,1) (страница 0, смещение 1) на (0,2). Потому что физически Postgres создал новый кортеж — новую версию строки. Понимание этого поведения Postgres поможет вам проектировать системы, работающие более эффективно.
SELECT 1
pg=# select ctid, xmin, xmax, * from t1;
ctid | xmin | xmax | id
-------+-------+------+----
(0,1) | 47496 | 0 | 1
(1 row)
pg=# update t1 set id = id where id = 1;
UPDATE 1
pg=# select ctid, xmin, xmax, * from t1;
ctid | xmin | xmax | id
-------+-------+------+----
(0,2) | 47497 | 0 | 1
(1 row)
Источник: https://postgres.ai/blog/20230722-10-postgres-tips-for-beginners
👍26👎1
День 1639. #ЗаметкиНаПолях
Использовать Записи, Классы или Структуры
Классы и структуры были частью C#, начиная с версии 1.0, но совсем недавно были добавлены записи. Сегодня сравним, где что лучше использовать.
Классы
Классы - наиболее универсальная из доступных структур данных, но требует больше ресурсов. Это не значит, что классы — плохой выбор. Высоко оптимизированные классы предлагают широкий спектр функций, что делает их мощным инструментом для написания кода. Вот типичное определение класса:
- Могут наследовать поведение от других классов.
- Выделение памяти для класса происходит в куче, а не в стеке. Обычно это означает больше накладных расходов, чем при использовании типов-значений. - Классы являются изменяемыми, т.е. их данные могут измениться после создания экземпляра.
Структуры
Структуры лучше всего использовать для представления простых данных с небольшим поведением или без него. Вот типичная реализация структуры:
- Хорошая практика – рассматривать структуры как неизменяемые типы.
- Размещаются в стеке, поэтому выделение/освобождение их обычно намного дешевле, чем при использовании ссылочных типов.
- Не могут наследовать от других.
См. подробнее, когда использовать структуру
Записи
Записи — это облегчённые ссылочные типы с семантикой значений. Кроме того, компилятор «из коробки» предлагает некоторые функции, такие как равенство по значению и форматирование при выводе. Вот два способа определения записи:
- Записи по умолчанию - ссылочные типы.
- Можно определить конструкторы и свойства.
- Можно использовать наследование.
- Используются как небольшие неизменяемые структуры данных.
- в C# 10 появились структуры-записи (record struct), которые являются структурами, поддерживают все свойства записей, кроме наследования.
Запись стоит использовать, когда нужно инкапсулировать данные без сложного поведения (распространённый пример этого сценария — объекты передачи данных — DTO). Если структура данных будет выполнять сложное поведение и будет большим экземпляром, это признаки того, что нужно использовать класс. Кроме того, лучше выбрать класс, если понадобится изменение данных экземпляра после создания.
Источник: https://code-maze.com/csharp-should-we-use-records-classes-or-structs/
Использовать Записи, Классы или Структуры
Классы и структуры были частью C#, начиная с версии 1.0, но совсем недавно были добавлены записи. Сегодня сравним, где что лучше использовать.
Классы
Классы - наиболее универсальная из доступных структур данных, но требует больше ресурсов. Это не значит, что классы — плохой выбор. Высоко оптимизированные классы предлагают широкий спектр функций, что делает их мощным инструментом для написания кода. Вот типичное определение класса:
public class ApiClient : HttpClientОсобенности
{
private string _myField = "";
public string MyProperty { get; set; }
public ApiClient()
{
BaseAddress = new Uri("https://api.com");
}
public string MyMethod()
=> "hello";
}
- Могут наследовать поведение от других классов.
- Выделение памяти для класса происходит в куче, а не в стеке. Обычно это означает больше накладных расходов, чем при использовании типов-значений. - Классы являются изменяемыми, т.е. их данные могут измениться после создания экземпляра.
Структуры
Структуры лучше всего использовать для представления простых данных с небольшим поведением или без него. Вот типичная реализация структуры:
public struct GeoLocationОсобенности
{
private double _latitude;
private double _longitude;
public GeoLocation(double lat, double lng)
{
_latitude = lat;
_longitude = lng;
}
public override string ToString()
=> $"(lat: {_latitude}, lon:{_longitude})";
}
- Хорошая практика – рассматривать структуры как неизменяемые типы.
- Размещаются в стеке, поэтому выделение/освобождение их обычно намного дешевле, чем при использовании ссылочных типов.
- Не могут наследовать от других.
См. подробнее, когда использовать структуру
Записи
Записи — это облегчённые ссылочные типы с семантикой значений. Кроме того, компилятор «из коробки» предлагает некоторые функции, такие как равенство по значению и форматирование при выводе. Вот два способа определения записи:
// обычныйОсобенности
public record ApiData
{
public int Id { get; init; }
}
// с позиционными параметрами
public record ApiData(int Id);
- Записи по умолчанию - ссылочные типы.
- Можно определить конструкторы и свойства.
- Можно использовать наследование.
- Используются как небольшие неизменяемые структуры данных.
- в C# 10 появились структуры-записи (record struct), которые являются структурами, поддерживают все свойства записей, кроме наследования.
Запись стоит использовать, когда нужно инкапсулировать данные без сложного поведения (распространённый пример этого сценария — объекты передачи данных — DTO). Если структура данных будет выполнять сложное поведение и будет большим экземпляром, это признаки того, что нужно использовать класс. Кроме того, лучше выбрать класс, если понадобится изменение данных экземпляра после создания.
Источник: https://code-maze.com/csharp-should-we-use-records-classes-or-structs/
👍15
День 1640. #ЗаметкиНаПолях
Разработка API для Людей.
Часть 4. Шаблоны разработки. Начало
Часть 1
Часть 2
Часть 3
В этой части серии рассмотрим некоторые шаблоны проектирования, которые достаточно универсальны, чтобы быть полезными практически всем, кто участвует в проектировании API.
1. Язык
Называть вещи трудно. Проблема в том, что, как и в случае с именами переменных и функций, вы хотите, чтобы маршруты API, поля и типы были понятными, но лаконичными.
Используйте простой язык
Это очевидно, но на практике довольно сложно сделать и может привести к «эффекту велосипедного сарая». Постарайтесь выразить самую суть понятия и не бойтесь использовать словарь синонимов. Например, не путайте понятия пользователя и клиента. Пользователь напрямую использует ваш API, клиент (или конечный пользователь) – тот, кто покупает товары или услуги, которые может предлагать ваш пользователь через ваш API.
Избегайте жаргона
Не думайте, что ваш пользователь знает всё о вашей конкретной отрасли. Например, 16-значный номер кредитной карты называется основным номером счета (Primary Account Number, PAN). В финтех-кругах, люди говорят о PAN, DPAN и FPAN, поэтому поймут, если в платёжном API будет:
2. Структура
Используйте enum вместо bool
Представим, что у нас есть API для модели подписки. Мы хотим, чтобы пользователи могли определить, активна подписка или отменена. Кажется разумным определить:
Вместо этого проще сделать поле статуса с перечислением:
Используйте вложенные объекты для расширяемости
Пробуйте логически сгруппировать поля вместе. Это:
Окончание следует…
Источник: https://dev.to/stripe/common-design-patterns-at-stripe-1hb4
Разработка API для Людей.
Часть 4. Шаблоны разработки. Начало
Часть 1
Часть 2
Часть 3
В этой части серии рассмотрим некоторые шаблоны проектирования, которые достаточно универсальны, чтобы быть полезными практически всем, кто участвует в проектировании API.
1. Язык
Называть вещи трудно. Проблема в том, что, как и в случае с именами переменных и функций, вы хотите, чтобы маршруты API, поля и типы были понятными, но лаконичными.
Используйте простой язык
Это очевидно, но на практике довольно сложно сделать и может привести к «эффекту велосипедного сарая». Постарайтесь выразить самую суть понятия и не бойтесь использовать словарь синонимов. Например, не путайте понятия пользователя и клиента. Пользователь напрямую использует ваш API, клиент (или конечный пользователь) – тот, кто покупает товары или услуги, которые может предлагать ваш пользователь через ваш API.
Избегайте жаргона
Не думайте, что ваш пользователь знает всё о вашей конкретной отрасли. Например, 16-значный номер кредитной карты называется основным номером счета (Primary Account Number, PAN). В финтех-кругах, люди говорят о PAN, DPAN и FPAN, поэтому поймут, если в платёжном API будет:
card.pan = 4242424242424242;Но для более широкой аудитории всё же лучше подойдёт:
card.number = 4242424242424242;Это особенно важно, когда вы думаете о том, кто является аудиторией вашего API. Скорее всего, это разработчик, не знакомый с финансовыми терминами, поэтому лучше предположить, что люди не знакомы с жаргоном вашей отрасли.
2. Структура
Используйте enum вместо bool
Представим, что у нас есть API для модели подписки. Мы хотим, чтобы пользователи могли определить, активна подписка или отменена. Кажется разумным определить:
Subscription.canceled={true, false}Это сработает, но что если вам нужно будет добавить приостановку подписки? Т.е. мы делаем перерыв в приеме платежей, но подписка активна и не отменена. Придётся добавить новое поле:
Subscription.canceled={true, false}Теперь, чтобы увидеть фактический статус подписки, нам нужно смотреть на два поля. А что, если они оба true? Можно ли приостановить подписку, которая была отменена?
Subscription.paused={true, false}
Вместо этого проще сделать поле статуса с перечислением:
Subscription.status={"active", "canceled"}Тогда приостановку легко добавить, добавив значение перечисления:
Subscription.status={"active", "canceled", "paused"}Мы добавили функциональность, но сохранили сложность API на том же уровне, а также сделали его более описательным. Если мы когда-нибудь решим удалить функцию приостановки подписки, удалить значение перечисления всегда будет проще, чем удалить поле. Наверняка есть случаи, когда поле bool подойдёт лучше, но всегда рассматривайте возможность возникновения третьего варианта.
Используйте вложенные объекты для расширяемости
Пробуйте логически сгруппировать поля вместе. Это:
customer.address = {выглядит гораздо понятнее, чем:
line1: "Main Street 123",
city: "San Francisco",
postal_code: "12345"
};
customer.address_line1 = "Main street 123";Первый вариант значительно упрощает добавление дополнительного поля позже (например, поле страны, если вы решите расширить свой бизнес для зарубежных клиентов) и гарантирует, что имена полей не станут слишком длинными.
customer.address_city = "San Francisco";
customer.address_postal_code: "12345";
Окончание следует…
Источник: https://dev.to/stripe/common-design-patterns-at-stripe-1hb4
👍12
День 1641. #ЗаметкиНаПолях
Разработка API для Людей.
Часть 4. Шаблоны разработки. Окончание
Часть 1
Часть 2
Часть 3
Начало
Чтобы облегчить жизнь разработчикам, чётко укажите, что именно возвращается. Например, маршрут
Используйте систему разрешений
Допустим, для крупного клиента вы добавили новую функцию, чтобы они протестировали её в бета-версии. Новый маршрут не задокументирован, о нём никто не знает, поэтому можно не волноваться. Несколько недель спустя вы вносите изменения в функцию, о которых попросил крупный клиент… И получаете серию гневных писем от других пользователей, у которых всё сломалось. Оказывается, о вашем секретном маршруте узнали другие.
Теперь надо не только решать проблемы клиентов. Теперь «бета»-функция фактически выпущена, т.к. об изменениях в ней придётся сообщать всем.
Если вы хотите, чтобы закрытые API оставались закрытыми, убедитесь, что к ним нельзя получить доступ, без соответствующих разрешений. Самый простой способ — привязать систему разрешений к ключу API. Если ключ API не авторизован для использования маршрута, верните сообщение об ошибке со статусом 403.
Сделайте идентификаторы неугадываемыми
Если вы разрабатываете API, который возвращает объекты со связанными с ними идентификаторами, убедитесь, что эти их нельзя угадать или каким-либо иным образом реконструировать. Если идентификаторы просто последовательные, то в лучшем случае вы непреднамеренно выдаёте ненужную информацию о бизнесе, в худшем случае - сильно подрываете безопасность.
Например, после покупки на сайте я получил идентификатор подтверждения заказа «10». Я могу сделать два предположения:
- У вас не такой большой бизнес, как вы, вероятно, заявляете.
- Я потенциально могу получить информацию о 9 предыдущих заказах (и обо всех будущих), так как знаю их идентификаторы. Если указанный ниже маршрут не защищён системой разрешений, можно угадать идентификатор и возможно получить закрытую информацию о других ваших клиентах:
Источник: https://dev.to/stripe/common-design-patterns-at-stripe-1hb4
Разработка API для Людей.
Часть 4. Шаблоны разработки. Окончание
Часть 1
Часть 2
Часть 3
Начало
3. ОтветыВ большинстве случаев вызов API делается для получения или изменения данных. В последнем случае нормой является возврат изменённого ресурса. Например, если вы обновите email клиента, то в ответе вы ожидаете получить данные клиента с обновленным email.
Возвращайте тип объекта
Чтобы облегчить жизнь разработчикам, чётко укажите, что именно возвращается. Например, маршрут
/v1/customers/:customer/payment_methods/:payment_methodдолжен вернуть тип PaymentMethod для данного клиента. Это должно быть очевидно из маршрута, но на всякий случай, верните тип объекта в поле "object", чтобы избежать путаницы:
{Это очень помогает при поиске по логам или для добавления методов защитного программирования на клиенте:
"id": "pm_123",
"object": "payment_method",
"created": 1672217299,
"customer": "cus_123",
…
}
if (response.Data.Object != "payment_method")4. Безопасность
{
// не тот объект, который ожидался
return;
}
Используйте систему разрешений
Допустим, для крупного клиента вы добавили новую функцию, чтобы они протестировали её в бета-версии. Новый маршрут не задокументирован, о нём никто не знает, поэтому можно не волноваться. Несколько недель спустя вы вносите изменения в функцию, о которых попросил крупный клиент… И получаете серию гневных писем от других пользователей, у которых всё сломалось. Оказывается, о вашем секретном маршруте узнали другие.
Теперь надо не только решать проблемы клиентов. Теперь «бета»-функция фактически выпущена, т.к. об изменениях в ней придётся сообщать всем.
Если вы хотите, чтобы закрытые API оставались закрытыми, убедитесь, что к ним нельзя получить доступ, без соответствующих разрешений. Самый простой способ — привязать систему разрешений к ключу API. Если ключ API не авторизован для использования маршрута, верните сообщение об ошибке со статусом 403.
Сделайте идентификаторы неугадываемыми
Если вы разрабатываете API, который возвращает объекты со связанными с ними идентификаторами, убедитесь, что эти их нельзя угадать или каким-либо иным образом реконструировать. Если идентификаторы просто последовательные, то в лучшем случае вы непреднамеренно выдаёте ненужную информацию о бизнесе, в худшем случае - сильно подрываете безопасность.
Например, после покупки на сайте я получил идентификатор подтверждения заказа «10». Я могу сделать два предположения:
- У вас не такой большой бизнес, как вы, вероятно, заявляете.
- Я потенциально могу получить информацию о 9 предыдущих заказах (и обо всех будущих), так как знаю их идентификаторы. Если указанный ниже маршрут не защищён системой разрешений, можно угадать идентификатор и возможно получить закрытую информацию о других ваших клиентах:
https://api.example.com/v1/orders/9Делайте идентификаторы неугадываемыми, например, используя UUID. Он, по сути, представляет собой строку случайных чисел и букв, что означает, что невозможно угадать, как будет выглядеть следующий идентификатор, основываясь на том, который у вас есть. Вы теряете в удобстве (гораздо проще говорить о «заказе 42», чем о «заказе 123e4567-e89b-12d3-a456-426614174000»), но вы компенсируете это преимуществами безопасности. Не забудьте сделать его понятным для человека, добавив префиксы объектов.
Источник: https://dev.to/stripe/common-design-patterns-at-stripe-1hb4
👍10
День 1642.
Давайте сегодня обсудим. К этому меня подтолкнули рекомендации ютуба с «музыкой для программирования». Меня интересно, почему эта музыка всегда однотипная? С чего взялось убеждение, что лучше всего программируется под электро-техно-транс… вотэтовсё? Вкусы у всех разные, но почему-то «для программирования» всегда предлагается один жанр.
Что вы слушаете, когда пишете код?
Голосуйте ниже и пишите в комментариях.
Источник изображения: https://8tracks.com/explore/programming
Давайте сегодня обсудим. К этому меня подтолкнули рекомендации ютуба с «музыкой для программирования». Меня интересно, почему эта музыка всегда однотипная? С чего взялось убеждение, что лучше всего программируется под электро-техно-транс… вотэтовсё? Вкусы у всех разные, но почему-то «для программирования» всегда предлагается один жанр.
Что вы слушаете, когда пишете код?
Голосуйте ниже и пишите в комментариях.
Источник изображения: https://8tracks.com/explore/programming
👍6
Что вы слушаете, когда пишете код?
Anonymous Poll
39%
только в тишине
17%
радио (случайные треки под настроение)
10%
музыку «для программирования»
40%
плейлист любимых песен (для поднятия настроения)
22%
незнакомые песни или музыку без слов (чтобы не отвлекали)
9%
звуки природы/города/кафе/офиса и т.п.
14%
подкасты/фильмы/ТВ/сериалы (мне не мешает)
5%
другое (напишу в комментариях)
День 1643. #PostgresTips
Советы по Postgres для Начинающих
2. Делайте EXPLAIN ANALYZE всегда с BUFFERS
Понимание того, как работает запрос, имеет решающее значение для оптимизации его производительности. В PostgreSQL команда EXPLAIN является основным инструментом для достижения этой цели. Однако для более детального разбора следует использовать EXPLAIN (ANALYZE, BUFFERS). И вот почему:
- EXPLAIN сам по себе предоставляет план запроса, давая вам представление об операциях, которые Postgres намеревается использовать для выборки или изменения данных: последовательное сканирование, сканирование индекса, объединение, сортировку и многое другое. Эту команду следует использовать исключительно для проверки плана запроса без выполнения.
- EXPLAIN ANALYZE не только показывает запланированные операции, но также выполняет запрос и предоставляет фактическую статистику времени выполнения. Это позволяет сравнивать, например, предполагаемые количества строк на каждом этапе с фактическими количествами, помогая определить, где Postgres может делать неточные предположения. Также ANALYZE предоставляет информацию о времени выполнения каждого этапа.
- EXPLAIN (ANALYZE, BUFFERS) предоставляет информацию об использовании буфера — в частности, сколько блоков попало в пул буферов или считано в него из базового кеша или диска. Это даёт ценную информацию о том, насколько интенсивен по дисковому чтению/записи ваш запрос, что может серьёзно сказаться на времени его выполнения. А если у вас задан параметр
3. Оптимальный выбор UI-инструментов: помимо pgAdmin
При изучении Postgres одним из первых вопросов, с которым вы столкнётесь, будет выбор клиента или IDE. Многие новички начинают с pgAdmin из-за его популярности и доступности. Но доступны более мощные и универсальные инструменты.
Одним из самых мощных клиентов для PostgreSQL является встроенный инструмент командной строки psql. Он содержит функции, обеспечивающие эффективное взаимодействие с базой данных, и вы найдёте его практически в любой системе, где установлен PostgreSQL.
Если вы больше склоняетесь к графическим IDE, есть несколько, которые предлагают баланс между удобством для пользователя и расширенными возможностями: DBeaver, DataGrip от JetBrains и Postico предоставляют сложные UI-интерфейсы с поддержкой выполнения запросов, визуализации данных и многого другого.
Однако независимо от того, какой графический инструмент вы выберете, потратить некоторое время на изучение всех тонкостей psql может оказаться невероятно полезным и обязательно окупится.
Источник: https://postgres.ai/blog/20230722-10-postgres-tips-for-beginners
Советы по Postgres для Начинающих
2. Делайте EXPLAIN ANALYZE всегда с BUFFERS
Понимание того, как работает запрос, имеет решающее значение для оптимизации его производительности. В PostgreSQL команда EXPLAIN является основным инструментом для достижения этой цели. Однако для более детального разбора следует использовать EXPLAIN (ANALYZE, BUFFERS). И вот почему:
- EXPLAIN сам по себе предоставляет план запроса, давая вам представление об операциях, которые Postgres намеревается использовать для выборки или изменения данных: последовательное сканирование, сканирование индекса, объединение, сортировку и многое другое. Эту команду следует использовать исключительно для проверки плана запроса без выполнения.
- EXPLAIN ANALYZE не только показывает запланированные операции, но также выполняет запрос и предоставляет фактическую статистику времени выполнения. Это позволяет сравнивать, например, предполагаемые количества строк на каждом этапе с фактическими количествами, помогая определить, где Postgres может делать неточные предположения. Также ANALYZE предоставляет информацию о времени выполнения каждого этапа.
- EXPLAIN (ANALYZE, BUFFERS) предоставляет информацию об использовании буфера — в частности, сколько блоков попало в пул буферов или считано в него из базового кеша или диска. Это даёт ценную информацию о том, насколько интенсивен по дисковому чтению/записи ваш запрос, что может серьёзно сказаться на времени его выполнения. А если у вас задан параметр
track_io_timing = on
, вы получите информацию о времени выполнения для всех операций ввода-вывода.3. Оптимальный выбор UI-инструментов: помимо pgAdmin
При изучении Postgres одним из первых вопросов, с которым вы столкнётесь, будет выбор клиента или IDE. Многие новички начинают с pgAdmin из-за его популярности и доступности. Но доступны более мощные и универсальные инструменты.
Одним из самых мощных клиентов для PostgreSQL является встроенный инструмент командной строки psql. Он содержит функции, обеспечивающие эффективное взаимодействие с базой данных, и вы найдёте его практически в любой системе, где установлен PostgreSQL.
Если вы больше склоняетесь к графическим IDE, есть несколько, которые предлагают баланс между удобством для пользователя и расширенными возможностями: DBeaver, DataGrip от JetBrains и Postico предоставляют сложные UI-интерфейсы с поддержкой выполнения запросов, визуализации данных и многого другого.
Однако независимо от того, какой графический инструмент вы выберете, потратить некоторое время на изучение всех тонкостей psql может оказаться невероятно полезным и обязательно окупится.
Источник: https://postgres.ai/blog/20230722-10-postgres-tips-for-beginners
👍14
День 1644. #ЧтоНовенького
JetBrains Представили Предиктивную Отладку
Меня иногда спрашивают, почему я рассказываю о новинках или инструментах Visual Studio, но редко пишу про Rider или ReSharper. Ответ прост: «Я пользуюсь Visual Studio и не могу достоверно оценить инструменты от JetBrains, поэтому не хочу писать о том, чего сам не пробовал». Однако об этой новинке очень захотелось рассказать, потому что, на мой взгляд, это очень круто.
С введением инструментов отладки разработчики ПО получили возможность интерактивно исследовать поток исполнения программ для поиска ошибок в реальных средах. Эти инструменты постоянно совершенствовались по мере того, как росла сложность создаваемого ПО. Тем не менее, разработчики часто оказываются в ситуации пошаговой отладки с множеством перезапусков и разбросанными по коду точками останова. Теперь в ReSharper 2023.2 появился предиктивный отладчик (в Rider появится позднее). Его можно включить в меню Tools > Debugger > Editor Integration > Predictive Debugger (Инструменты > Отладчик > Интеграция с Редактором > Предиктивный Отладчик), отметив флажок Show predicted values (beta) (Показывать прогнозируемые значения (бета)).
Простой пример предиктивной отладки (см. анимацию ниже)
Цвета дают представление о том, что происходит:
- Выражения, выделенные зелёным или красным цветом, означают, что выражение было оценено как истинное или ложное соответственно.
- Операторы, выделенные серым цветом, указывают на то, что этот путь кода не будет выполняться (подобно мертвому коду).
- Значения в конце строк, выделенные синим цветом, показывают прогнозируемые значения после выполнения соответствующего оператора.
- Жёлтые или красные подсказки показывают, где заканчивается предсказание; например, когда метод возвращает значение, выдаётся исключение (перехваченное или не перехваченное) или программа останавливается (Environment.Exit).
Прогноз также может заканчиваться вызовом функции, которую отладчик не решается оценить. Поскольку во время предиктивной отладки код исполняется, отладчик должен быть уверен, что он вызывает только чистые функции и не приводит к изменению состояния приложения. В анимации ниже такая функция int.TryParse (изменяет состояние локальной переменной), поэтому при отладке вам будет предложено явно разрешить выполнение оценки таких функций.
Также предполагается использование аннотаций (например, из JetBrains.Annotations) для тонкой настройки.
Будущие улучшения и ограничения
Планируется использовать внешние аннотации, чтобы пометить код, который будет разрешено оценивать, чтобы уменьшить количество возможных прерываний из-за нечистых вычислений. Например, объявить File.Exists или int.TryParse «чистыми» функциями.
К сожалению, код async/await не поддерживается, поскольку отладчик не допускает многопоточных вычислений.
Источник: https://blog.jetbrains.com/dotnet/2023/07/27/introducing-predictive-debugging-a-game-changing-look-into-the-future/
JetBrains Представили Предиктивную Отладку
Меня иногда спрашивают, почему я рассказываю о новинках или инструментах Visual Studio, но редко пишу про Rider или ReSharper. Ответ прост: «Я пользуюсь Visual Studio и не могу достоверно оценить инструменты от JetBrains, поэтому не хочу писать о том, чего сам не пробовал». Однако об этой новинке очень захотелось рассказать, потому что, на мой взгляд, это очень круто.
С введением инструментов отладки разработчики ПО получили возможность интерактивно исследовать поток исполнения программ для поиска ошибок в реальных средах. Эти инструменты постоянно совершенствовались по мере того, как росла сложность создаваемого ПО. Тем не менее, разработчики часто оказываются в ситуации пошаговой отладки с множеством перезапусков и разбросанными по коду точками останова. Теперь в ReSharper 2023.2 появился предиктивный отладчик (в Rider появится позднее). Его можно включить в меню Tools > Debugger > Editor Integration > Predictive Debugger (Инструменты > Отладчик > Интеграция с Редактором > Предиктивный Отладчик), отметив флажок Show predicted values (beta) (Показывать прогнозируемые значения (бета)).
Простой пример предиктивной отладки (см. анимацию ниже)
Цвета дают представление о том, что происходит:
- Выражения, выделенные зелёным или красным цветом, означают, что выражение было оценено как истинное или ложное соответственно.
- Операторы, выделенные серым цветом, указывают на то, что этот путь кода не будет выполняться (подобно мертвому коду).
- Значения в конце строк, выделенные синим цветом, показывают прогнозируемые значения после выполнения соответствующего оператора.
- Жёлтые или красные подсказки показывают, где заканчивается предсказание; например, когда метод возвращает значение, выдаётся исключение (перехваченное или не перехваченное) или программа останавливается (Environment.Exit).
Прогноз также может заканчиваться вызовом функции, которую отладчик не решается оценить. Поскольку во время предиктивной отладки код исполняется, отладчик должен быть уверен, что он вызывает только чистые функции и не приводит к изменению состояния приложения. В анимации ниже такая функция int.TryParse (изменяет состояние локальной переменной), поэтому при отладке вам будет предложено явно разрешить выполнение оценки таких функций.
Также предполагается использование аннотаций (например, из JetBrains.Annotations) для тонкой настройки.
Будущие улучшения и ограничения
Планируется использовать внешние аннотации, чтобы пометить код, который будет разрешено оценивать, чтобы уменьшить количество возможных прерываний из-за нечистых вычислений. Например, объявить File.Exists или int.TryParse «чистыми» функциями.
К сожалению, код async/await не поддерживается, поскольку отладчик не допускает многопоточных вычислений.
Источник: https://blog.jetbrains.com/dotnet/2023/07/27/introducing-predictive-debugging-a-game-changing-look-into-the-future/
👍10
День 1645. #ЗаметкиНаПолях
Scrutor в .NET
Сегодня рассмотрим библиотеку Scrutor, варианты её использования и что она предлагает для упрощения внедрения зависимостей, а также о реализации сквозных задач с использованием шаблона декоратора.
Пакет Scrutor улучшает код внедрения зависимостей, предоставляя набор расширений для встроенного контейнера зависимостей .NET, которые добавляют поддержку сканирования готовых сборок и декорирования типов.
Использование Scrutor для сканирования сборки
Scrutor помогает упростить наш код внедрения зависимостей за счёт динамического поиска типов внутри сборок и их регистрации во время выполнения. Используя сканирование сборки, мы можем частично автоматизировать наш код регистрации зависимостей. Кроме того, мы можем использовать сканирование сборки для создания расширяемых систем. С помощью сканирования мы можем создать программу, способную находить и загружать дополнительные модули во время выполнения.
Используя широкий спектр методов расширения, которые предоставляет Scrutor, мы можем очень точно определить сервисы, которые мы хотим зарегистрировать, например, из другой сборки (библиотеки классов):
Работа с обобщёнными типами
Если мы хотим добавить реализацию обобщённого типа, то вместо обобщённой реализации метода AssignableTo, мы можем использовать метод с аргументом типа:
Мы можем использовать спецификаторы времени жизни зависимостей:
Обработка нескольких реализаций
Стратегия регистрации — это то, как Scrutor обрабатывает случаи, когда для одного и того же интерфейса существует несколько реализаций:
- RegistrationStrategy.Append - добавляет новую регистрацию для существующих сервисов.
- RegistrationStrategy.Throw - выдаёт ошибку при попытке зарегистрировать существующий сервис.
Окончание следует…
Источник: https://code-maze.com/dotnet-dependency-injection-with-scrutor/
Scrutor в .NET
Сегодня рассмотрим библиотеку Scrutor, варианты её использования и что она предлагает для упрощения внедрения зависимостей, а также о реализации сквозных задач с использованием шаблона декоратора.
Пакет Scrutor улучшает код внедрения зависимостей, предоставляя набор расширений для встроенного контейнера зависимостей .NET, которые добавляют поддержку сканирования готовых сборок и декорирования типов.
Использование Scrutor для сканирования сборки
Scrutor помогает упростить наш код внедрения зависимостей за счёт динамического поиска типов внутри сборок и их регистрации во время выполнения. Используя сканирование сборки, мы можем частично автоматизировать наш код регистрации зависимостей. Кроме того, мы можем использовать сканирование сборки для создания расширяемых систем. С помощью сканирования мы можем создать программу, способную находить и загружать дополнительные модули во время выполнения.
builder.Services.Scan(s => sЗдесь мы вызываем метод расширения Scan() который выполнит сканирование сборки (в данном случае - той, из которой он вызван) и выберет сервисы для регистрации на основе предоставленного нами селектора (в данном случае из пространства имён "MyApp.Services"). Наконец, мы регистрируем выбранные нами типы в качестве подстановки для всех интерфейсов, которые они реализуют, вызывая AsImplementedInterfaces(), т.к. обычно наши зависимости представляют собой интерфейсы (типа IUserService), а не классы напрямую.
.FromCallingAssembly()
.AddClasses(c =>
с.InNamespaces("MyApp.Services"))
.AsImplementedInterfaces()
);
Используя широкий спектр методов расширения, которые предоставляет Scrutor, мы можем очень точно определить сервисы, которые мы хотим зарегистрировать, например, из другой сборки (библиотеки классов):
builder.Services.Scan(s => sЗдесь мы просим Scrutor сканировать сборку, в которой находится интерфейс ICustomerService, используя метод расширения FromAssemblyOf<ICustomerService>(). Затем среди всех реализаций в этой сборке мы выбираем только те, которые могут быть назначены ICustomerService.
.FromAssemblyOf<ICustomerService>()
.AddClasses(c =>
c.AssignableTo<ICustomerService>())
.AsMatchingInterface());
Работа с обобщёнными типами
Если мы хотим добавить реализацию обобщённого типа, то вместо обобщённой реализации метода AssignableTo, мы можем использовать метод с аргументом типа:
…Указание времени жизни зависимостей
.AddClasses(c =>
c.AssignableTo(typeof(IService<>)))
…
Мы можем использовать спецификаторы времени жизни зависимостей:
builder.Services.Scan(s => sИли соответственно WithScopedLifetime() и WithSingletonLifetime().
…
.WithTransientLifetime()
);
Обработка нескольких реализаций
Стратегия регистрации — это то, как Scrutor обрабатывает случаи, когда для одного и того же интерфейса существует несколько реализаций:
builder.Services.Scan(s => sВ этом коде, используя метод UsingRegistrationStrategy() с параметром RegistrationStrategy.Skip, мы указываем Scrutor игнорировать дополнительные реализации любого интерфейса после регистрации первой. Другие варианты:
…
.UsingRegistrationStrategy(RegistrationStrategy.Skip)
.AsImplementedInterfaces()
);
- RegistrationStrategy.Append - добавляет новую регистрацию для существующих сервисов.
- RegistrationStrategy.Throw - выдаёт ошибку при попытке зарегистрировать существующий сервис.
Окончание следует…
Источник: https://code-maze.com/dotnet-dependency-injection-with-scrutor/
👍17
День 1646. #ЗаметкиНаПолях
Scrutor в .NET. Окончание
Начало
Реализация паттерна Декоратор с помощью Scrutor
Мы можем использовать паттерн проектирования Декоратор для расширения функциональности существующего объекта без изменения его кода. Для этой цели декоратор разработан как оболочка вокруг объектов, которые необходимо расширить, чтобы он мог перехватывать вызовы методов обёрнутого объекта.
Реализация простого декоратора
Мы можем использовать метод расширения Decorate() для управления регистрацией объектов-декораторов. Определим декоратор для репозитория, который делает запись в консоль, когда мы обращаемся к списку сущностей.
Для этого нам нужно наследоваться от того же интерфейса, который реализует репозиторий:
Регистрация декораторов в Scrutor
Для регистрации декоратора в контейнере зависимостей в Scrutor используется метод расширения Decorate<,>():
После того, как мы зарегистрируем декоратор в Scrutor, все вызовы методов IRepository<User> будут перехвачены RepositoryLoggingDecorator<User>.
Замечание: В этом примере мы использовали метод Console.WriteLine() для ведения журнала, в реальном же сценарии мы бы использовали ILogger<T> и фреймворк журналирования.
Источник: https://code-maze.com/dotnet-dependency-injection-with-scrutor/
Scrutor в .NET. Окончание
Начало
Реализация паттерна Декоратор с помощью Scrutor
Мы можем использовать паттерн проектирования Декоратор для расширения функциональности существующего объекта без изменения его кода. Для этой цели декоратор разработан как оболочка вокруг объектов, которые необходимо расширить, чтобы он мог перехватывать вызовы методов обёрнутого объекта.
Реализация простого декоратора
Мы можем использовать метод расширения Decorate() для управления регистрацией объектов-декораторов. Определим декоратор для репозитория, который делает запись в консоль, когда мы обращаемся к списку сущностей.
Для этого нам нужно наследоваться от того же интерфейса, который реализует репозиторий:
public class RepositoryLoggerDecorator<T> : IRepository<T>Декоратор должен принимать в конструкторе экземпляр декорируемого репозитория, в нашем случае это экземпляр IRepository<T>, который мы будем оборачивать. В методе GetAll() мы записываем сообщение в консоль, а затем вызываем метод GetAll() обёрнутого объекта.
{
private readonly IRepository<T> _decoratedRepo;
public RepositoryLoggerDecorator(
IRepository<T> decoratedRepo)
{
_decoratedRepo = decoratedRepo;
}
public IEnumerable<T> GetAll()
{
Console.WriteLine("Retrieved list of users");
return _decoratedRepo.GetAll();
}
}
Регистрация декораторов в Scrutor
Для регистрации декоратора в контейнере зависимостей в Scrutor используется метод расширения Decorate<,>():
builder.ServicesПервый аргумент типа метода Decorate<,>() представляет тип сервиса, который мы хотим декорировать, а второй аргумент типа — это тип декоратора.
.Decorate<IRepository<User>,
RepositoryLoggerDecorator<User>>();
После того, как мы зарегистрируем декоратор в Scrutor, все вызовы методов IRepository<User> будут перехвачены RepositoryLoggingDecorator<User>.
Замечание: В этом примере мы использовали метод Console.WriteLine() для ведения журнала, в реальном же сценарии мы бы использовали ILogger<T> и фреймворк журналирования.
Источник: https://code-maze.com/dotnet-dependency-injection-with-scrutor/
👍7
День 1647. #ВопросыНаСобеседовании
Самые часто задаваемые вопросы на собеседовании по C#
Давненько не было ничего из этой рубрики. Нумерацию продолжу, если что, предыдущие вопросы доступны по тегу #ВопросыНаСобеседовании.
19. В чем разница между интерфейсами IEnumerable, ICollection и IList в C#?
IEnumerable, ICollection и IList — это интерфейсы, предоставляемые библиотеками платформы .NET для работы с коллекциями данных. Они имеют следующие особенности.
IEnumerable:
- Предоставляет базовые функции для итерации по коллекции с использованием цикла foreach.
- Поддерживает доступ только для чтения.
- Идеально подходит для ситуаций, когда вам нужно только перебирать коллекцию.
- Определяет только один метод: GetEnumerator(), который возвращает IEnumerator.
ICollection:
- Наследуется от IEnumerable.
- Представляет собой группу элементов с дополнительными функциями, такими как размер (свойство Count), перечисление и синхронизация (через свойства IsSynchronized и SyncRoot).
- Добавляет основные операции с коллекцией, такие как Add, Remove, Clear, проверку вхождения элемента (Contains) или проверку списка на доступность записи IsReadOnly.
- Поддерживает доступ как для чтения, так и для записи.
IList:
- Наследуется от ICollection.
- Представляет список элементов, доступных по индексу.
- Предоставляет расширенные возможности управления коллекцией, такие как вставка и удаление по индексу (методы Insert и RemoveAt).
- Поддерживает произвольный доступ к элементам через индексатор.
См. также
- «В чём разница между IEnumerable и IQueryable?»
- «Какой Интерфейс Коллекции Использовать?»
Источник: https://dev.to/bytehide/20-c-interview-questions-for-experienced-2023-1hl6
Самые часто задаваемые вопросы на собеседовании по C#
Давненько не было ничего из этой рубрики. Нумерацию продолжу, если что, предыдущие вопросы доступны по тегу #ВопросыНаСобеседовании.
19. В чем разница между интерфейсами IEnumerable, ICollection и IList в C#?
IEnumerable, ICollection и IList — это интерфейсы, предоставляемые библиотеками платформы .NET для работы с коллекциями данных. Они имеют следующие особенности.
IEnumerable:
- Предоставляет базовые функции для итерации по коллекции с использованием цикла foreach.
- Поддерживает доступ только для чтения.
- Идеально подходит для ситуаций, когда вам нужно только перебирать коллекцию.
- Определяет только один метод: GetEnumerator(), который возвращает IEnumerator.
ICollection:
- Наследуется от IEnumerable.
- Представляет собой группу элементов с дополнительными функциями, такими как размер (свойство Count), перечисление и синхронизация (через свойства IsSynchronized и SyncRoot).
- Добавляет основные операции с коллекцией, такие как Add, Remove, Clear, проверку вхождения элемента (Contains) или проверку списка на доступность записи IsReadOnly.
- Поддерживает доступ как для чтения, так и для записи.
IList:
- Наследуется от ICollection.
- Представляет список элементов, доступных по индексу.
- Предоставляет расширенные возможности управления коллекцией, такие как вставка и удаление по индексу (методы Insert и RemoveAt).
- Поддерживает произвольный доступ к элементам через индексатор.
См. также
- «В чём разница между IEnumerable и IQueryable?»
- «Какой Интерфейс Коллекции Использовать?»
Источник: https://dev.to/bytehide/20-c-interview-questions-for-experienced-2023-1hl6
👍25
День 1648.
Сегодня порекомендую вам один из лучших, на мой взгляд, докладов DotNext Autumn 2022 (почти год ждал, когда его выложат в открытый доступ). Роман Неволин «Пишем приложения, которые не ломаются в продакшене»
Любой разработчик в своей работе учитывает много метрик качества: производительность, читаемость, расширяемость. Однако, прежде чем всё это будет играть какую-либо роль, приложение для начала должно просто работать.
Об этом и пойдет речь:
1. О написании кода, который уменьшает вероятность ошибки
- Уменьшайте шансы пропустить валидацию и прочее
- Делайте модели настолько точными, насколько можете
- Отделяйте внешние данные от бизнесовых моделей
- Отделяйте бизнес-логику от всего остального
2. О тестировании
- Вынесите общую инициализацию из теста
- Спрячьте конкретные инструменты и детали реализации
- Держите специфическую инициализацию в тесте, а не в файлах
- Воспроизводите реальное окружение
3 О деплое
- Автоматизируйте все проверки, какие можете
- Наблюдайте за всем, что происходит в приложении
- Пишите постмортемы (что случилось, почему, как исправить в будущем)
- Действительно делайте то, что написано в постмортеме
Роман разбирает, как на каждом из этапов разработки уменьшить вероятность возникновения багов, как отслеживать их в тестировании и что делать, если вы все-таки задеплоили некорректный код.
Сегодня порекомендую вам один из лучших, на мой взгляд, докладов DotNext Autumn 2022 (почти год ждал, когда его выложат в открытый доступ). Роман Неволин «Пишем приложения, которые не ломаются в продакшене»
Любой разработчик в своей работе учитывает много метрик качества: производительность, читаемость, расширяемость. Однако, прежде чем всё это будет играть какую-либо роль, приложение для начала должно просто работать.
Об этом и пойдет речь:
1. О написании кода, который уменьшает вероятность ошибки
- Уменьшайте шансы пропустить валидацию и прочее
- Делайте модели настолько точными, насколько можете
- Отделяйте внешние данные от бизнесовых моделей
- Отделяйте бизнес-логику от всего остального
2. О тестировании
- Вынесите общую инициализацию из теста
- Спрячьте конкретные инструменты и детали реализации
- Держите специфическую инициализацию в тесте, а не в файлах
- Воспроизводите реальное окружение
3 О деплое
- Автоматизируйте все проверки, какие можете
- Наблюдайте за всем, что происходит в приложении
- Пишите постмортемы (что случилось, почему, как исправить в будущем)
- Действительно делайте то, что написано в постмортеме
Роман разбирает, как на каждом из этапов разработки уменьшить вероятность возникновения багов, как отслеживать их в тестировании и что делать, если вы все-таки задеплоили некорректный код.
👍27