День 2491. #ЗаметкиНаПолях
Паттерн Идемпотентный Потребитель. Начало
Распределенные системы по своей природе ненадёжны. Одна из ключевых проблем — гарантировать, что сообщения обрабатываются ровно один раз. Теоретически это невозможно гарантировать в большинстве систем. Если вы проектируете систему, предполагая, что каждое сообщение будет обработано ровно один раз, вы подвергаете себя риску неявного повреждения данных. Но мы можем спроектировать систему так, чтобы побочные эффекты применялись ровно один раз, используя паттерн «Идемпотентный потребитель».
Что может пойти не так при публикации
Предположим, сервис публикует событие при создании новой заметки:
Конкретная реализация издателя или брокера сообщений не важна.
Теперь представьте:
- Издатель отправляет сообщение брокеру.
- Брокер сохраняет его и отправляет ACK.
- Сбой в сети: ACK не доходит до производителя.
- Производитель по тайм-ауту повторяет попытку публикации.
- Теперь у брокера два события NoteCreated.
С точки зрения производителя, он действовал правильно. Но потребитель получил два события о создании одной и той же заметки.
И это только один из путей возникновения сбоя. Вы также можете получить дубликаты из-за:
- Повторных доставок брокером.
- Сбоев потребителя + повторных попыток.
Т.е. даже если вы всё сделали «правильно» на издателе, потребителю всё равно придётся защищаться.
Идемпотентность на стороне издателя (управляет брокер)
Многие брокеры сообщений поддерживают идемпотентную публикацию посредством дедупликации сообщений, если вы укажете уникальный идентификатор сообщения. Например, Azure Service Bus может обнаруживать дубликаты и игнорировать повторные публикации сообщений с тем же идентификатором в течение заданного периода. Amazon SQS и другие брокеры также предлагают аналогичные гарантии.
Не нужно заново изобретать эту логику в вашем приложении. Ключ к успеху — назначить каждому сообщению стабильный идентификатор, уникально отражающий отправляемое логическое событие. Например, при публикации события NoteCreated:
Если происходит сбой сети после отправки сообщения, приложение может повторить попытку. Но когда брокер обнаруживает тот же MessageId, он понимает, что это дубликат, и безопасно отбрасывает его. Вы получаете дедупликацию без каких-либо специальных таблиц отслеживания или дополнительного состояния в вашем сервисе.
Эта идемпотентность на уровне брокера решает широкий класс проблем на стороне производителя: повторные попытки сети, временные сбои и дублирующиеся публикации.
Она не обрабатывает повторные попытки потребителя, которые происходят при повторной доставке сообщений или сбое вашего сервиса во время обработки.
С этим поможет шаблон «Идемпотентный потребитель».
Продолжение следует…
Источник: https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
Паттерн Идемпотентный Потребитель. Начало
Распределенные системы по своей природе ненадёжны. Одна из ключевых проблем — гарантировать, что сообщения обрабатываются ровно один раз. Теоретически это невозможно гарантировать в большинстве систем. Если вы проектируете систему, предполагая, что каждое сообщение будет обработано ровно один раз, вы подвергаете себя риску неявного повреждения данных. Но мы можем спроектировать систему так, чтобы побочные эффекты применялись ровно один раз, используя паттерн «Идемпотентный потребитель».
Что может пойти не так при публикации
Предположим, сервис публикует событие при создании новой заметки:
await publisher.PublishAsync(
new NoteCreated(note.Id, note.Title, note.Content));
Конкретная реализация издателя или брокера сообщений не важна.
Теперь представьте:
- Издатель отправляет сообщение брокеру.
- Брокер сохраняет его и отправляет ACK.
- Сбой в сети: ACK не доходит до производителя.
- Производитель по тайм-ауту повторяет попытку публикации.
- Теперь у брокера два события NoteCreated.
С точки зрения производителя, он действовал правильно. Но потребитель получил два события о создании одной и той же заметки.
И это только один из путей возникновения сбоя. Вы также можете получить дубликаты из-за:
- Повторных доставок брокером.
- Сбоев потребителя + повторных попыток.
Т.е. даже если вы всё сделали «правильно» на издателе, потребителю всё равно придётся защищаться.
Идемпотентность на стороне издателя (управляет брокер)
Многие брокеры сообщений поддерживают идемпотентную публикацию посредством дедупликации сообщений, если вы укажете уникальный идентификатор сообщения. Например, Azure Service Bus может обнаруживать дубликаты и игнорировать повторные публикации сообщений с тем же идентификатором в течение заданного периода. Amazon SQS и другие брокеры также предлагают аналогичные гарантии.
Не нужно заново изобретать эту логику в вашем приложении. Ключ к успеху — назначить каждому сообщению стабильный идентификатор, уникально отражающий отправляемое логическое событие. Например, при публикации события NoteCreated:
var message = new NoteCreated(
note.Id, note.Title, note.Content)
{
MessageId = Guid.NewGuid() // либо note.Id
};
await publisher.PublishAsync(message);
Если происходит сбой сети после отправки сообщения, приложение может повторить попытку. Но когда брокер обнаруживает тот же MessageId, он понимает, что это дубликат, и безопасно отбрасывает его. Вы получаете дедупликацию без каких-либо специальных таблиц отслеживания или дополнительного состояния в вашем сервисе.
Эта идемпотентность на уровне брокера решает широкий класс проблем на стороне производителя: повторные попытки сети, временные сбои и дублирующиеся публикации.
Она не обрабатывает повторные попытки потребителя, которые происходят при повторной доставке сообщений или сбое вашего сервиса во время обработки.
С этим поможет шаблон «Идемпотентный потребитель».
Продолжение следует…
Источник: https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
👍12
День 2492. #ЗаметкиНаПолях
Паттерн Идемпотентный Потребитель. Продолжение
Начало
Реализация «Идемпотентного потребителя»
Вот пример идемпотентного потребителя события создания заметки NoteCreated:
Важные детали
1. Ключ Идемпотентности
Используем:
- MessageId из контекста передачи сообщений (ctx.MessageId);
- ConsumerName (чтобы несколько получателей могли безопасно обрабатывать одно и то же сообщение).
При поступлении дублирующего сообщения выполняется быстрый выход, и ничего не происходит.
Также важно иметь ограничение уникальности (MessageId, ConsumerName) в таблице MessageConsumers для предотвращения гонок. Таким образом, даже при параллельной обработке одного и того же сообщения только одна сможет вставить запись.
2. Атомарные побочные эффекты + идемпотентность записи
Обработка и сохранение записи получателя сообщения происходят в одной транзакции, поэтому:
- Если обработка завершается неудачей, в таблице MessageConsumers нет записи, поэтому сообщение можно отправить повторно.
- Если обработка завершается успешно, то и заметка, и строка в MessageConsumers фиксируются одновременно.
- Вы никогда не окажетесь в состоянии, когда работа выполнена, но сообщение не помечено как обработанное, и наоборот.
3. Обработка доставки по принципу «как минимум один раз»
Большинство реалистичных конфигураций выполняются по принципу «как минимум один раз»:
- Потребитель обрабатывает сообщение;
- Сбой подтверждения / тайм-аут;
- Брокер повторно доставляет;
- Ваш код выполняется снова.
При использовании этого шаблона второй запуск обращается к таблице MessageConsumers и завершается раньше времени.
Нет дублирования побочных эффектов.
Это работает, за исключением одного нюанса…
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
Паттерн Идемпотентный Потребитель. Продолжение
Начало
Реализация «Идемпотентного потребителя»
Вот пример идемпотентного потребителя события создания заметки NoteCreated:
internal class NoteCreatedConsumer(
DbContext dbContext,
// … кэш, логгер и т.п.
)
: IConsumer<NoteCreated>
{
public async Task
ConsumeAsync(ConsumeContext<NoteCreated> ctx)
{
// Проверяем, получали ли мы это сообщение
if (await dbCtx
.MessageConsumers.AnyAsync(c =>
c.MessageId == ctx.MessageId &&
c.ConsumerName == nameof(NoteCreatedConsumer)))
return;
using var transaction = await
dbCtx.Database.BeginTransactionAsync();
// … сохраняем заметку в базе
// Записываем, что сообщение обработано
dbCtx.MessageConsumers
.Add(new MessageConsumer
{
MessageId = ctx.MessageId,
ConsumerName = nameof(NoteCreatedConsumer),
ConsumedAtUtc = DateTime.UtcNow
});
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
// … обновляем кэш, пишем в лог
}
}
Важные детали
1. Ключ Идемпотентности
if (await dbCtx
.MessageConsumers.AnyAsync(c =>
c.MessageId == ctx.MessageId &&
c.ConsumerName == nameof(NoteCreatedConsumer)))
return;
Используем:
- MessageId из контекста передачи сообщений (ctx.MessageId);
- ConsumerName (чтобы несколько получателей могли безопасно обрабатывать одно и то же сообщение).
При поступлении дублирующего сообщения выполняется быстрый выход, и ничего не происходит.
Также важно иметь ограничение уникальности (MessageId, ConsumerName) в таблице MessageConsumers для предотвращения гонок. Таким образом, даже при параллельной обработке одного и того же сообщения только одна сможет вставить запись.
2. Атомарные побочные эффекты + идемпотентность записи
Обработка и сохранение записи получателя сообщения происходят в одной транзакции, поэтому:
- Если обработка завершается неудачей, в таблице MessageConsumers нет записи, поэтому сообщение можно отправить повторно.
- Если обработка завершается успешно, то и заметка, и строка в MessageConsumers фиксируются одновременно.
- Вы никогда не окажетесь в состоянии, когда работа выполнена, но сообщение не помечено как обработанное, и наоборот.
3. Обработка доставки по принципу «как минимум один раз»
Большинство реалистичных конфигураций выполняются по принципу «как минимум один раз»:
- Потребитель обрабатывает сообщение;
- Сбой подтверждения / тайм-аут;
- Брокер повторно доставляет;
- Ваш код выполняется снова.
При использовании этого шаблона второй запуск обращается к таблице MessageConsumers и завершается раньше времени.
Нет дублирования побочных эффектов.
Это работает, за исключением одного нюанса…
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
👍10
День 2493. #ЗаметкиНаПолях
Паттерн Идемпотентный Потребитель. Окончание
Начало
Продолжение
Детерминированные и недетерминированные обработчики
Что произойдёт, когда ваш обработчик вызывает что-то вне базы данных: API, отправка email, платёжный шлюз или очередь фоновых заданий? Всё это распространённые побочные эффекты, которые также должны быть идемпотентными.
Эти вызовы находятся за пределами транзакции. БД может успешно завершить транзакцию, но, если сеть перестанет работать до ответа внешнего сервиса, вы не сможете определить, произошло действие или нет. При повторной попытке клиент может отправить ещё один email или дважды списать средства с карты.
Мы вступили на сложную территорию недетерминированных обработчиков: операции, которые невозможно безопасно повторить. Есть две основные стратегии решения этой проблемы.
1. Использовать ключ идемпотентности во внешнем вызове
Если внешний сервис это поддерживает, передавайте стабильный идентификатор, например, MessageId сообщения, с каждым запросом. Многие API, включая платёжные системы и платформы email, позволяют указывать заголовок c ключом идемпотентности. Сервис гарантирует, что идентичные запросы с одним и тем же ключом будут выполнены только один раз:
Даже если запрос будет повторён, провайдер распознает ключ и отбросит дубликат. Это самый простой и надёжный подход, если внешняя зависимость его поддерживает.
2. Сохраните намерение локально
Если внешний сервис не поддерживает ключи идемпотентности, вы можете сымитировать его. Сохраните запись о предполагаемом действии в базе перед вызовом внешней системы. Например, таблица PendingEmails, содержащая информацию о сообщениях на отправку.
Фоновый процесс может позже читать эти записи и выполнять действие один раз. Это делает процесс детерминированным, но ценой большей сложности, дополнительных таблиц и фоновых процессов. Это часто оверинжиниринг, если только побочный эффект не является критическим или необратимым, например, платёж.
Компромисс сводится к оценке последствий. Если последствия повторения действия существенны, явно добавьте идемпотентность. В противном случае повтор операции может быть приемлемым.
Когда идемпотентный потребитель не нужен
Если операция изначально идемпотентна, часто можно пропустить дополнительную табличную и транзакционную логику.
Установка флага состояния или обновление кэша — примеры детерминированных действий, которые можно безопасно выполнять несколько раз. Это операции, которые перезаписывают состояние, а не добавляют к нему данные.
Некоторые обработчики также используют проверки предусловий, чтобы избежать дублирования. Если обработчик обновляет сущность, он может сначала проверить, находится ли эта сущность уже в желаемом состоянии, и вернуть управление раньше времени. Этого простого защитного условия может быть достаточно.
Не применяйте шаблон «Идемпотентный потребитель» бездумно везде. Применяйте его там, где он защищает вас от реального ущерба, где дублирование обработки приводит к финансовым последствиям или несогласованности данных.
Во всём остальном — чем проще, тем лучше.
Источник: https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
Паттерн Идемпотентный Потребитель. Окончание
Начало
Продолжение
Детерминированные и недетерминированные обработчики
Что произойдёт, когда ваш обработчик вызывает что-то вне базы данных: API, отправка email, платёжный шлюз или очередь фоновых заданий? Всё это распространённые побочные эффекты, которые также должны быть идемпотентными.
Эти вызовы находятся за пределами транзакции. БД может успешно завершить транзакцию, но, если сеть перестанет работать до ответа внешнего сервиса, вы не сможете определить, произошло действие или нет. При повторной попытке клиент может отправить ещё один email или дважды списать средства с карты.
Мы вступили на сложную территорию недетерминированных обработчиков: операции, которые невозможно безопасно повторить. Есть две основные стратегии решения этой проблемы.
1. Использовать ключ идемпотентности во внешнем вызове
Если внешний сервис это поддерживает, передавайте стабильный идентификатор, например, MessageId сообщения, с каждым запросом. Многие API, включая платёжные системы и платформы email, позволяют указывать заголовок c ключом идемпотентности. Сервис гарантирует, что идентичные запросы с одним и тем же ключом будут выполнены только один раз:
await emailSvc.SendAsync(new EmailRequest
{
To = user.Email,
Subject = "Привет!",
Body = "Спасибо за подписку на канал.",
IdempotencyKey = ctx.MessageId
});
Даже если запрос будет повторён, провайдер распознает ключ и отбросит дубликат. Это самый простой и надёжный подход, если внешняя зависимость его поддерживает.
2. Сохраните намерение локально
Если внешний сервис не поддерживает ключи идемпотентности, вы можете сымитировать его. Сохраните запись о предполагаемом действии в базе перед вызовом внешней системы. Например, таблица PendingEmails, содержащая информацию о сообщениях на отправку.
Фоновый процесс может позже читать эти записи и выполнять действие один раз. Это делает процесс детерминированным, но ценой большей сложности, дополнительных таблиц и фоновых процессов. Это часто оверинжиниринг, если только побочный эффект не является критическим или необратимым, например, платёж.
Компромисс сводится к оценке последствий. Если последствия повторения действия существенны, явно добавьте идемпотентность. В противном случае повтор операции может быть приемлемым.
Когда идемпотентный потребитель не нужен
Если операция изначально идемпотентна, часто можно пропустить дополнительную табличную и транзакционную логику.
Установка флага состояния или обновление кэша — примеры детерминированных действий, которые можно безопасно выполнять несколько раз. Это операции, которые перезаписывают состояние, а не добавляют к нему данные.
Некоторые обработчики также используют проверки предусловий, чтобы избежать дублирования. Если обработчик обновляет сущность, он может сначала проверить, находится ли эта сущность уже в желаемом состоянии, и вернуть управление раньше времени. Этого простого защитного условия может быть достаточно.
Не применяйте шаблон «Идемпотентный потребитель» бездумно везде. Применяйте его там, где он защищает вас от реального ущерба, где дублирование обработки приводит к финансовым последствиям или несогласованности данных.
Во всём остальном — чем проще, тем лучше.
Источник: https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
👍13
День 2494. #ЧтоНовенького
Модернизируем .NET-приложения с Copilot
Модернизация приложений — не просто поддержание актуальности версии. Старые фреймворки могут создавать риски безопасности и снижать производительность. Обновление старого приложения .NET не обязательно означает поиск неисправных сборок и непонятных ошибок. Однако для многих разработчиков простое обновление версии оборачивается часами ручных исправлений и борьбы с конфликтами зависимостей.
GitHub Copilot - помощник по модернизации, который проведёт вас через каждый этап, автоматизирует сложную работу и поможет перейти от «это может занять несколько недель» к «сделано за несколько часов».
Предварительные требования
- Visual Studio 2026 (или VS 2022 версии 17.14.17 или новее);
- Лицензия GitHub Copilot (Pro, Pro+, Business или Enterprise);
- Рабочая нагрузка .NET Desktop с включёнными компонентами: GitHub Copilot и GitHub Copilot app modernization.
Обновление приложения .NET
1. Откройте проект или решение
Начните с запуска Visual Studio и откройте свой проект или решение .NET.
2. Начните сеанс агента
- Щёлкните правой кнопкой мыши по проекту или решению в обозревателе решений и выберите Modernize (Модернизировать).
- Либо откройте чат GitHub Copilot и введите @modernize, а затем ваш запрос.
3. Выберите путь
Выберите опцию Upgrade to a newer version of .NET (Обновиться до более новой версии .NET).
4. Оценка и планирование
Copilot оценит ваш код и зависимости, затем:
- Задаст несколько вопросов о ваших целях, чтобы адаптировать план.
- Создаст план обновления в формате Markdown для прозрачности (см. картинку 1).
- Позволит просмотреть и отредактировать план перед дальнейшими действиями.
Вы можете отредактировать план, добавив контекст, изменив порядок этапов или исключив определённые проекты перед утверждением.
5. Применение изменений и устранение ошибок
После утверждения плана Copilot:
- Автоматически обновит файлы, скорректирует импорт и исправит синтаксические ошибки;
- Обработает ошибки сборки в цикле исправления и тестирования для обеспечения стабильности;
- Будет отслеживать ход выполнения в документе «Подробности обновления» для наглядности;
- Будет фиксировать каждый важный шаг в Git для лёгкого отката при необходимости;
- Если Copilot обнаружит проблему, которую не может исправить автоматически, он приостановит работу и запросит действия от вас, сохраняя контроль над процессом.
6. Проверка результатов
После завершения Copilot предоставит:
- Подробный отчёт с хешами коммитов Git для отслеживания;
- Раздел «Следующие шаги» для действий после обновления, таких как обновление конвейеров CI/CD или запуск интеграционных тестов.
Источник: https://devblogs.microsoft.com/dotnet/modernizing-dotnet-with-github-copilot-agent-mode/
Модернизируем .NET-приложения с Copilot
Модернизация приложений — не просто поддержание актуальности версии. Старые фреймворки могут создавать риски безопасности и снижать производительность. Обновление старого приложения .NET не обязательно означает поиск неисправных сборок и непонятных ошибок. Однако для многих разработчиков простое обновление версии оборачивается часами ручных исправлений и борьбы с конфликтами зависимостей.
GitHub Copilot - помощник по модернизации, который проведёт вас через каждый этап, автоматизирует сложную работу и поможет перейти от «это может занять несколько недель» к «сделано за несколько часов».
Предварительные требования
- Visual Studio 2026 (или VS 2022 версии 17.14.17 или новее);
- Лицензия GitHub Copilot (Pro, Pro+, Business или Enterprise);
- Рабочая нагрузка .NET Desktop с включёнными компонентами: GitHub Copilot и GitHub Copilot app modernization.
Обновление приложения .NET
1. Откройте проект или решение
Начните с запуска Visual Studio и откройте свой проект или решение .NET.
2. Начните сеанс агента
- Щёлкните правой кнопкой мыши по проекту или решению в обозревателе решений и выберите Modernize (Модернизировать).
- Либо откройте чат GitHub Copilot и введите @modernize, а затем ваш запрос.
3. Выберите путь
Выберите опцию Upgrade to a newer version of .NET (Обновиться до более новой версии .NET).
4. Оценка и планирование
Copilot оценит ваш код и зависимости, затем:
- Задаст несколько вопросов о ваших целях, чтобы адаптировать план.
- Создаст план обновления в формате Markdown для прозрачности (см. картинку 1).
- Позволит просмотреть и отредактировать план перед дальнейшими действиями.
Вы можете отредактировать план, добавив контекст, изменив порядок этапов или исключив определённые проекты перед утверждением.
5. Применение изменений и устранение ошибок
После утверждения плана Copilot:
- Автоматически обновит файлы, скорректирует импорт и исправит синтаксические ошибки;
- Обработает ошибки сборки в цикле исправления и тестирования для обеспечения стабильности;
- Будет отслеживать ход выполнения в документе «Подробности обновления» для наглядности;
- Будет фиксировать каждый важный шаг в Git для лёгкого отката при необходимости;
- Если Copilot обнаружит проблему, которую не может исправить автоматически, он приостановит работу и запросит действия от вас, сохраняя контроль над процессом.
6. Проверка результатов
После завершения Copilot предоставит:
- Подробный отчёт с хешами коммитов Git для отслеживания;
- Раздел «Следующие шаги» для действий после обновления, таких как обновление конвейеров CI/CD или запуск интеграционных тестов.
Источник: https://devblogs.microsoft.com/dotnet/modernizing-dotnet-with-github-copilot-agent-mode/
👎21👍7
День 2495. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
10. Управление памятью и сборка мусора
«Объясните роль сборки мусора в .NET и расскажите, как она помогает управлять памятью? С какими потенциальные проблемами могут столкнуться разработчики при сборке мусора, и какие есть способы их решения?»
Хороший ответ
«Сборка мусора (GC) в .NET — это форма автоматического управления памятью. Сборщик мусора управляет освобождением памяти для приложений. При создании экземпляров объектов в .NET среда CLR (Common Language Runtime) выделяет для них память в управляемой куче. По мере создания новых объектов куча заполняется, и память необходимо освобождать. Сборщик мусора автоматизирует этот процесс, периодически определяя объекты, которые больше не используются приложением, и освобождая занимаемую ими память.
Сборщик мусора работает по модели поколений для повышения производительности, разделяя кучу на три поколения:
- 0 - короткоживущие объекты. GC очищает это поколение чаще всего.
- 1 - служит буфером между короткоживущими и долгоживущими объектами.
- 2 - содержит долгоживущие объекты.
Объект изначально помещается в поколение 0. Если он «выжил» при первой сборке мусора, он перемещается в поколение 1, потом – в поколение 2. Идея в том, что объекты, которые используются долгое время, скорее всего продолжат использоваться и далее, поэтому не имеет смысла часто их проверять.
После удаления неиспользуемых объектов сборщик мусора иногда также перемещает «выжившие» объекты в начало кучи. Это уменьшает фрагментированность памяти и повышает производительность приложений.
Из потенциальных проблем, связанных со сборкой мусора, можно выделить:
1. Задержки
Поскольку сборка мусора недетерминирована, она может происходить в неподходящее время, увеличивая задержку отклика приложения. Разработчики могут снизить задержки, оптимизируя создание и управление объектами, например, повторно используя объекты, где это возможно, или избегая выделений в куче для больших объектов (LOH), сборка которых требует больших затрат.
2. Утечки памяти
Утечки памяти могут возникать и в управляемых языках, таких как C#. Разработчики должны гарантировать, что объекты, занимающие большой объём памяти, правильно удаляются и не сохраняются случайно в глобальных или статических ссылках.
Разработчики могут влиять на производительность сборки мусора, например, используя слабые ссылки для больших объектов, к которым редко обращаются, и минимизируя использование финализаторов, которые могут задерживать сборку мусора.
Часто встречающийся некорректный ответ
«Сборка мусора в .NET автоматически очищает все неиспользуемые объекты, поэтому .NET разработчикам не нужно беспокоиться об управлении памятью. GC всё делает за нас, гарантируя, что приложение использует минимум памяти».
Этот ответ демонстрирует ошибочное представление о сборке мусора.
- Чрезмерная уверенность в GC: Хотя GC действительно помогает управлять памятью, разработчикам всё равно следует осознавать, сколько и каких объектов они создают и как управляют памятью, чтобы предотвратить такие проблемы, как утечки памяти и чрезмерное использование памяти.
- Непонимание оптимизации памяти: Это утверждение ошибочно предполагает, что сборка мусора сама по себе оптимизирует использование памяти приложением. В действительности неэффективные практики кодирования могут привести к неэффективному использованию памяти и проблемам с производительностью, независимо от наличия GC.
- Пренебрежение ручной очисткой: Этот ответ игнорирует необходимость шаблонов ручной очистки, особенно для неуправляемых ресурсов или больших управляемых объектов, которые могут существенно повлиять на производительность, если ими не управлять.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
10. Управление памятью и сборка мусора
«Объясните роль сборки мусора в .NET и расскажите, как она помогает управлять памятью? С какими потенциальные проблемами могут столкнуться разработчики при сборке мусора, и какие есть способы их решения?»
Хороший ответ
«Сборка мусора (GC) в .NET — это форма автоматического управления памятью. Сборщик мусора управляет освобождением памяти для приложений. При создании экземпляров объектов в .NET среда CLR (Common Language Runtime) выделяет для них память в управляемой куче. По мере создания новых объектов куча заполняется, и память необходимо освобождать. Сборщик мусора автоматизирует этот процесс, периодически определяя объекты, которые больше не используются приложением, и освобождая занимаемую ими память.
Сборщик мусора работает по модели поколений для повышения производительности, разделяя кучу на три поколения:
- 0 - короткоживущие объекты. GC очищает это поколение чаще всего.
- 1 - служит буфером между короткоживущими и долгоживущими объектами.
- 2 - содержит долгоживущие объекты.
Объект изначально помещается в поколение 0. Если он «выжил» при первой сборке мусора, он перемещается в поколение 1, потом – в поколение 2. Идея в том, что объекты, которые используются долгое время, скорее всего продолжат использоваться и далее, поэтому не имеет смысла часто их проверять.
После удаления неиспользуемых объектов сборщик мусора иногда также перемещает «выжившие» объекты в начало кучи. Это уменьшает фрагментированность памяти и повышает производительность приложений.
Из потенциальных проблем, связанных со сборкой мусора, можно выделить:
1. Задержки
Поскольку сборка мусора недетерминирована, она может происходить в неподходящее время, увеличивая задержку отклика приложения. Разработчики могут снизить задержки, оптимизируя создание и управление объектами, например, повторно используя объекты, где это возможно, или избегая выделений в куче для больших объектов (LOH), сборка которых требует больших затрат.
2. Утечки памяти
Утечки памяти могут возникать и в управляемых языках, таких как C#. Разработчики должны гарантировать, что объекты, занимающие большой объём памяти, правильно удаляются и не сохраняются случайно в глобальных или статических ссылках.
Разработчики могут влиять на производительность сборки мусора, например, используя слабые ссылки для больших объектов, к которым редко обращаются, и минимизируя использование финализаторов, которые могут задерживать сборку мусора.
Часто встречающийся некорректный ответ
«Сборка мусора в .NET автоматически очищает все неиспользуемые объекты, поэтому .NET разработчикам не нужно беспокоиться об управлении памятью. GC всё делает за нас, гарантируя, что приложение использует минимум памяти».
Этот ответ демонстрирует ошибочное представление о сборке мусора.
- Чрезмерная уверенность в GC: Хотя GC действительно помогает управлять памятью, разработчикам всё равно следует осознавать, сколько и каких объектов они создают и как управляют памятью, чтобы предотвратить такие проблемы, как утечки памяти и чрезмерное использование памяти.
- Непонимание оптимизации памяти: Это утверждение ошибочно предполагает, что сборка мусора сама по себе оптимизирует использование памяти приложением. В действительности неэффективные практики кодирования могут привести к неэффективному использованию памяти и проблемам с производительностью, независимо от наличия GC.
- Пренебрежение ручной очисткой: Этот ответ игнорирует необходимость шаблонов ручной очистки, особенно для неуправляемых ресурсов или больших управляемых объектов, которые могут существенно повлиять на производительность, если ими не управлять.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👍10
День 2496. #SystemDesign101
8 Структур Данных, Использующихся в БД
Данные могут быть индексированы в памяти или на диске. Аналогично, форматы данных различаются, например, числа, строки, географические координаты и т.д. Система может быть ориентирована на запись или чтение. Все эти факторы влияют на выбор формата индекса базы данных.
Ниже перечислены некоторые из наиболее популярных структур, используемых для индексации данных.
1. Skiplist
Вероятностная структура данных, позволяющая в среднем за O(log(n)) времени выполнять операции добавления, удаления и поиска элементов. Распространённый тип индекса в памяти. Используется в Redis.
2. Хэш-индекс
Структура данных, используемая для быстрого поиска точных совпадений в БД, основанная на хэш-таблицах. Работает с помощью хэш-функции, которая преобразует значение ключа в хэш-код (число), а затем используется для определения "бакета" (корзины), где хранится ссылка на нужную запись.
3. SS-таблица
Формат хранения данных в виде неизменяемого файла на диске, содержащего отсортированные по ключам пары "ключ-значение". Данные из временной памяти (Memtable) сбрасываются на диск в виде SS-таблицы, что делает их постоянными и отсортированными для быстрого доступа.
4. LSM-дерево
Skiplist + SSTable. Cтруктура данных, используемая в БД для эффективного хранения и обработки большого количества записей, особенно при частых вставках и удалениях. Новые данные помещаются в отсортированный буфер в оперативной памяти (Memtable), затем периодически сбрасываются на диск в виде SS-таблиц, которые затем объединяются в фоновом режиме.
5. B-дерево
Сбалансированная древовидная структура данных, которая оптимизирована для работы с большими объёмами информации, хранящейся на диске или во внешней памяти. Каждый узел B-дерева может содержать множество ключей и ссылок на множество потомков, что позволяет уменьшить высоту дерева и, как следствие, сократить количество операций чтения-записи, что критически важно для БД и файловых систем.
6. Инвертированный индекс
Структура данных, которая сопоставляет слова с документами, в которых они встречаются. Является ключевым элементом поисковых систем, так как позволяет быстро находить документы по заданному слову или фразе, перебирая списки документов, а не все документы целиком. Используется в Lucene.
7. Суффиксное дерево
Cжатое дерево, представляющее все суффиксы заданной строки. Позволяет быстро решать задачи, связанные с поиском подстрок, такие как поиск вхождений, поиск самых длинных общих подстрок и т.п.
8. R-дерево
Древовидная структура данных для индексации многомерной, в основном пространственной, информации, такой как географические координаты, прямоугольники или многоугольники. Позволяет эффективно выполнять запросы к таким данным, разбивая пространство на перекрывающиеся области с помощью ограничивающих прямоугольников, и организует объекты в узлах дерева для быстрой фильтрации и поиска.
Источник: https://blog.bytebytego.com
8 Структур Данных, Использующихся в БД
Данные могут быть индексированы в памяти или на диске. Аналогично, форматы данных различаются, например, числа, строки, географические координаты и т.д. Система может быть ориентирована на запись или чтение. Все эти факторы влияют на выбор формата индекса базы данных.
Ниже перечислены некоторые из наиболее популярных структур, используемых для индексации данных.
1. Skiplist
Вероятностная структура данных, позволяющая в среднем за O(log(n)) времени выполнять операции добавления, удаления и поиска элементов. Распространённый тип индекса в памяти. Используется в Redis.
2. Хэш-индекс
Структура данных, используемая для быстрого поиска точных совпадений в БД, основанная на хэш-таблицах. Работает с помощью хэш-функции, которая преобразует значение ключа в хэш-код (число), а затем используется для определения "бакета" (корзины), где хранится ссылка на нужную запись.
3. SS-таблица
Формат хранения данных в виде неизменяемого файла на диске, содержащего отсортированные по ключам пары "ключ-значение". Данные из временной памяти (Memtable) сбрасываются на диск в виде SS-таблицы, что делает их постоянными и отсортированными для быстрого доступа.
4. LSM-дерево
Skiplist + SSTable. Cтруктура данных, используемая в БД для эффективного хранения и обработки большого количества записей, особенно при частых вставках и удалениях. Новые данные помещаются в отсортированный буфер в оперативной памяти (Memtable), затем периодически сбрасываются на диск в виде SS-таблиц, которые затем объединяются в фоновом режиме.
5. B-дерево
Сбалансированная древовидная структура данных, которая оптимизирована для работы с большими объёмами информации, хранящейся на диске или во внешней памяти. Каждый узел B-дерева может содержать множество ключей и ссылок на множество потомков, что позволяет уменьшить высоту дерева и, как следствие, сократить количество операций чтения-записи, что критически важно для БД и файловых систем.
6. Инвертированный индекс
Структура данных, которая сопоставляет слова с документами, в которых они встречаются. Является ключевым элементом поисковых систем, так как позволяет быстро находить документы по заданному слову или фразе, перебирая списки документов, а не все документы целиком. Используется в Lucene.
7. Суффиксное дерево
Cжатое дерево, представляющее все суффиксы заданной строки. Позволяет быстро решать задачи, связанные с поиском подстрок, такие как поиск вхождений, поиск самых длинных общих подстрок и т.п.
8. R-дерево
Древовидная структура данных для индексации многомерной, в основном пространственной, информации, такой как географические координаты, прямоугольники или многоугольники. Позволяет эффективно выполнять запросы к таким данным, разбивая пространство на перекрывающиеся области с помощью ограничивающих прямоугольников, и организует объекты в узлах дерева для быстрой фильтрации и поиска.
Источник: https://blog.bytebytego.com
👍16
День 2497. #ЗаметкиНаПолях
Используем Выражения Коллекций для Пользовательских Типов
В C#12 появились выражения коллекций — новый упрощённый синтаксис для инициализации коллекций:
Этот синтаксис отлично работает со встроенными типами коллекций, но как насчёт наших типов коллекций? Здесь на помощь приходит атрибут CollectionBuilderAttribute, позволяющий расширить этот современный синтаксис на пользовательские типы.
Пользовательские коллекции
Представьте, что вы создали свой тип коллекции:
Раньше вы не могли использовать синтаксис выражений коллекций для этого типа. Вам приходилось использовать старый способ:
Либо (что не сильно меняет дело):
Не слишком элегантно.
Атрибут CollectionBuilderAttribute устраняет этот разрыв, сообщая компилятору, как конструировать вашу коллекцию из выражения коллекции:
Теперь мы можем использовать:
Компилятор автоматически вызовет метод Create, передав ему элементы.
Как это работает
Атрибут принимает два параметра:
1. Тип построителя – тип, содержащий фабричный метод (
2. Имя метода – имя статического метода, создающего экземпляр (
Метод построителя должен:
- быть статическим;
- принимать либо ReadOnlySpan<T> (предпочтительно), либо T[];
- возвращать экземпляр типа коллекции;
- иметь параметры типа, соответствующие вашей коллекции.
Замечание: также необходимо, чтобы коллекция имела «тип итерации», то есть имела метод
Когда всё это реализовано, компилятор может сгенерировать необходимый код для построения вашей коллекции наиболее эффективным способом.
Источник: https://bartwullems.blogspot.com/2025/11/how-to-uninstall-older-net-core-versions.html
Используем Выражения Коллекций для Пользовательских Типов
В C#12 появились выражения коллекций — новый упрощённый синтаксис для инициализации коллекций:
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Этот синтаксис отлично работает со встроенными типами коллекций, но как насчёт наших типов коллекций? Здесь на помощь приходит атрибут CollectionBuilderAttribute, позволяющий расширить этот современный синтаксис на пользовательские типы.
Пользовательские коллекции
Представьте, что вы создали свой тип коллекции:
public class MyCollection<T>
{
private readonly List<T> _items;
public MyCollection(ReadOnlySpan<T> items) =>
_items = [.. items];
public IEnumerator<T> GetEnumerator()
=> _items.GetEnumerator();
// другие члены…
}
Раньше вы не могли использовать синтаксис выражений коллекций для этого типа. Вам приходилось использовать старый способ:
var myCol =
new MyCollection<int>(new[] { 1, 2, 3, 4, 5 });
Либо (что не сильно меняет дело):
var myCol =
new MyCollection<int>([1, 2, 3, 4, 5]);
Не слишком элегантно.
Атрибут CollectionBuilderAttribute устраняет этот разрыв, сообщая компилятору, как конструировать вашу коллекцию из выражения коллекции:
[CollectionBuilder(typeof(MyCollectionBuilder),
nameof(MyCollectionBuilder.Create))]
public class MyCollection<T>
{
//…
}
public static class MyCollectionBuilder
{
public static MyCollection<T>
Create<T>(ReadOnlySpan<T> items)
=> new([..items]);
}
Теперь мы можем использовать:
MyCollection<int> myCol = [1, 2, 3, 4, 5];
Компилятор автоматически вызовет метод Create, передав ему элементы.
Как это работает
Атрибут принимает два параметра:
1. Тип построителя – тип, содержащий фабричный метод (
typeof(MyCollectionBuilder));2. Имя метода – имя статического метода, создающего экземпляр (
"Create").Метод построителя должен:
- быть статическим;
- принимать либо ReadOnlySpan<T> (предпочтительно), либо T[];
- возвращать экземпляр типа коллекции;
- иметь параметры типа, соответствующие вашей коллекции.
Замечание: также необходимо, чтобы коллекция имела «тип итерации», то есть имела метод
GetEnumerator(), возвращающий IEnumerator (или IEnumerator<T>). Можно либо реализовать интерфейс IEnumerable или IEnumerable<T>, либо просто добавить метод GetEnumerator().Когда всё это реализовано, компилятор может сгенерировать необходимый код для построения вашей коллекции наиболее эффективным способом.
Источник: https://bartwullems.blogspot.com/2025/11/how-to-uninstall-older-net-core-versions.html
👍19
День 2498. #Architecture
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Начало
Архитектура Вертикальных Срезов (Vertical Slice Architecture) кажется очень удобной, когда вы с ней впервые сталкиваетесь. Не нужно прыгать между семью уровнями, чтобы добавить одно поле, не нужны десятки проектов в решении. Но при реализации более сложных функций начинают проявляться недостатки.
Допустим, у нас есть срезы CreateOrder, UpdateOrder и GetOrder. Внезапно возникает повторение: логика проверки адреса заказа в 3х местах. А подсчёт подытога, налогов и итога нужен как для корзины, так и для оформления заказа. Хочется создать общий проект или папку SharedServices. Это самый критический момент во внедрении VSA. Выберете неправильный вариант, и снова создадите связанность, от которой пытались избавиться. Выберите правильный - сохраните независимость, которой отличается VSA.
Чистая архитектура устанавливает строгие ограничения. Она точно определяет, где находится код:
- сущности - в домене (Domain),
- интерфейсы — в приложении (Application),
- реализации — в инфраструктуре (Infrastructure).
Это безопасно, предотвращает ошибки, но также препятствует использованию обходных путей, даже когда они уместны.
VSA устраняет ограничения. Она гласит: «Организуйте код по функциям, а не по техническим особенностям». Это обеспечивает скорость и гибкость, но перекладывает бремя дисциплины на вас. Что делать?
Ловушка: «Общий» мусорный ящик
Путь наименьшего сопротивления — создать проект (или папку) с именем Shared, Common или Utils. Это почти всегда ошибка.
Представьте проект Common.Services с классом OrderCalculationService:
- метод для итогов корзины (используется Cart),
- для истории доходов (используется Reporting),
- метод для форматирования счетов (используется Invoices).
3 несвязанные задачи. 3 разные причины изменений. Класс, объединяющий всё.
Общий проект неизбежно становится мусорным ящиком для всего, чему вы не удосужились дать правильное название. Это создает запутанную сеть зависимостей, где несвязанные функции связаны друг с другом, потому что они случайно используют один и тот же вспомогательный метод. Мы вернули связанность, от которой пытались избавиться.
Схема принятия решений
В потенциальной ситуации совместного использования кода нужно задать 3 вопроса:
1. Это инфраструктурный или доменный код?
Инфраструктура (контексты БД, ведение журнала, HTTP-клиенты) почти всегда используются из нескольких мест. Концепции домена требуют более тщательного изучения (об этом позже).
2. Насколько стабильна концепция?
Если меняется раз в год – можно кидать в общий код. Если при каждом запросе на новую функцию, оставьте её локальной.
3. Нарушается ли «правило трёх»?
Дублирование кода 1 раз допустимо. Создание 3х копий должно насторожить. Не абстрагируйтесь, пока не достигнете трёх.
Мы решаем эту проблему рефакторингом кода. Далее рассмотрим несколько примеров.
Продолжение следует…
Источник: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Начало
Архитектура Вертикальных Срезов (Vertical Slice Architecture) кажется очень удобной, когда вы с ней впервые сталкиваетесь. Не нужно прыгать между семью уровнями, чтобы добавить одно поле, не нужны десятки проектов в решении. Но при реализации более сложных функций начинают проявляться недостатки.
Допустим, у нас есть срезы CreateOrder, UpdateOrder и GetOrder. Внезапно возникает повторение: логика проверки адреса заказа в 3х местах. А подсчёт подытога, налогов и итога нужен как для корзины, так и для оформления заказа. Хочется создать общий проект или папку SharedServices. Это самый критический момент во внедрении VSA. Выберете неправильный вариант, и снова создадите связанность, от которой пытались избавиться. Выберите правильный - сохраните независимость, которой отличается VSA.
Чистая архитектура устанавливает строгие ограничения. Она точно определяет, где находится код:
- сущности - в домене (Domain),
- интерфейсы — в приложении (Application),
- реализации — в инфраструктуре (Infrastructure).
Это безопасно, предотвращает ошибки, но также препятствует использованию обходных путей, даже когда они уместны.
VSA устраняет ограничения. Она гласит: «Организуйте код по функциям, а не по техническим особенностям». Это обеспечивает скорость и гибкость, но перекладывает бремя дисциплины на вас. Что делать?
Ловушка: «Общий» мусорный ящик
Путь наименьшего сопротивления — создать проект (или папку) с именем Shared, Common или Utils. Это почти всегда ошибка.
Представьте проект Common.Services с классом OrderCalculationService:
- метод для итогов корзины (используется Cart),
- для истории доходов (используется Reporting),
- метод для форматирования счетов (используется Invoices).
3 несвязанные задачи. 3 разные причины изменений. Класс, объединяющий всё.
Общий проект неизбежно становится мусорным ящиком для всего, чему вы не удосужились дать правильное название. Это создает запутанную сеть зависимостей, где несвязанные функции связаны друг с другом, потому что они случайно используют один и тот же вспомогательный метод. Мы вернули связанность, от которой пытались избавиться.
Схема принятия решений
В потенциальной ситуации совместного использования кода нужно задать 3 вопроса:
1. Это инфраструктурный или доменный код?
Инфраструктура (контексты БД, ведение журнала, HTTP-клиенты) почти всегда используются из нескольких мест. Концепции домена требуют более тщательного изучения (об этом позже).
2. Насколько стабильна концепция?
Если меняется раз в год – можно кидать в общий код. Если при каждом запросе на новую функцию, оставьте её локальной.
3. Нарушается ли «правило трёх»?
Дублирование кода 1 раз допустимо. Создание 3х копий должно насторожить. Не абстрагируйтесь, пока не достигнете трёх.
Мы решаем эту проблему рефакторингом кода. Далее рассмотрим несколько примеров.
Продолжение следует…
Источник: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
👍12
День 2499. #Architecture
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Продолжение
Начало
Три уровня общего кода
Вместо бинарного разделения «общий/локальный код» мыслите тремя уровнями.
Уровень 1. Техническая инфраструктура (спокойно используйте общий код)
Чистая инфраструктура, одинаково влияющая на все срезы: логеры, фабрики подключений к БД, промежуточное ПО аутентификации, шаблон Result, конвейеры валидации и т.п. Централизуйте всё это в проекте Shared.Kernel или Infrastructure. Обратите внимание, что это также может быть папка внутри проекта. Такой код редко меняется в связи с бизнес-требованиями.
Уровень 2. Концепции домена (используйте общий код)
Вместо того, чтобы разбрасывать бизнес-правила по срезам, передавайте их в сущности и объекты-значения:
CancelOrder, GetOrder и UpdateOrder все используют одни бизнес-правила. Логика живёт в одном месте. Т.е. разные вертикальные срезы могут использовать одну и ту же модель домена.
Уровень 3: Логика, специфичная для конкретного объекта (сохраняйте её локально)
Логика, общая для связанных срезов, таких как CreateOrder и UpdateOrder, не обязательно должна быть глобальной. Создайте общую папку (из каждого правила есть исключения) внутри функции:
Это также имеет скрытое преимущество. Если вы удалите функцию Orders, общая логика будет удалена вместе с ней. Никакого зомби-кода.
Наконец рассмотрим несколько сложных сценариев, которые большинство упускают из виду.
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Продолжение
Начало
Три уровня общего кода
Вместо бинарного разделения «общий/локальный код» мыслите тремя уровнями.
Уровень 1. Техническая инфраструктура (спокойно используйте общий код)
Чистая инфраструктура, одинаково влияющая на все срезы: логеры, фабрики подключений к БД, промежуточное ПО аутентификации, шаблон Result, конвейеры валидации и т.п. Централизуйте всё это в проекте Shared.Kernel или Infrastructure. Обратите внимание, что это также может быть папка внутри проекта. Такой код редко меняется в связи с бизнес-требованиями.
// Техническое ядро
public readonly record struct Result
{
public bool IsSuccess { get; }
public string Error { get; }
private Result(bool isSuccess, string error)
{
IsSuccess = isSuccess;
Error = error;
}
public static Result Success()
=> new(true, string.Empty);
public static Result Failure(string error)
=> new(false, error);
}
Уровень 2. Концепции домена (используйте общий код)
Вместо того, чтобы разбрасывать бизнес-правила по срезам, передавайте их в сущности и объекты-значения:
// Сущность с бизнес-логикой
public class Order
{
public Guid Id { get; set; }
public OrderStatus Status { get; set; }
public List<OrderLine> Lines { get; set; }
public bool CanBeCancelled() =>
Status == OrderStatus.Pending;
public Result Cancel()
{
if (!CanBeCancelled())
return Result.Failure("Нельзя отменить подтверждённый заказ.");
Status = OrderStatus.Cancelled;
return Result.Success();
}
}
CancelOrder, GetOrder и UpdateOrder все используют одни бизнес-правила. Логика живёт в одном месте. Т.е. разные вертикальные срезы могут использовать одну и ту же модель домена.
Уровень 3: Логика, специфичная для конкретного объекта (сохраняйте её локально)
Логика, общая для связанных срезов, таких как CreateOrder и UpdateOrder, не обязательно должна быть глобальной. Создайте общую папку (из каждого правила есть исключения) внутри функции:
📂 Features
└📂 Orders
├📂 CreateOrder
├📂 UpdateOrder
├📂 GetOrder
└📂 Shared
├📄 OrderValidator.cs
└📄 OrderPricingService.cs
Это также имеет скрытое преимущество. Если вы удалите функцию Orders, общая логика будет удалена вместе с ней. Никакого зомби-кода.
Наконец рассмотрим несколько сложных сценариев, которые большинство упускают из виду.
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
👍9
День 2500. #Architecture
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Окончание
Начало
Продолжение
Совместное использование между функциями
Что насчёт совместного использования кода между несвязанными функциями в VSA? CreateOrder должен проверять наличие клиента. GenerateInvoice должен рассчитывать налог. И Orders и Customers должны форматировать уведомления. Это не совсем вписывается в папку Shared определённой функции. Куда деть эту логику?
1. Действительно ли нужен общий код?
Большинство случаев «совместного использования» между функциями — это просто замаскированный доступ к данным. Если CreateOrder нужны данные о клиентах, он напрямую обращается к БД. Он не обращается к функции Customers. Каждый срез владеет своим доступом к данным. Сущность Customer является общей (находится в домене), но Orders и Customers не имеют общего сервиса.
2. Когда действительно нужна общая логика
- Логика домена (бизнес-правила, калькуляции) → Domain/Services
- Инфраструктура (внешние API, форматирование) → Infrastructure/Services
CreateOrder и GenerateInvoice могут использовать этот код без привязки друг к другу.
Прежде чем создавать какой-либо кросс-функциональный сервис, спросите себя: может ли эта логика размещаться в доменной сущности? Большая часть «общей бизнес-логики» на самом деле представляет собой доступ к данным, доменную логику, принадлежащую сущности, или преждевременную абстракцию.
Если нужно вызвать побочный эффект в другой функции, лучше использовать сообщения и события. В качестве альтернативы, функция, которую вы хотите вызвать, может использовать фасад (открытый API) для этой операции.
3. Когда дублирование — правильный выбор
Иногда «общий» код только кажется общим:
Они идентичны. Возникает соблазн создать SharedOrderDto. Не поддавайтесь ему.
На следующей неделе в GetOrder понадобится URL трекинга. Но CreateOrder выполняется до отправки, поэтому не имеет URL. Если бы используете общий класс DTO, у вас появится свойство, допускающее NULL, которое в половине случаев остаётся пустым, что сбивает с толку. Дублирование дешевле, чем неправильная абстракция.
Структура Проекта
Вот как может выглядеть структура проекта VSA:
-
-
-
-
-
Правила
1. Функции владеют своими моделями запросов/ответов. Исключений нет.
2. Перемещайте бизнес-логику в домен. Сущности и объекты-значения — лучшее место для совместного использования бизнес-правил.
3. Сохраняйте совместное использование семейств функций локальным. Если это нужно только срезам Order, храните его в Features/Orders/Shared (не стесняйтесь найти более подходящее название, чем Shared).
4. Инфраструктура по умолчанию является общей. Контексты БД, HTTP-клиенты, ведение журнала. Это технические вопросы.
5. Применяйте правило трёх. Не извлекайте данные, пока не найдёте 3 реальных использования идентичной, стабильной логики.
Источник: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Окончание
Начало
Продолжение
Совместное использование между функциями
Что насчёт совместного использования кода между несвязанными функциями в VSA? CreateOrder должен проверять наличие клиента. GenerateInvoice должен рассчитывать налог. И Orders и Customers должны форматировать уведомления. Это не совсем вписывается в папку Shared определённой функции. Куда деть эту логику?
1. Действительно ли нужен общий код?
Большинство случаев «совместного использования» между функциями — это просто замаскированный доступ к данным. Если CreateOrder нужны данные о клиентах, он напрямую обращается к БД. Он не обращается к функции Customers. Каждый срез владеет своим доступом к данным. Сущность Customer является общей (находится в домене), но Orders и Customers не имеют общего сервиса.
2. Когда действительно нужна общая логика
- Логика домена (бизнес-правила, калькуляции) → Domain/Services
- Инфраструктура (внешние API, форматирование) → Infrastructure/Services
// Domain/Services/TaxCalculator.cs
public class TaxCalculator
{
public decimal CalculateTax(
Address address, decimal subtotal)
{
var rate = GetTaxRate(address.State, address.Country);
return subtotal * rate;
}
}
CreateOrder и GenerateInvoice могут использовать этот код без привязки друг к другу.
Прежде чем создавать какой-либо кросс-функциональный сервис, спросите себя: может ли эта логика размещаться в доменной сущности? Большая часть «общей бизнес-логики» на самом деле представляет собой доступ к данным, доменную логику, принадлежащую сущности, или преждевременную абстракцию.
Если нужно вызвать побочный эффект в другой функции, лучше использовать сообщения и события. В качестве альтернативы, функция, которую вы хотите вызвать, может использовать фасад (открытый API) для этой операции.
3. Когда дублирование — правильный выбор
Иногда «общий» код только кажется общим:
// Features/Orders/GetOrder
public record GetOrderResponse(Guid Id, decimal Total, string Status);
// Features/Orders/CreateOrder
public record CreateOrderResponse(Guid Id, decimal Total, string Status);
Они идентичны. Возникает соблазн создать SharedOrderDto. Не поддавайтесь ему.
На следующей неделе в GetOrder понадобится URL трекинга. Но CreateOrder выполняется до отправки, поэтому не имеет URL. Если бы используете общий класс DTO, у вас появится свойство, допускающее NULL, которое в половине случаев остаётся пустым, что сбивает с толку. Дублирование дешевле, чем неправильная абстракция.
Структура Проекта
Вот как может выглядеть структура проекта VSA:
📂 src
└📂 Features
│ ├📂 Orders
│ │ ├📂 CreateOrder
│ │ ├📂 UpdateOrder
│ │ └📂 Shared
│ ├📂 Customers
│ │ ├📂 GetCustomer
│ │ └📂 Shared
│ └📂 Invoices
│ └📂 GenerateInvoice
└📂 Domain
│ ├📂 Entities
│ ├📂 ValueObjects
│ └📂 Services
└📂 Infrastructure
│ ├📂 Persistence
│ └📂 Services
└📂 Shared
└📂 Behaviors
-
Features — Изолированные срезы. Каждый со своими моделями запроса/ответа. -
Features/[Name]/Shared — Локальный общий код между связанными срезами.-
Domain — Сущности, объекты-значения и доменные сервисы. Общая бизнес-логика живёт здесь.-
Infrastructure — Технические вопросы.-
Shared — Только общее поведение.Правила
1. Функции владеют своими моделями запросов/ответов. Исключений нет.
2. Перемещайте бизнес-логику в домен. Сущности и объекты-значения — лучшее место для совместного использования бизнес-правил.
3. Сохраняйте совместное использование семейств функций локальным. Если это нужно только срезам Order, храните его в Features/Orders/Shared (не стесняйтесь найти более подходящее название, чем Shared).
4. Инфраструктура по умолчанию является общей. Контексты БД, HTTP-клиенты, ведение журнала. Это технические вопросы.
5. Применяйте правило трёх. Не извлекайте данные, пока не найдёте 3 реальных использования идентичной, стабильной логики.
Источник: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
👍16
День 2501.
Сегодня порекомендую видео от моего любимого докладчика Дилана Бити. Недавно на конференции NDC в Копенгагене он выступал с докладом “Algorithms Demystified”.
Бывало ли у вас, что вы застревали на какой-то проблеме с кодом? Возможно, реализовывали какую-то функцию в одном из своих проектов, или решали задачи на LeetCode или Advent of Code, и застряли. Вы не можете понять, как получить нужный результат. Поэтому спрашиваете у коллег, на Stack Overflow или Reddit, и получаете ответ вроде: «Да тут просто надо использовать алгоритм Дейкстры»… и ваш мозг зависает. Использовать что? Вы гуглите и обнаруживаете, что это «поиск кратчайших путей между узлами во взвешенном графе», и теперь нужно узнать, что такое «узел» и что такое «взвешенный граф»… а затем понять, как превратить всё это в работающий код.
Алгоритмы — ключ ко всем видам функций и систем, от сетей до автокоррекции, и понимание того, как они работают, поможет вам создавать более качественное программное обеспечение, исправлять едва заметные ошибки и решать задачи на Advent of Code. В этом докладе Дилан расскажет о некоторых из его любимых алгоритмов, объяснит их важность и поможет вам понять, что они делают и как.
Сегодня порекомендую видео от моего любимого докладчика Дилана Бити. Недавно на конференции NDC в Копенгагене он выступал с докладом “Algorithms Demystified”.
Бывало ли у вас, что вы застревали на какой-то проблеме с кодом? Возможно, реализовывали какую-то функцию в одном из своих проектов, или решали задачи на LeetCode или Advent of Code, и застряли. Вы не можете понять, как получить нужный результат. Поэтому спрашиваете у коллег, на Stack Overflow или Reddit, и получаете ответ вроде: «Да тут просто надо использовать алгоритм Дейкстры»… и ваш мозг зависает. Использовать что? Вы гуглите и обнаруживаете, что это «поиск кратчайших путей между узлами во взвешенном графе», и теперь нужно узнать, что такое «узел» и что такое «взвешенный граф»… а затем понять, как превратить всё это в работающий код.
Алгоритмы — ключ ко всем видам функций и систем, от сетей до автокоррекции, и понимание того, как они работают, поможет вам создавать более качественное программное обеспечение, исправлять едва заметные ошибки и решать задачи на Advent of Code. В этом докладе Дилан расскажет о некоторых из его любимых алгоритмов, объяснит их важность и поможет вам понять, что они делают и как.
YouTube
Algorithms Demystified - Dylan Beattie - NDC Copenhagen 2025
This talk was recorded at NDC Copenhagen in Copenhagen, Denmark. #ndccopenhagen #ndcconferences #developer #softwaredeveloper
Attend the next NDC conference near you:
https://ndcconferences.com
https://ndccopenhagen.com/
Subscribe to our YouTube channel…
Attend the next NDC conference near you:
https://ndcconferences.com
https://ndccopenhagen.com/
Subscribe to our YouTube channel…
51👍11
День 2502. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
11. Различия между современным .NET и .NET Framework
«Объясните ключевые различия между современным .NET и .NET Framework? Также обсудите ситуации, в которых один вариант может быть более подходящим, чем другой».
Хороший ответ
Современный .NET и .NET Framework — это фреймворки, разработанные Microsoft, но они предназначены для разных потребностей и сценариев.
- Поддержка кроссплатформенности: современный .NET кроссплатформенный, поддерживает Windows, Linux и macOS, что делает его подходящим для приложений, требующих широкого охвата в различных операционных системах. .NET Framework же ограничен Windows.
- Производительность и масштабируемость: современный .NET оптимизирован для производительности и масштабируемости. Он включает в себя такие усовершенствования, как веб-сервер Kestrel и компилятор RyuJIT, которые эффективнее и быстрее аналогов из .NET Framework. Это делает современный .NET идеальным для создания высокопроизводительных и масштабируемых веб-приложений.
- Архитектура микросервисов: современный .NET разработан для поддержки архитектуры микросервисов, позволяя разработчикам создавать и развертывать независимо обновляемые и масштабируемые сервисы. Он легковесен и имеет встроенную поддержку технологий контейнеризации, таких как Docker.
- Совместимость: .NET Framework поддерживает более широкий спектр старых API .NET и сторонние библиотеки, которые могут быть недоступны в современной .NET. Этот подход часто выбирают для устаревших приложений, использующих эти API, и для проектов, где совместимость со старыми технологиями .NET критически важна.
Инструменты и обновления: современный .NET выигрывает от параллельного управления версиями, которое позволяет нескольким версиям среды выполнения существовать на одной машине. Это особенно полезно для тестирования новых функций, не затрагивая существующие приложения. С другой стороны, .NET Framework не поддерживает эту функцию и требует обновления единственного экземпляра на хосте.
Подводя итог, можно выделить следующие сценарии, в которых один вариант может быть более подходящим:
- Современный .NET подходит для новых корпоративных приложений, особенно тех, которые требуют кроссплатформенной функциональности, архитектуры микросервисов или требуют работы в контейнерных средах.
- .NET Framework подойдёт для поддержки существующих приложений, использующих старые библиотеки или специфичные для Windows API, которые не поддерживаются современным .NET.
Часто даваемый неудачный ответ
«NET Core — это просто более новая версия .NET Framework, поэтому всегда лучше использовать .NET Core для всех проектов, поскольку он новее и в конечном итоге заменит .NET Framework.»
Этот ответ чрезмерно упрощает различия и игнорирует конкретные сильные стороны и варианты использования каждого фреймворка:
- Непонимание области применения и вариантов использования: Предложение использовать .NET Core или современный .NET для всех проектов игнорирует сценарии, в которых .NET Framework может по-прежнему быть необходим из-за совместимости со старыми технологиями и широкого использования в корпоративном секторе.
- Игнорирование проблем совместимости: Утверждение, что современный .NET заменит .NET Framework, не учитывает тот факт, что некоторые приложения зависят от функций и библиотек, доступных только в .NET Framework.
- Упрощение выбора технологий: Выбор технологий должен основываться на конкретных требованиях, возможностях и стратегических целях, а не просто Выбор чего-то, что новее.
Эта распространённая ошибка происходит из-за отсутствия глубокого понимания экосистем обоих фреймворков, что приводит к неверной оценке их применимости.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
11. Различия между современным .NET и .NET Framework
«Объясните ключевые различия между современным .NET и .NET Framework? Также обсудите ситуации, в которых один вариант может быть более подходящим, чем другой».
Хороший ответ
Современный .NET и .NET Framework — это фреймворки, разработанные Microsoft, но они предназначены для разных потребностей и сценариев.
- Поддержка кроссплатформенности: современный .NET кроссплатформенный, поддерживает Windows, Linux и macOS, что делает его подходящим для приложений, требующих широкого охвата в различных операционных системах. .NET Framework же ограничен Windows.
- Производительность и масштабируемость: современный .NET оптимизирован для производительности и масштабируемости. Он включает в себя такие усовершенствования, как веб-сервер Kestrel и компилятор RyuJIT, которые эффективнее и быстрее аналогов из .NET Framework. Это делает современный .NET идеальным для создания высокопроизводительных и масштабируемых веб-приложений.
- Архитектура микросервисов: современный .NET разработан для поддержки архитектуры микросервисов, позволяя разработчикам создавать и развертывать независимо обновляемые и масштабируемые сервисы. Он легковесен и имеет встроенную поддержку технологий контейнеризации, таких как Docker.
- Совместимость: .NET Framework поддерживает более широкий спектр старых API .NET и сторонние библиотеки, которые могут быть недоступны в современной .NET. Этот подход часто выбирают для устаревших приложений, использующих эти API, и для проектов, где совместимость со старыми технологиями .NET критически важна.
Инструменты и обновления: современный .NET выигрывает от параллельного управления версиями, которое позволяет нескольким версиям среды выполнения существовать на одной машине. Это особенно полезно для тестирования новых функций, не затрагивая существующие приложения. С другой стороны, .NET Framework не поддерживает эту функцию и требует обновления единственного экземпляра на хосте.
Подводя итог, можно выделить следующие сценарии, в которых один вариант может быть более подходящим:
- Современный .NET подходит для новых корпоративных приложений, особенно тех, которые требуют кроссплатформенной функциональности, архитектуры микросервисов или требуют работы в контейнерных средах.
- .NET Framework подойдёт для поддержки существующих приложений, использующих старые библиотеки или специфичные для Windows API, которые не поддерживаются современным .NET.
Часто даваемый неудачный ответ
«NET Core — это просто более новая версия .NET Framework, поэтому всегда лучше использовать .NET Core для всех проектов, поскольку он новее и в конечном итоге заменит .NET Framework.»
Этот ответ чрезмерно упрощает различия и игнорирует конкретные сильные стороны и варианты использования каждого фреймворка:
- Непонимание области применения и вариантов использования: Предложение использовать .NET Core или современный .NET для всех проектов игнорирует сценарии, в которых .NET Framework может по-прежнему быть необходим из-за совместимости со старыми технологиями и широкого использования в корпоративном секторе.
- Игнорирование проблем совместимости: Утверждение, что современный .NET заменит .NET Framework, не учитывает тот факт, что некоторые приложения зависят от функций и библиотек, доступных только в .NET Framework.
- Упрощение выбора технологий: Выбор технологий должен основываться на конкретных требованиях, возможностях и стратегических целях, а не просто Выбор чего-то, что новее.
Эта распространённая ошибка происходит из-за отсутствия глубокого понимания экосистем обоих фреймворков, что приводит к неверной оценке их применимости.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👎4👍2