День 2022. #ЗаметкиНаПолях #EFCore
Полезные Функции EF Core. Окончание
Начало
3. Чистые SQL-запросы
EF Core 8 добавил новую функцию, которая позволяет нам запрашивать несопоставленные (unmapped) типы. Предположим, мы хотим получить данные из представления БД, хранимой процедуры или таблицы, которые напрямую не соответствуют ни одному из классов сущностей контекста БД. Например, получить сводные данные продаж продукта. В EF Core 8 можно определить простой класс и запросить его напрямую:
Метод SqlQuery возвращает IQueryable, что позволяет встраивать чистые SQL-запросы в LINQ-запросы.
Не забывайте использовать параметризованные запросы, чтобы предотвратить SQL-инъекции. Метод SqlQuery принимает FormattableString, что означает, что вы можете безопасно использовать интерполированную строку. Каждый аргумент преобразуется в параметр SQL.
См. подробнее.
4. Фильтры запросов
Фильтры автоматически добавляются в LINQ-запросы, когда вы извлекаете сущности соответствующего типа. Это избавляет от необходимости многократно писать одну и ту же логику фильтрации в нескольких местах.
Обычные сценарии:
- Мягкие удаления: отфильтровывают записи, помеченные как удалённые.
- Мультитенантность: фильтруют данные на основе текущего пользователя.
- Безопасность на уровне строк: ограничивают доступ к определённым записям на основе ролей или разрешений пользователей.
В мультитенантном приложении часто нужно фильтровать данные на основе текущего пользователя:
См. подробнее о мультитенантности с помощью EF Core.
Настройка нескольких фильтров запроса для одной сущности применит только последний. Можно использовать IgnoreQueryFilters, чтобы обойти фильтры в запросах, где это необходимо.
Источник: https://www.milanjovanovic.tech/blog/5-ef-core-features-you-need-to-know
Полезные Функции EF Core. Окончание
Начало
3. Чистые SQL-запросы
EF Core 8 добавил новую функцию, которая позволяет нам запрашивать несопоставленные (unmapped) типы. Предположим, мы хотим получить данные из представления БД, хранимой процедуры или таблицы, которые напрямую не соответствуют ни одному из классов сущностей контекста БД. Например, получить сводные данные продаж продукта. В EF Core 8 можно определить простой класс и запросить его напрямую:
public class ProductSummary
{
public int Id { get; set; }
public string Name { get; set; }
public decimal TotalSales { get; set; }
}
var sums = await context.Database
.SqlQuery<ProductSummary>(
@$"""
SELECT p.Id, p.Name,
SUM(oi.Quantity * oi.UnitPrice) AS TotalSales
FROM Products p
JOIN OrderItems oi ON p.ProductId = oi.ProductId
WHERE p.CategoryId = {categoryId}
GROUP BY p.Id, p.Name
""")
.ToListAsync();
Метод SqlQuery возвращает IQueryable, что позволяет встраивать чистые SQL-запросы в LINQ-запросы.
Не забывайте использовать параметризованные запросы, чтобы предотвратить SQL-инъекции. Метод SqlQuery принимает FormattableString, что означает, что вы можете безопасно использовать интерполированную строку. Каждый аргумент преобразуется в параметр SQL.
См. подробнее.
4. Фильтры запросов
Фильтры автоматически добавляются в LINQ-запросы, когда вы извлекаете сущности соответствующего типа. Это избавляет от необходимости многократно писать одну и ту же логику фильтрации в нескольких местах.
Обычные сценарии:
- Мягкие удаления: отфильтровывают записи, помеченные как удалённые.
- Мультитенантность: фильтруют данные на основе текущего пользователя.
- Безопасность на уровне строк: ограничивают доступ к определённым записям на основе ролей или разрешений пользователей.
В мультитенантном приложении часто нужно фильтровать данные на основе текущего пользователя:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
// Связываем продукты с пользователями
public int TenantId { get; set; }
}
protected override void
OnModelCreating(ModelBuilder mb)
{
// Текущий пользователь задаётся из контекста/запроса
mb.Entity<Product>()
.HasQueryFilter(p => p.TenantId == _currentTenantId);
}
// Запросы автоматически фильтруются по пользователю:
var products = context.Products.ToList();
См. подробнее о мультитенантности с помощью EF Core.
Настройка нескольких фильтров запроса для одной сущности применит только последний. Можно использовать IgnoreQueryFilters, чтобы обойти фильтры в запросах, где это необходимо.
Источник: https://www.milanjovanovic.tech/blog/5-ef-core-features-you-need-to-know
👍26
День 2023. #ЗаметкиНаПолях
Используем Memory для Эффективного Управления Памятью. Начало
В C# разработчики имеют доступ к мощному API Memory<T>, позволяющему гибко и эффективно работать с памятью.
Мы используем Span для безопасного предоставления непрерывной области памяти. Как и Span<T>, Memory<T> представляет собой непрерывную область памяти. Однако она может находиться в куче, а не только в стеке, как Span.
См. также «Различия Между Span и Memory в C#»
Создание Memory
В примере выше memory ссылается на данные в куче, т.к. массивы в C# размещаются в куче.
Memory<T> и асинхронные методы
Здесь мы имитируем асинхронную операцию с помощью Task.Delay(), а затем получаем доступ к данным в памяти с помощью memory.Span и выводим их на консоль. Это было бы невозможно с помощью Span<T>, поскольку мы не можем использовать Span<T> в асинхронном коде из-за того, что это ref struct.
Метод расширения AsMemory()
Метод расширения string.AsMemory() позволяет создавать объект Memory<char> из строки без копирования базовых данных. Это может быть полезно при передаче подстрок методам, которые принимают параметры Memory<T>, без дополнительных выделений памяти:
Мы создаем Memory<char> из строки с помощью AsMemory(), делаем срез этого Memory<char>, чтобы получить новый Memory<char>, представляющий часть исходной строки. И выводим этот срез на консоль, преобразуя его в строку с помощью метода ToString().
Далее рассмотрим продвинутые методы управления памятью.
Окончание следует…
Источник: https://code-maze.com/csharp-using-memory/
Используем Memory для Эффективного Управления Памятью. Начало
В C# разработчики имеют доступ к мощному API Memory<T>, позволяющему гибко и эффективно работать с памятью.
Мы используем Span для безопасного предоставления непрерывной области памяти. Как и Span<T>, Memory<T> представляет собой непрерывную область памяти. Однако она может находиться в куче, а не только в стеке, как Span.
См. также «Различия Между Span и Memory в C#»
Создание Memory
var numbers = new int[] { 1, 2, 3, 4, 5 };
var memory = new Memory<int>(numbers);В примере выше memory ссылается на данные в куче, т.к. массивы в C# размещаются в куче.
Memory<T> и асинхронные методы
public static async Task
ProcessMemoryAsync(Memory<int> memory)
{
await Task.Delay(1000);
for (var i = 0; i < memory.Span.Length; i++)
{
var item = memory.Span[i];
Console.WriteLine(item);
}
}
Здесь мы имитируем асинхронную операцию с помощью Task.Delay(), а затем получаем доступ к данным в памяти с помощью memory.Span и выводим их на консоль. Это было бы невозможно с помощью Span<T>, поскольку мы не можем использовать Span<T> в асинхронном коде из-за того, что это ref struct.
Метод расширения AsMemory()
Метод расширения string.AsMemory() позволяет создавать объект Memory<char> из строки без копирования базовых данных. Это может быть полезно при передаче подстрок методам, которые принимают параметры Memory<T>, без дополнительных выделений памяти:
const string str = "Hello, World!";
var memory = str.AsMemory();
var slice = memory.Slice(7, 5);
Console.WriteLine(slice.ToString());
// World
Мы создаем Memory<char> из строки с помощью AsMemory(), делаем срез этого Memory<char>, чтобы получить новый Memory<char>, представляющий часть исходной строки. И выводим этот срез на консоль, преобразуя его в строку с помощью метода ToString().
Далее рассмотрим продвинутые методы управления памятью.
Окончание следует…
Источник: https://code-maze.com/csharp-using-memory/
👍15
День 2024. #ЗаметкиНаПолях
Используем Memory для Эффективного Управления Памятью. Окончание
Начало
Модели владения и интерфейс IMemoryOwner
Метод MemoryPool<T>.Rent() возвращает интерфейс IMemoryOwner<T>, который действует как владелец блока памяти. Общий пул позволяет сдавать блок памяти в аренду. Когда память больше не используется, владелец блока несёт ответственность за его утилизацию:
Мы арендуем блок памяти из общего пула с помощью MemoryPool<int>.Shared.Rent(). Он возвращает IMemoryOwner<int>, который владеет арендованным блоком. Затем извлекаем Memory<int>, который представляет этот блок памяти через свойство Memory. Мы используем этот блок для хранения некоторых данных, а затем отображаем эти данные.
Директива using отвечает за утилизацию блока и возвращение его в пул, делая его доступным для последующих вызовов Rent().
Реальный сценарий использования Memory<T> в C#
Создадим метод, который считывает данные из файла в арендованный блок памяти, обрабатывает данные, а затем возвращает память в пул:
Как и в предыдущем примере, мы арендуем блок памяти из пула. Затем используем его в качестве буфера для чтения данных из файла с помощью метода FileStream.ReadAsync(). После чтения и обработки данных выводим их на консоль.
Метод дольше по времени, чем обычное чтение файла (например, построчно), но экономит память, переиспользуя её из пула.
Итого
Благодаря возможности выделения памяти как в стеке, так и в куче, совместимости со строками и пулингу памяти, с помощью Memory<T> разработчики могут оптимизировать использование памяти, повысить производительность приложений и создавать более надёжные и масштабируемые программные решения.
Источник: https://code-maze.com/csharp-using-memory/
Используем Memory для Эффективного Управления Памятью. Окончание
Начало
Модели владения и интерфейс IMemoryOwner
Метод MemoryPool<T>.Rent() возвращает интерфейс IMemoryOwner<T>, который действует как владелец блока памяти. Общий пул позволяет сдавать блок памяти в аренду. Когда память больше не используется, владелец блока несёт ответственность за его утилизацию:
using IMemoryOwner<int> owner =
MemoryPool<int>.Shared.Rent(16);
var memory = owner.Memory;
for (var i = 0; i < memory.Length; i++)
memory.Span[i] = i;
foreach (var item in memory.Span)
Console.WriteLine(item);
Мы арендуем блок памяти из общего пула с помощью MemoryPool<int>.Shared.Rent(). Он возвращает IMemoryOwner<int>, который владеет арендованным блоком. Затем извлекаем Memory<int>, который представляет этот блок памяти через свойство Memory. Мы используем этот блок для хранения некоторых данных, а затем отображаем эти данные.
Директива using отвечает за утилизацию блока и возвращение его в пул, делая его доступным для последующих вызовов Rent().
Реальный сценарий использования Memory<T> в C#
Создадим метод, который считывает данные из файла в арендованный блок памяти, обрабатывает данные, а затем возвращает память в пул:
public static async Task
ProcessFileAsync(string path)
{
using var owner =
MemoryPool<byte>.Shared.Rent(4096);
var buffer = owner.Memory[..4096];
await using var stream = File.OpenRead(path);
int bytes;
while ((bytes = await stream.ReadAsync(buffer)) > 0)
{
var data = buffer[..bytes];
for (var i = 0; i < data.Span.Length; i++)
{
var b = data.Span[i];
Console.Write((char)b);
}
Console.WriteLine();
}
}
Как и в предыдущем примере, мы арендуем блок памяти из пула. Затем используем его в качестве буфера для чтения данных из файла с помощью метода FileStream.ReadAsync(). После чтения и обработки данных выводим их на консоль.
Метод дольше по времени, чем обычное чтение файла (например, построчно), но экономит память, переиспользуя её из пула.
Итого
Благодаря возможности выделения памяти как в стеке, так и в куче, совместимости со строками и пулингу памяти, с помощью Memory<T> разработчики могут оптимизировать использование памяти, повысить производительность приложений и создавать более надёжные и масштабируемые программные решения.
Источник: https://code-maze.com/csharp-using-memory/
👍10
День 2025. #BlazorBasics
Основы Blazor. Маршрутизация и Навигация. Начало
Маршрутизация — ключевая функция любого одностраничного приложения. Она позволяет разработчику организовать веб-сайт, а пользователю — перемещаться по разным страницам.
Когда мы используем директиву @page в компоненте Blazor, компилятор добавляет RouteAttribute во время компиляции, делая компонент доступным в качестве маршрута.
Маршрутизатор в Blazor
При создании проекта Blazor из шаблона в файле App.razor создаётся такой код:
Маршрутизатор работает одинаково как в приложениях Blazor Server, так и в WebAssembly. При запуске он считывает сборку приложения (из свойства AppAssembly) и ищет RouteAttribute, применённый к скомпилированным классам компонентов Blazor.
Если маршрут найден, используется компонент RouteView для визуализации представления, связанного с информацией о маршрутизации. Также устанавливается компонент макета по умолчанию - DefaultLayout (он используется для всех страниц, которые явно не указывают макет с помощью директивы @layout).
Если маршрут не найден, отображается простое сообщение об ошибке. Его можно изменить и сделать более дружелюбным к пользователю.
FocusOnNavigate позволяет поместить фокус на элементе HTML после завершения навигации. Можно использовать любой селектор CSS.
Важно: Маршрутизация в Blazor требует, чтобы мы установили тег <base> в разделе заголовка HTML. В шаблоне по умолчанию он устанавливается в файле _Host.cshtml. Измените это значение в зависимости от того, где и как вы размещаете своё приложение.
NavigationManager
NavigationManager позволяет нам переходить с одной страницы на другую в коде C#.
Мы внедрили NavigationManager на страницу при помощи директивы @inject и использовали его в метода NavigateTo, привязанном к элементам списка через событие @onclick. При навигации с использованием NavigationManager мы можем предоставить различные опции (NavigationOptions). Например, обойти клиентскую маршрутизацию и заставить браузер загрузить новую страницу с сервера с помощью свойства ForceLoad. Или заменить запись истории с помощью свойства ReplaceHistoryEntry.
Помимо инициирования навигации, мы также можем прослушивать событие LocationChanged для изменения маршрута и выполнять пользовательский код при навигации на определенную страницу.
Окончание следует…
Источник: https://www.telerik.com/blogs/blazor-basics-blazor-routing-navigation-fundamentals
Основы Blazor. Маршрутизация и Навигация. Начало
Маршрутизация — ключевая функция любого одностраничного приложения. Она позволяет разработчику организовать веб-сайт, а пользователю — перемещаться по разным страницам.
Когда мы используем директиву @page в компоненте Blazor, компилятор добавляет RouteAttribute во время компиляции, делая компонент доступным в качестве маршрута.
Маршрутизатор в Blazor
При создании проекта Blazor из шаблона в файле App.razor создаётся такой код:
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
Маршрутизатор работает одинаково как в приложениях Blazor Server, так и в WebAssembly. При запуске он считывает сборку приложения (из свойства AppAssembly) и ищет RouteAttribute, применённый к скомпилированным классам компонентов Blazor.
Если маршрут найден, используется компонент RouteView для визуализации представления, связанного с информацией о маршрутизации. Также устанавливается компонент макета по умолчанию - DefaultLayout (он используется для всех страниц, которые явно не указывают макет с помощью директивы @layout).
Если маршрут не найден, отображается простое сообщение об ошибке. Его можно изменить и сделать более дружелюбным к пользователю.
FocusOnNavigate позволяет поместить фокус на элементе HTML после завершения навигации. Можно использовать любой селектор CSS.
Важно: Маршрутизация в Blazor требует, чтобы мы установили тег <base> в разделе заголовка HTML. В шаблоне по умолчанию он устанавливается в файле _Host.cshtml. Измените это значение в зависимости от того, где и как вы размещаете своё приложение.
NavigationManager
NavigationManager позволяет нам переходить с одной страницы на другую в коде C#.
@page "/list"
@inject NavigationManager Nav
<ul>
<li @onclick="() => NavigateTo(1)">1</li>
<li @onclick="() => NavigateTo(2)">2</li>
</ul>
@code {
public void NavigateTo(int id)
{
Nav.NavigateTo($"item/{id}");
}
}
Мы внедрили NavigationManager на страницу при помощи директивы @inject и использовали его в метода NavigateTo, привязанном к элементам списка через событие @onclick. При навигации с использованием NavigationManager мы можем предоставить различные опции (NavigationOptions). Например, обойти клиентскую маршрутизацию и заставить браузер загрузить новую страницу с сервера с помощью свойства ForceLoad. Или заменить запись истории с помощью свойства ReplaceHistoryEntry.
Помимо инициирования навигации, мы также можем прослушивать событие LocationChanged для изменения маршрута и выполнять пользовательский код при навигации на определенную страницу.
Окончание следует…
Источник: https://www.telerik.com/blogs/blazor-basics-blazor-routing-navigation-fundamentals
👍9
День 2026. #BlazorBasics
Основы Blazor. Маршрутизация и Навигация. Окончание
Начало
Маршруты
Маршруты страниц Blazor определяются аналогично маршрутам Razor Pages. Они могут включать параметры в фигурных скобках и ограничения параметров, указанные после двоеточия:
@page "/list"
@page "/item/{id}" – параметр id
@page "/item/{id:int}" – целочисленный параметр id
Cтроки запроса
Чтобы компонент страницы мог читать параметр из строки запроса, нужно использовать атрибуты Parameter и SupplyParameterFromQuery:
Когда мы вызываем URL
Компоненты NavLink и NavMenu
Компонент NavLink можно использовать вместо HTML-тега <a>. Он автоматически применяет класс CSS “active” в зависимости от того, соответствует ли его URL текущему URL.
Компонент NavMenu в шаблоне проекта Blazor содержит пример использования компонента NavLink. Вот его упрощённый код:
Поведение сопоставления маршрута с адресом ссылки настраивается с помощью перечисления NavLinkMatch:
- All - ссылка активна, если адрес полностью соответствует адресу текущего компонента.
- Prefix - ссылка активна, если адрес соответствует началу адреса текущего компонента (по умолчанию).
Также можно предоставить пользовательский класс CSS для активного NavLink с помощью свойства ActiveClass (по умолчанию — "active").
Индикация загрузки между переходами страниц
Иногда страницы могут загружаться в течение длительного времени. Мы можем использовать компонент Navigating в компоненте Router, чтобы показать пользовательскую разметку во время перехода между страницами:
Однако лучшим решением будет загружать данные на странице асинхронно, а переходы между страницами делать как можно более быстрыми.
Итого
Маршрутизация и навигация являются основополагающими при работе с одностраничными приложениями. Blazor обеспечивает простую, но мощную реализацию обоих из коробки. Не нужно выбирать один из нескольких вариантов маршрутизации; всё это включено в Blazor. Тем не менее, он предоставляет много места для пользовательского кода и является очень гибким и расширяемым. Он не только обрабатывает простые варианты использования, но и предоставляет API для сложных сценариев.
Источник: https://www.telerik.com/blogs/blazor-basics-blazor-routing-navigation-fundamentals
Основы Blazor. Маршрутизация и Навигация. Окончание
Начало
Маршруты
Маршруты страниц Blazor определяются аналогично маршрутам Razor Pages. Они могут включать параметры в фигурных скобках и ограничения параметров, указанные после двоеточия:
@page "/list"
@page "/item/{id}" – параметр id
@page "/item/{id:int}" – целочисленный параметр id
Cтроки запроса
Чтобы компонент страницы мог читать параметр из строки запроса, нужно использовать атрибуты Parameter и SupplyParameterFromQuery:
@page "/search"
<h3>Результаты поиска</h3>
<p>Вы искали: @FirstName @LastName</p>
@code {
[Parameter]
[SupplyParameterFromQuery]
public string FirstName { get; set; }
[Parameter]
[SupplyParameterFromQuery]
public string LastName { get; set; }
}
Когда мы вызываем URL
/search?firstname=Jon&lastname=Smith, Blazor назначает обоим свойствам соответствующие части строки запроса. Если вы хотите иметь другое имя для свойства, чем в URL, можно использовать свойство Name атрибута SupplyParameterFromQuery, чтобы определить имя параметра строки запроса.Компоненты NavLink и NavMenu
Компонент NavLink можно использовать вместо HTML-тега <a>. Он автоматически применяет класс CSS “active” в зависимости от того, соответствует ли его URL текущему URL.
Компонент NavMenu в шаблоне проекта Blazor содержит пример использования компонента NavLink. Вот его упрощённый код:
<nav>
<NavLink href="" Match="NavLinkMatch.All">
Home
</NavLink>
<NavLink href="counter">
Counter
</NavLink>
…
</nav>
Поведение сопоставления маршрута с адресом ссылки настраивается с помощью перечисления NavLinkMatch:
- All - ссылка активна, если адрес полностью соответствует адресу текущего компонента.
- Prefix - ссылка активна, если адрес соответствует началу адреса текущего компонента (по умолчанию).
Также можно предоставить пользовательский класс CSS для активного NavLink с помощью свойства ActiveClass (по умолчанию — "active").
Индикация загрузки между переходами страниц
Иногда страницы могут загружаться в течение длительного времени. Мы можем использовать компонент Navigating в компоненте Router, чтобы показать пользовательскую разметку во время перехода между страницами:
<Router AppAssembly="@typeof(App).Assembly">
…
<Navigating>
<p>Loading…</p>
</Navigating>
</Router>
Однако лучшим решением будет загружать данные на странице асинхронно, а переходы между страницами делать как можно более быстрыми.
Итого
Маршрутизация и навигация являются основополагающими при работе с одностраничными приложениями. Blazor обеспечивает простую, но мощную реализацию обоих из коробки. Не нужно выбирать один из нескольких вариантов маршрутизации; всё это включено в Blazor. Тем не менее, он предоставляет много места для пользовательского кода и является очень гибким и расширяемым. Он не только обрабатывает простые варианты использования, но и предоставляет API для сложных сценариев.
Источник: https://www.telerik.com/blogs/blazor-basics-blazor-routing-navigation-fundamentals
👍7
День 2027. #УрокиРазработки
Уроки 50 Лет Разработки ПО
Урок 19. Разрабатывайте продукты так, чтобы их легко было использовать правильно и трудно — неправильно
Хорошо продуманный UI делает продукт одновременно простым для правильного использования и трудным для неправильного. Подсказки и пункты меню чётко описывают действия, при этом используется терминология, понятная предполагаемым пользователям. Дизайнер предоставляет варианты возврата, позволяющие пользователю вернуться к предыдущему экрану или запустить выполнение задачи с самого начала, желательно без повторного ввода уже предоставленной информации. Ввод данных осуществляется в логической последовательности. Значения для ввода в каждое поле очевидно вытекают из оформления этих полей: с помощью раскрывающихся списков или из расположенных рядом текстовых инструкций. Помимо удобства использования, дизайнеры также должны учитывать возможность ошибок и стараться предотвращать или помогать исправлять их.
Есть 4 варианта обращения с потенциальными ошибками.
1. Не дать пользователю возможности ошибиться
Предотвращение ошибок — предпочтительная стратегия. Если пользователь должен вводить данные в изначально пустое текстовое поле, то оно предполагает возможность ввода произвольных данных, которые программа обязательно должна проверить. Раскрывающиеся списки (или другие элементы управления) с допустимыми вариантами позволяют ввести только допустимые значения. Не давайте возможности ввести недопустимые варианты. Элементы управления, позволяющие ввести несуществующую дату, вроде 30 февраля или выбрать год истечения карты меньше текущего, логически бессмысленны. Разрешение ввода неверных данных приведёт к ошибке, когда приложение попытается обработать информацию.
2. Затруднить пользователю возможность ошибиться
Если нельзя лишить пользователя возможности ошибиться, то хотя бы затрудните её. Добавьте поясняющий текст, чтобы исключить двусмысленность в понимании. Не заставляйте пользователя вводить одну и ту же информацию дважды, т.к. это удваивает вероятность ошибиться и занимает вдвое больше времени. Например, если форма запрашивает два адреса — доставки и выставления счета, — дайте возможность указать, что они совпадают, установив флажок.
3. Упростить исправление допущенной ошибки
Несмотря на все ваши усилия, ошибки (допущенные пользователем или системой во время работы) всё равно будут возникать. Предусмотрите для пользователя простую возможность исправлять их. Особенно полезны в этом отношении многоуровневая функция отмены/возврата и чёткие, содержательные сообщения, помогающие пользователю исправить любые ошибки. Загадочные числовые коды ошибок HTTP или сетевых сбоев могут помочь при технической диагностике, но бесполезны для обычного пользователя.
4. Просто позволить ошибкам случиться (не делайте так!)
Наименее желательный вариант — позволить ошибкам случиться и заставить пользователя самому разбираться с последствиями. Если пользователь просит запустить некую процедуру, имеющую определённые предварительные условия, которые должны быть выполнены, ПО должно проверять эти условия и помочь пользователю выполнить их, если это необходимо, а не просто продолжать работать в надежде на лучшее. Более того, оно вообще не должно запускать процедуру, если предварительные условия не выполнены. Система должна сама обнаруживать невыполненные предварительные условия и сообщать о них как можно раньше, чтобы не тратить время пользователя впустую. Пользователи ценят системы, которые им понятны, предотвращают или исправляют их ошибки и взаимодействуют с ними, услужливо предоставляя ясные подсказки.
Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
Уроки 50 Лет Разработки ПО
Урок 19. Разрабатывайте продукты так, чтобы их легко было использовать правильно и трудно — неправильно
Хорошо продуманный UI делает продукт одновременно простым для правильного использования и трудным для неправильного. Подсказки и пункты меню чётко описывают действия, при этом используется терминология, понятная предполагаемым пользователям. Дизайнер предоставляет варианты возврата, позволяющие пользователю вернуться к предыдущему экрану или запустить выполнение задачи с самого начала, желательно без повторного ввода уже предоставленной информации. Ввод данных осуществляется в логической последовательности. Значения для ввода в каждое поле очевидно вытекают из оформления этих полей: с помощью раскрывающихся списков или из расположенных рядом текстовых инструкций. Помимо удобства использования, дизайнеры также должны учитывать возможность ошибок и стараться предотвращать или помогать исправлять их.
Есть 4 варианта обращения с потенциальными ошибками.
1. Не дать пользователю возможности ошибиться
Предотвращение ошибок — предпочтительная стратегия. Если пользователь должен вводить данные в изначально пустое текстовое поле, то оно предполагает возможность ввода произвольных данных, которые программа обязательно должна проверить. Раскрывающиеся списки (или другие элементы управления) с допустимыми вариантами позволяют ввести только допустимые значения. Не давайте возможности ввести недопустимые варианты. Элементы управления, позволяющие ввести несуществующую дату, вроде 30 февраля или выбрать год истечения карты меньше текущего, логически бессмысленны. Разрешение ввода неверных данных приведёт к ошибке, когда приложение попытается обработать информацию.
2. Затруднить пользователю возможность ошибиться
Если нельзя лишить пользователя возможности ошибиться, то хотя бы затрудните её. Добавьте поясняющий текст, чтобы исключить двусмысленность в понимании. Не заставляйте пользователя вводить одну и ту же информацию дважды, т.к. это удваивает вероятность ошибиться и занимает вдвое больше времени. Например, если форма запрашивает два адреса — доставки и выставления счета, — дайте возможность указать, что они совпадают, установив флажок.
3. Упростить исправление допущенной ошибки
Несмотря на все ваши усилия, ошибки (допущенные пользователем или системой во время работы) всё равно будут возникать. Предусмотрите для пользователя простую возможность исправлять их. Особенно полезны в этом отношении многоуровневая функция отмены/возврата и чёткие, содержательные сообщения, помогающие пользователю исправить любые ошибки. Загадочные числовые коды ошибок HTTP или сетевых сбоев могут помочь при технической диагностике, но бесполезны для обычного пользователя.
4. Просто позволить ошибкам случиться (не делайте так!)
Наименее желательный вариант — позволить ошибкам случиться и заставить пользователя самому разбираться с последствиями. Если пользователь просит запустить некую процедуру, имеющую определённые предварительные условия, которые должны быть выполнены, ПО должно проверять эти условия и помочь пользователю выполнить их, если это необходимо, а не просто продолжать работать в надежде на лучшее. Более того, оно вообще не должно запускать процедуру, если предварительные условия не выполнены. Система должна сама обнаруживать невыполненные предварительные условия и сообщать о них как можно раньше, чтобы не тратить время пользователя впустую. Пользователи ценят системы, которые им понятны, предотвращают или исправляют их ошибки и взаимодействуют с ними, услужливо предоставляя ясные подсказки.
Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
👍13
День 2028. #ЧтоНовенького #CSharp13
Альтернативный Доступ к Коллекциям
До C#13 ref-структуры не могли быть аргументами параметра-типа в обобщённых типах. Теперь в объявление обобщённого типа после ключевого слова where добавлено «анти-ограничение» allows ref struct, которое сообщает, что аргументом для параметра типа может быть ref-структура. Анти-ограничение оно, потому что по факту оно не ограничивает возможный набор параметров типа, а расширяет его. Это анти-ограничение налагает все доступные для ref-структур правила (вроде запрета размещения в куче) на все экземпляры параметра-типа. Смысл этого изменения также в том, чтобы можно было использовать типы Span в обобщённых-алгоритмах.
Например, таблицы поиска (lookup-таблицы) типа Dictionary<TKey,TValue> и HashSet<T> часто используются в качестве своего рода кэша. Однако, так как ключи таблиц обычно строкового типа, раньше приходилось выделять строку, чтобы обратиться в lookup-таблицу по ключу. Теперь упомянутое выше «анти-ограничение» добавляет новые возможности для этих типов коллекций.
Следующий код считает количество различных слов в тексте, используя так называемые альтернативные ключи словаря:
Сначала мы создаём lookup-словарь counts и указываем, что нам не важен регистр ключей.
Затем, чтобы искать в словаре по spanам, а не по строкам, мы создаём структуру lookup для альтернативного доступа к элементам. Здесь она вот такого длинного типа Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>>. В примере указан тип полностью для демонстрации. В реальном коде это можно заменить на var. Заметьте, что тут используется обобщённый тип AlternateLookup с параметром типа виде ref-структуры ReadOnlySpan<char>.
Далее мы разбиваем текст по «не-словам», используя Regex.EnumerateSplits. В результате получаем перечислитель диапазонов результатов. С его помощью мы делаем срезы исходного текста, которые представляют собой отдельные слова. Это переменная word типа ReadOnlySpan<char>. И вот здесь, чтобы не создавать строк для доступа в словарь counts по строковому ключу, нам и потребуется структура lookup, которая обеспечит доступ к словарю по альтернативному ключу типа ReadOnlySpan<char>. В данном случае мы увеличиваем счётчик для текущего слова.
Теперь мы можем взять текст и посчитать в нём слова:
Источник: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13
Альтернативный Доступ к Коллекциям
До C#13 ref-структуры не могли быть аргументами параметра-типа в обобщённых типах. Теперь в объявление обобщённого типа после ключевого слова where добавлено «анти-ограничение» allows ref struct, которое сообщает, что аргументом для параметра типа может быть ref-структура. Анти-ограничение оно, потому что по факту оно не ограничивает возможный набор параметров типа, а расширяет его. Это анти-ограничение налагает все доступные для ref-структур правила (вроде запрета размещения в куче) на все экземпляры параметра-типа. Смысл этого изменения также в том, чтобы можно было использовать типы Span в обобщённых-алгоритмах.
Например, таблицы поиска (lookup-таблицы) типа Dictionary<TKey,TValue> и HashSet<T> часто используются в качестве своего рода кэша. Однако, так как ключи таблиц обычно строкового типа, раньше приходилось выделять строку, чтобы обратиться в lookup-таблицу по ключу. Теперь упомянутое выше «анти-ограничение» добавляет новые возможности для этих типов коллекций.
Следующий код считает количество различных слов в тексте, используя так называемые альтернативные ключи словаря:
static Dictionary<string, int>
CountWords(ReadOnlySpan<char> input)
{
Dictionary<string, int> counts =
new(StringComparer.OrdinalIgnoreCase);
Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>>
lookup = counts.GetAlternateLookup<string, int, ReadOnlySpan<char>>();
foreach (Range r in Regex.EnumerateSplits(input, @"\b\W+\b"))
{
ReadOnlySpan<char> word = input[r];
lookup[word] = lookup
.TryGetValue(word, out int count) ? count + 1 : 1;
}
return counts;
}
Сначала мы создаём lookup-словарь counts и указываем, что нам не важен регистр ключей.
Затем, чтобы искать в словаре по spanам, а не по строкам, мы создаём структуру lookup для альтернативного доступа к элементам. Здесь она вот такого длинного типа Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>>. В примере указан тип полностью для демонстрации. В реальном коде это можно заменить на var. Заметьте, что тут используется обобщённый тип AlternateLookup с параметром типа виде ref-структуры ReadOnlySpan<char>.
Далее мы разбиваем текст по «не-словам», используя Regex.EnumerateSplits. В результате получаем перечислитель диапазонов результатов. С его помощью мы делаем срезы исходного текста, которые представляют собой отдельные слова. Это переменная word типа ReadOnlySpan<char>. И вот здесь, чтобы не создавать строк для доступа в словарь counts по строковому ключу, нам и потребуется структура lookup, которая обеспечит доступ к словарю по альтернативному ключу типа ReadOnlySpan<char>. В данном случае мы увеличиваем счётчик для текущего слова.
Теперь мы можем взять текст и посчитать в нём слова:
var text = "<какой-то длинный текст>";
foreach (var word in CountWords(text))
Console.WriteLine(word);
Источник: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13
👍20
День 2029. #Оффтоп
«У Нас Кончились Столбцы» — Лучшая Худшая Кодовая База. Начало
Все мы мечтаем работать с передовыми технологиями, современными системами, чистым и понятным кодом. Однако, если вы приходите в компанию, где кодовой базе исполнилось 10+ лет, вы неизбежно столкнётесь с тем, что взорвёт ваш мозг (не спрашивайте, откуда я знаю). Но эта история поразила даже меня. Текст довольно длинный, будет разбит на несколько постов, но я решил его не сокращать, чтобы не лишать вас удовольствия.
Когда я начал программировать в детстве, я не знал, что людям платят за программирование. Даже когда я окончил среднюю школу, я предполагал, что мир «профессиональной разработки» выглядит совсем иначе, чем код, который я писал в свободное время. Когда мне повезло попасть на свою первую работу в сфере ПО, я быстро понял, насколько я был прав и неправ. Моя первая работа была испытанием огнём, и по сей день эта кодовая база остается худшей и лучшей кодовой базой, над которой мне когда-либо доводилось работать. Хотя кодовая база навсегда останется в этой конкретной компании, я хочу поделиться с вами некоторыми из её самых забавных и страшных историй.
База данных живёт вечно
В большой устаревшей системе БД — больше, чем место для хранения данных, она — источник культуры. БД устанавливает ограничения на то, как работает система в целом. Это точка, где встречается весь код. БД — это место водопоя, и в нашем случае он был довольно загрязнён.
Вы знаете, что у SQL Server есть ограничение на количество столбцов в таблице? Я тоже не знал. В то время их было 1024, сегодня, похоже, 4096. Само собой, большинству людей это знать не нужно. Мы знали. Причина в том, что в Merchants (наша таблица для хранения информации о клиентах) давно закончились столбцы. Решением стала Merchants2. Таблица с (если правильно помню) 500+ столбцами.
Merchants (и её лучшая подруга Merchants2) были источником жизненной силы системы. Всё крутилось вокруг Merchants так или иначе. Но Merchants не была единственной (или двойной) таблицей. Было много правильно нормализованных таблиц, все с внешними ключами к Merchants. Но одна из них всегда будет занимать особое место в моем сердце, SequenceKey.
SequenceKey
Для простоты понимания я воссоздал всю таблицу SequenceKey выше. Да. Вы правильно прочитали, это вся таблица. Таблица с одним ключом и одним значением. Можно сказать, что SequenceKey - идеальная таблица. Что может быть проще?
Но вы можете спросить себя, какое возможное применение может иметь таблица с одним столбцом и строкой? Генерация идентификаторов. Как мне сказали в то время, когда-то давно SQL Server не поддерживал автоинкрементные идентификаторы. Это был принятое, правильное решение. Мои попытки выяснить, правда ли это, были безрезультатными. Но на практике её роль была гораздо большей.
SequenceKey была связующим звеном. В каждой хранимой процедуре, которая создавала новые сущности, вы сначала брали ключ из SequenceKey, увеличивали его. А затем вставляли его в качестве идентификатора в N разных таблиц. Теперь у вас было неявное соединение между всеми этими таблицами сущностей. Если вы видели идентификатор в системе, то была большая вероятность, что связанные таблицы будут иметь строку с точно таким же идентификатором. Честно говоря, довольно умно.
Календарь
База данных может существовать вечно, но наша система входа была ограничена календарём. Я не имею в виду настоящий календарь. Я имею в виду таблицу базы данных под названием calendar. Что она содержала? Заполненный вручную календарь. Когда я спросил нашего местного гуру (которого звали Мунч), он сообщил мне, что, когда календарь заканчивается, мы не можем входить в систему. Это произошло несколько лет назад. Поэтому они заставили стажёра заполнить ещё 5 лет, чтобы убедиться, что этого не произойдет в ближайшее время. Какая система использовала этот календарь? Никто не знал.
Продолжение следует…
Источник: https://jimmyhmiller.github.io/ugliest-beautiful-codebase
Автор оригинала: Jimmy Miller
«У Нас Кончились Столбцы» — Лучшая Худшая Кодовая База. Начало
Все мы мечтаем работать с передовыми технологиями, современными системами, чистым и понятным кодом. Однако, если вы приходите в компанию, где кодовой базе исполнилось 10+ лет, вы неизбежно столкнётесь с тем, что взорвёт ваш мозг (не спрашивайте, откуда я знаю). Но эта история поразила даже меня. Текст довольно длинный, будет разбит на несколько постов, но я решил его не сокращать, чтобы не лишать вас удовольствия.
Когда я начал программировать в детстве, я не знал, что людям платят за программирование. Даже когда я окончил среднюю школу, я предполагал, что мир «профессиональной разработки» выглядит совсем иначе, чем код, который я писал в свободное время. Когда мне повезло попасть на свою первую работу в сфере ПО, я быстро понял, насколько я был прав и неправ. Моя первая работа была испытанием огнём, и по сей день эта кодовая база остается худшей и лучшей кодовой базой, над которой мне когда-либо доводилось работать. Хотя кодовая база навсегда останется в этой конкретной компании, я хочу поделиться с вами некоторыми из её самых забавных и страшных историй.
База данных живёт вечно
В большой устаревшей системе БД — больше, чем место для хранения данных, она — источник культуры. БД устанавливает ограничения на то, как работает система в целом. Это точка, где встречается весь код. БД — это место водопоя, и в нашем случае он был довольно загрязнён.
Вы знаете, что у SQL Server есть ограничение на количество столбцов в таблице? Я тоже не знал. В то время их было 1024, сегодня, похоже, 4096. Само собой, большинству людей это знать не нужно. Мы знали. Причина в том, что в Merchants (наша таблица для хранения информации о клиентах) давно закончились столбцы. Решением стала Merchants2. Таблица с (если правильно помню) 500+ столбцами.
Merchants (и её лучшая подруга Merchants2) были источником жизненной силы системы. Всё крутилось вокруг Merchants так или иначе. Но Merchants не была единственной (или двойной) таблицей. Было много правильно нормализованных таблиц, все с внешними ключами к Merchants. Но одна из них всегда будет занимать особое место в моем сердце, SequenceKey.
SequenceKey
------------
SequenceKey
------------
1251238
------------
Для простоты понимания я воссоздал всю таблицу SequenceKey выше. Да. Вы правильно прочитали, это вся таблица. Таблица с одним ключом и одним значением. Можно сказать, что SequenceKey - идеальная таблица. Что может быть проще?
Но вы можете спросить себя, какое возможное применение может иметь таблица с одним столбцом и строкой? Генерация идентификаторов. Как мне сказали в то время, когда-то давно SQL Server не поддерживал автоинкрементные идентификаторы. Это был принятое, правильное решение. Мои попытки выяснить, правда ли это, были безрезультатными. Но на практике её роль была гораздо большей.
SequenceKey была связующим звеном. В каждой хранимой процедуре, которая создавала новые сущности, вы сначала брали ключ из SequenceKey, увеличивали его. А затем вставляли его в качестве идентификатора в N разных таблиц. Теперь у вас было неявное соединение между всеми этими таблицами сущностей. Если вы видели идентификатор в системе, то была большая вероятность, что связанные таблицы будут иметь строку с точно таким же идентификатором. Честно говоря, довольно умно.
Календарь
База данных может существовать вечно, но наша система входа была ограничена календарём. Я не имею в виду настоящий календарь. Я имею в виду таблицу базы данных под названием calendar. Что она содержала? Заполненный вручную календарь. Когда я спросил нашего местного гуру (которого звали Мунч), он сообщил мне, что, когда календарь заканчивается, мы не можем входить в систему. Это произошло несколько лет назад. Поэтому они заставили стажёра заполнить ещё 5 лет, чтобы убедиться, что этого не произойдет в ближайшее время. Какая система использовала этот календарь? Никто не знал.
Продолжение следует…
Источник: https://jimmyhmiller.github.io/ugliest-beautiful-codebase
Автор оригинала: Jimmy Miller
👍29
День 2030. #Оффтоп
«У Нас Кончились Столбцы» — Лучшая Худшая Кодовая База. Продолжение
Начало
Сотрудники
Каждое утро в 7:15 таблица сотрудников удалялась. Все данные полностью исчезали. Затем в таблицу загружался csv-файл из adp. В это время мы не могли войти в систему. Иногда этот процесс давал сбой. Но это был не конец процесса. Данные нужно было реплицировать в штаб-квартиру. Поэтому электронное письмо отправлялось человеку, который каждый день нажимал кнопку, чтобы скопировать данные.
Заменяющая база данных
Вы могли бы подумать: неужели никому не пришло в голову почистить эту БД? Сделать её более удобной для работы? Да. Была копия БД. Данные в этой копии отставали примерно на 10 минут. Синхронизация работала только в одну сторону, но эта база данных была нормализована. Насколько нормализована? Чтобы перейти от продавца к его номеру телефона, требовалось соединить 7 таблиц.
Цифры продаж
У каждого продавца была квота, которую ему нужно было выполнить в месяц, называемая «победой». Таблицы, в которых хранились эти данные (не финансовые данные, а способ учёта продаж), были невероятно сложными. Каждый день назначенное задание выясняло, какие строки были добавлены и обновлены, и синхронизировало их с какой-то системой в штаб-квартире. Это не было проблемой, пока один продавец не понял, что можно попросить вручную изменить эти записи.
Он уже получил свою «победу» и заключил ещё одну большую сделку в том же месяце. Он хотел, чтобы её перенесли на следующий месяц. Это было поручено стажёру. Слухи разошлись, и в течение следующих 3 лет запросы росли в геометрической прогрессии. В какой-то момент у нас было 3 стажёра, чья постоянная работа заключалась в написании этих SQL-запросов. Написание приложения для этого посчитали слишком сложным. Однако перед уходом я помог этим стажерам создать его. Хотя понятия не имею, использовали ли они его.
Кодовая база
Но что такое БД без кодовой базы. Какая же это была великолепная кодовая база. Когда я пришёл, всё было в Team Foundation Server. Если вы не в курсе, это централизованная система управления исходным кодом от Microsoft. Основная кодовая база, с которой я работал, была наполовину VB, наполовину C#. Она работала на IIS и использовала состояние сеанса для всего. Что это означало на практике? Если вы переходили на страницу по пути A или пути B, вы видели на ней совсем разные вещи.
Но описать эту кодовую базу как просто половина VB, половина C# - это не сказать ничего. Наверное, все существующие тогда JS-фреймворки были добавлены в этот репозиторий. Как правило, с некоторыми пользовательскими изменениями, которые автор посчитал необходимыми. В частности, knockout, backbone и marionette. Ну и конечно, было немного jquery и плагинов jquery.
Но эта кодовая база не стояла особняком. Рядом с ней было около дюжины SOAP-сервисов и несколько нативных приложений Windows. Самым примечательным был менеджер по доставке. В документации сказано, что всё приложение было создано за выходные разработчиком-одиночкой. Назовем его Гилфойлом. Гилфойл был, по общему мнению, невероятно быстрым программистом. Я никогда не встречался с ним, но я чувствовал, что знаю его не только по его коду в репозиториях, но и по всему коду, оставшемуся на его жестких дисках.
Жесткие диски Гилфойла
Манч (да, это было его настоящее имя) держал жёсткий диск Гилфойла в RAID-массиве на своём столе много лет после того, как Гилфойл ушёл из компании. Почему? Потому что Гилфойл был известен тем, что не коммитил код. Более того, он создавал случайное одноразовое приложение Windows для одного пользователя. Поэтому не было редкостью, когда пользователь приходил к нам с отчётом об ошибке для приложения, которое существовало только на жёстком диске Гилфойла, а мы понятия не имели о его существовании.
Окончание следует…
Источник: https://jimmyhmiller.github.io/ugliest-beautiful-codebase
Автор оригинала: Jimmy Miller
«У Нас Кончились Столбцы» — Лучшая Худшая Кодовая База. Продолжение
Начало
Сотрудники
Каждое утро в 7:15 таблица сотрудников удалялась. Все данные полностью исчезали. Затем в таблицу загружался csv-файл из adp. В это время мы не могли войти в систему. Иногда этот процесс давал сбой. Но это был не конец процесса. Данные нужно было реплицировать в штаб-квартиру. Поэтому электронное письмо отправлялось человеку, который каждый день нажимал кнопку, чтобы скопировать данные.
Заменяющая база данных
Вы могли бы подумать: неужели никому не пришло в голову почистить эту БД? Сделать её более удобной для работы? Да. Была копия БД. Данные в этой копии отставали примерно на 10 минут. Синхронизация работала только в одну сторону, но эта база данных была нормализована. Насколько нормализована? Чтобы перейти от продавца к его номеру телефона, требовалось соединить 7 таблиц.
Цифры продаж
У каждого продавца была квота, которую ему нужно было выполнить в месяц, называемая «победой». Таблицы, в которых хранились эти данные (не финансовые данные, а способ учёта продаж), были невероятно сложными. Каждый день назначенное задание выясняло, какие строки были добавлены и обновлены, и синхронизировало их с какой-то системой в штаб-квартире. Это не было проблемой, пока один продавец не понял, что можно попросить вручную изменить эти записи.
Он уже получил свою «победу» и заключил ещё одну большую сделку в том же месяце. Он хотел, чтобы её перенесли на следующий месяц. Это было поручено стажёру. Слухи разошлись, и в течение следующих 3 лет запросы росли в геометрической прогрессии. В какой-то момент у нас было 3 стажёра, чья постоянная работа заключалась в написании этих SQL-запросов. Написание приложения для этого посчитали слишком сложным. Однако перед уходом я помог этим стажерам создать его. Хотя понятия не имею, использовали ли они его.
Кодовая база
Но что такое БД без кодовой базы. Какая же это была великолепная кодовая база. Когда я пришёл, всё было в Team Foundation Server. Если вы не в курсе, это централизованная система управления исходным кодом от Microsoft. Основная кодовая база, с которой я работал, была наполовину VB, наполовину C#. Она работала на IIS и использовала состояние сеанса для всего. Что это означало на практике? Если вы переходили на страницу по пути A или пути B, вы видели на ней совсем разные вещи.
Но описать эту кодовую базу как просто половина VB, половина C# - это не сказать ничего. Наверное, все существующие тогда JS-фреймворки были добавлены в этот репозиторий. Как правило, с некоторыми пользовательскими изменениями, которые автор посчитал необходимыми. В частности, knockout, backbone и marionette. Ну и конечно, было немного jquery и плагинов jquery.
Но эта кодовая база не стояла особняком. Рядом с ней было около дюжины SOAP-сервисов и несколько нативных приложений Windows. Самым примечательным был менеджер по доставке. В документации сказано, что всё приложение было создано за выходные разработчиком-одиночкой. Назовем его Гилфойлом. Гилфойл был, по общему мнению, невероятно быстрым программистом. Я никогда не встречался с ним, но я чувствовал, что знаю его не только по его коду в репозиториях, но и по всему коду, оставшемуся на его жестких дисках.
Жесткие диски Гилфойла
Манч (да, это было его настоящее имя) держал жёсткий диск Гилфойла в RAID-массиве на своём столе много лет после того, как Гилфойл ушёл из компании. Почему? Потому что Гилфойл был известен тем, что не коммитил код. Более того, он создавал случайное одноразовое приложение Windows для одного пользователя. Поэтому не было редкостью, когда пользователь приходил к нам с отчётом об ошибке для приложения, которое существовало только на жёстком диске Гилфойла, а мы понятия не имели о его существовании.
Окончание следует…
Источник: https://jimmyhmiller.github.io/ugliest-beautiful-codebase
Автор оригинала: Jimmy Miller
👍23
День 2031. #Оффтоп
«У Нас Кончились Столбцы» — Лучшая Худшая Кодовая База. Окончание
Начало
Продолжение
Баг в доставке
Одна особенно неприятная ошибка всплывала раз в несколько месяцев. После того, как мы отправляли товары, в очереди доставки застревали заказы одновременно и как отправленные, и как не отправленные. Я пробовал ряд обходных путей, чтобы исправить это.
Попутно я узнал, как мыслит Гилфойл. Приложение доставки извлекало всю БД, фильтровало по дате и сохраняло все заказы после даты запуска приложения. Оно полагалось на SOAP-сервис. Нет, сервис был чистой функцией. Все побочные эффекты были на клиенте. Там я обнаружил иерархию в 120 классов с различными методами и 10-уровневое наследование. Единственная проблема… ВСЕ МЕТОДЫ БЫЛИ ПУСТЫМИ.
В конце концов я узнал, что это было необходимо для создания структуры, к которой он мог бы затем применить рефлексию. Рефлексия позволила бы ему создать строку с разделителями (структура которой управлялась базой данных), которую он мог бы отправлять через сокет. Оказывается, всё это в итоге отправлялось в Kewill (сервис, который общался с перевозчиками). Почему происходила ошибка? Kewill повторно использовал 9-значные числа каждый месяц, а кто-то отключил задание cron, которое удаляло старые заказы.
Прекрасный беспорядок
Ещё много можно рассказать об этой кодовой базе. Например, о команде супер-старших разработчиков, которые переписывали всё это, не делая коммитов кода в течение 5 лет. Или о консультантах Red Hat, создавших одну БД, чтобы управлять всеми. Или об улучшении Джастином страницы поиска продавцов. Она была точкой входа во всё приложение. Каждый представитель службы поддержки клиентов звонил продавцу и вводил либо его ID, либо название, чтобы найти его информацию. Это приводило на огромную страницу с любой информацией, которая вам могла бы понадобиться, и всеми ссылками, которые вы могли бы захотеть посетить. Но она была чертовски медленной.
Однажды Джастину надоело слушать о том, как медленно работает страница, и он исправил её. Каждое поле стало конечной точкой. При загрузке страницы каждый компонент запрашивался отдельно. Когда загружался один, запрашивался следующий. Но время загрузки страницы сократилось с нескольких минут до доли секунды.
Два способа разделения
Почему Джастин смог это сделать? Потому что у проекта не было генерального плана, формата API, документации и т.п. Система была полным и абсолютным хаосом. Никто не мог его исправить, поэтому никто и не пытался. Что мы делали?
Это монолитное приложение из-за чистой необходимости превратилось в экосистему из хороших маленьких приложений по краям. Каждый кому поручали улучшить какую-то часть приложения, неизбежно отказывался от распутывания паутины и создавал своё уютное микро-приложение.
Работать так было на удивление приятно. Исчезли проблемы дублирования кода, согласованности и расширяемости. Код как можно меньше затрагивал окружающую его область и был легко заменяемым. Он был слабо связан, просто потому что связывать его было сложнее.
Итого
За всю мою карьеру с тех пор мне не доводилось работать в такой замечательно уродливой кодовой базе. Возможно, тот код был давно заброшен старшими разработчиками, а дописывали его стажеры и джуны. Или, возможно, это было потому, что не было никакой прослойки между разработчиками и пользователями, никакого сбора требований, никаких ТЗ. Просто вы стоите за столом представителя службы поддержки клиентов и спрашиваете его, как улучшить его жизнь.
Я скучаю по этой простой прямой связи между проблемой и кодом. Возможно, это ностальгия. Но каждый раз, когда я сталкиваюсь с очередным «корпоративным шаблоном проектирования», мой разум возвращается к той прекрасной, но ужасной кодовой базе.
Источник: https://jimmyhmiller.github.io/ugliest-beautiful-codebase
Автор оригинала: Jimmy Miller
«У Нас Кончились Столбцы» — Лучшая Худшая Кодовая База. Окончание
Начало
Продолжение
Баг в доставке
Одна особенно неприятная ошибка всплывала раз в несколько месяцев. После того, как мы отправляли товары, в очереди доставки застревали заказы одновременно и как отправленные, и как не отправленные. Я пробовал ряд обходных путей, чтобы исправить это.
Попутно я узнал, как мыслит Гилфойл. Приложение доставки извлекало всю БД, фильтровало по дате и сохраняло все заказы после даты запуска приложения. Оно полагалось на SOAP-сервис. Нет, сервис был чистой функцией. Все побочные эффекты были на клиенте. Там я обнаружил иерархию в 120 классов с различными методами и 10-уровневое наследование. Единственная проблема… ВСЕ МЕТОДЫ БЫЛИ ПУСТЫМИ.
В конце концов я узнал, что это было необходимо для создания структуры, к которой он мог бы затем применить рефлексию. Рефлексия позволила бы ему создать строку с разделителями (структура которой управлялась базой данных), которую он мог бы отправлять через сокет. Оказывается, всё это в итоге отправлялось в Kewill (сервис, который общался с перевозчиками). Почему происходила ошибка? Kewill повторно использовал 9-значные числа каждый месяц, а кто-то отключил задание cron, которое удаляло старые заказы.
Прекрасный беспорядок
Ещё много можно рассказать об этой кодовой базе. Например, о команде супер-старших разработчиков, которые переписывали всё это, не делая коммитов кода в течение 5 лет. Или о консультантах Red Hat, создавших одну БД, чтобы управлять всеми. Или об улучшении Джастином страницы поиска продавцов. Она была точкой входа во всё приложение. Каждый представитель службы поддержки клиентов звонил продавцу и вводил либо его ID, либо название, чтобы найти его информацию. Это приводило на огромную страницу с любой информацией, которая вам могла бы понадобиться, и всеми ссылками, которые вы могли бы захотеть посетить. Но она была чертовски медленной.
Однажды Джастину надоело слушать о том, как медленно работает страница, и он исправил её. Каждое поле стало конечной точкой. При загрузке страницы каждый компонент запрашивался отдельно. Когда загружался один, запрашивался следующий. Но время загрузки страницы сократилось с нескольких минут до доли секунды.
Два способа разделения
Почему Джастин смог это сделать? Потому что у проекта не было генерального плана, формата API, документации и т.п. Система была полным и абсолютным хаосом. Никто не мог его исправить, поэтому никто и не пытался. Что мы делали?
Это монолитное приложение из-за чистой необходимости превратилось в экосистему из хороших маленьких приложений по краям. Каждый кому поручали улучшить какую-то часть приложения, неизбежно отказывался от распутывания паутины и создавал своё уютное микро-приложение.
Работать так было на удивление приятно. Исчезли проблемы дублирования кода, согласованности и расширяемости. Код как можно меньше затрагивал окружающую его область и был легко заменяемым. Он был слабо связан, просто потому что связывать его было сложнее.
Итого
За всю мою карьеру с тех пор мне не доводилось работать в такой замечательно уродливой кодовой базе. Возможно, тот код был давно заброшен старшими разработчиками, а дописывали его стажеры и джуны. Или, возможно, это было потому, что не было никакой прослойки между разработчиками и пользователями, никакого сбора требований, никаких ТЗ. Просто вы стоите за столом представителя службы поддержки клиентов и спрашиваете его, как улучшить его жизнь.
Я скучаю по этой простой прямой связи между проблемой и кодом. Возможно, это ностальгия. Но каждый раз, когда я сталкиваюсь с очередным «корпоративным шаблоном проектирования», мой разум возвращается к той прекрасной, но ужасной кодовой базе.
Источник: https://jimmyhmiller.github.io/ugliest-beautiful-codebase
Автор оригинала: Jimmy Miller
👍18
День 2032. #ЧтоНовенького #CSharp13
Флаги функций с поддержкой тримминга
Два новых атрибута позволяют определять флаги функций, которые можно использовать для включения/отключения областей функциональности, а также для автоматического включения/отключения функций при тримминге или AOT-компиляции.
FeatureSwitchDefinitionAttribute
Атрибут FeatureSwitchDefinition может быть использован, чтобы флаг функции после сборки определялся как константа.
Здесь мы извлекаем значение IsSupported из конфигурации сборки. В данном случае оно определено в файле проекта (.csproj):
Мы добавляем RuntimeHostConfigurationOption с именем нужной функции (соответствующим параметру атрибута) и булевым значением, включена она или нет.
При сборке с включённым триммингом недосягаемый код, находящийся под флагом, удаляется. Когда приложение собирается с такой настройкой в файле проекта, Feature.IsSupported расценивается как константа false и Feature.Implementation удаляется из сборки.
Предложение описывает использование атрибута FeatureSwitchDefinition в библиотеках и для тримминга. Возможно ли его использование как флага функции в бизнес-логике пока не понятно, но, думаю, можно попробовать.
FeatureGuardAttribute
Атрибут FeatureGuard можно использовать для свойства флага функции в качестве защитной конструкции для кода, аннотированного атрибутами RequiresUnreferencedCode, RequiresAssemblyFiles или RequiresDynamicCode:
Здесь RuntimeFeature определяет API, которые доступны в среде выполнения. В данном случае свойство IsDynamicCodeSupported показывает, поддерживается ли в среде выполнения динамический код.
Поскольку свойство IsSupported возвращает false, когда динамический код не поддерживается, его можно использовать в качестве защиты для методов, которым требуется динамический код во время выполнения.
Атрибут FeatureGuard сообщает об этом анализатору тримминга и инструментам тримминга. Поэтому, если мы в файле проекта укажем
то Feature.DynamicImplementation будет удалён при AOT-публикации.
Источник: https://github.com/dotnet/core/blob/main/release-notes/9.0/preview/preview4/runtime.md
Флаги функций с поддержкой тримминга
Два новых атрибута позволяют определять флаги функций, которые можно использовать для включения/отключения областей функциональности, а также для автоматического включения/отключения функций при тримминге или AOT-компиляции.
FeatureSwitchDefinitionAttribute
Атрибут FeatureSwitchDefinition может быть использован, чтобы флаг функции после сборки определялся как константа.
public class Feature
{
[FeatureSwitchDefinition("Feature.IsSupported")]
internal static bool IsSupported =>
AppContext.TryGetSwitch("Feature.IsSupported", out bool enabled)
? enabled
: true;
internal static void Implementation() => …;
}
Здесь мы извлекаем значение IsSupported из конфигурации сборки. В данном случае оно определено в файле проекта (.csproj):
<ItemGroup>
<RuntimeHostConfigurationOption Include="Feature.IsSupported"
Value="false" Trim="true" />
</ItemGroup>
Мы добавляем RuntimeHostConfigurationOption с именем нужной функции (соответствующим параметру атрибута) и булевым значением, включена она или нет.
При сборке с включённым триммингом недосягаемый код, находящийся под флагом, удаляется. Когда приложение собирается с такой настройкой в файле проекта, Feature.IsSupported расценивается как константа false и Feature.Implementation удаляется из сборки.
Предложение описывает использование атрибута FeatureSwitchDefinition в библиотеках и для тримминга. Возможно ли его использование как флага функции в бизнес-логике пока не понятно, но, думаю, можно попробовать.
FeatureGuardAttribute
Атрибут FeatureGuard можно использовать для свойства флага функции в качестве защитной конструкции для кода, аннотированного атрибутами RequiresUnreferencedCode, RequiresAssemblyFiles или RequiresDynamicCode:
public class Feature
{
[FeatureGuard(typeof(RequiresDynamicCodeAttribute))]
internal static bool IsSupported =>
RuntimeFeature.IsDynamicCodeSupported;
[RequiresDynamicCode("Feature requires dynamic code support.")]
internal static void DynamicImplementation()
=> …; // Использует dynamic
}
Здесь RuntimeFeature определяет API, которые доступны в среде выполнения. В данном случае свойство IsDynamicCodeSupported показывает, поддерживается ли в среде выполнения динамический код.
Поскольку свойство IsSupported возвращает false, когда динамический код не поддерживается, его можно использовать в качестве защиты для методов, которым требуется динамический код во время выполнения.
if (Feature.IsSupported)
Feature.DynamicImplementation();
Атрибут FeatureGuard сообщает об этом анализатору тримминга и инструментам тримминга. Поэтому, если мы в файле проекта укажем
<PublishAot>true</PublishAot>
то Feature.DynamicImplementation будет удалён при AOT-публикации.
Источник: https://github.com/dotnet/core/blob/main/release-notes/9.0/preview/preview4/runtime.md
6👍7
День 2033. #BlazorBasics
Основы Blazor. (Ре-)Рендеринг Компонентов. Начало
Рендеринг компонентов — основная механика приложений Blazor, превращающая компоненты C# в HTML и CSS. Методы жизненного цикла позволяют нам выполнять пользовательский код синхронно или асинхронно.
Приложения Blazor состоят из дерева компонентов, которые создают интерактивный UI. Рендеринг начинается на вершине дерева и переходит к дочерним элементам каждого компонента. Так родительский компонент решает, какие дочерние компоненты создавать.
Методы жизненного цикла компонентов Blazor
Процесс рендеринга не может быть прерван; в противном случае UI бы «завис». Поэтому компоненты Blazor используют методы жизненного цикла, чтобы позволить выполнять пользовательский код в течение их жизненного цикла.
Компонент Blazor впервые отрисовывается после создания его экземпляра при включении в качестве дочернего компонента см. диаграмму ниже.
Метод SetParametersAsync
Это первый метод обратного вызова в жизненном цикле компонента. Он получает параметры, предоставленные родительским компонентом. Переопределяя SetParametersAsync, мы можем использовать пользовательский код для изменения значений, предоставляемых родительским компонентом. По умолчанию параметры будут установлены в свойства, декорированные атрибутом Parameter. Если мы хотим использовать параметры компонента по-другому, мы можем использовать пользовательский код и переопределить метод SetParametersAsync, чтобы определить желаемое поведение.
Методы OnInitialized и OnInitializedAsync
После того, как компонент Blazor завершает выполнение метода SetParametersAsync и, следовательно, все параметры из родительского компонента установлены, вызываются методы жизненного цикла OnInitialized и OnInitializedAsync. Это идеальное место для загрузки данных из БД и назначения их свойствам, используемым в шаблоне компонента.
Как показано в этом примере, мы также можем использовать значения, параметров компонента в синхронном методе жизненного цикла OnInitialized.
Окончание следует…
Источник: https://www.telerik.com/blogs/blazor-basics-blazor-component-re-rendering
Основы Blazor. (Ре-)Рендеринг Компонентов. Начало
Рендеринг компонентов — основная механика приложений Blazor, превращающая компоненты C# в HTML и CSS. Методы жизненного цикла позволяют нам выполнять пользовательский код синхронно или асинхронно.
Приложения Blazor состоят из дерева компонентов, которые создают интерактивный UI. Рендеринг начинается на вершине дерева и переходит к дочерним элементам каждого компонента. Так родительский компонент решает, какие дочерние компоненты создавать.
Методы жизненного цикла компонентов Blazor
Процесс рендеринга не может быть прерван; в противном случае UI бы «завис». Поэтому компоненты Blazor используют методы жизненного цикла, чтобы позволить выполнять пользовательский код в течение их жизненного цикла.
Компонент Blazor впервые отрисовывается после создания его экземпляра при включении в качестве дочернего компонента см. диаграмму ниже.
Метод SetParametersAsync
Это первый метод обратного вызова в жизненном цикле компонента. Он получает параметры, предоставленные родительским компонентом. Переопределяя SetParametersAsync, мы можем использовать пользовательский код для изменения значений, предоставляемых родительским компонентом. По умолчанию параметры будут установлены в свойства, декорированные атрибутом Parameter. Если мы хотим использовать параметры компонента по-другому, мы можем использовать пользовательский код и переопределить метод SetParametersAsync, чтобы определить желаемое поведение.
@code {
// параметры компонента
public override async Task
SetParametersAsync(ParameterView pv)
{
// пользовательский код
await base.SetParametersAsync(pv);
}
}Методы OnInitialized и OnInitializedAsync
После того, как компонент Blazor завершает выполнение метода SetParametersAsync и, следовательно, все параметры из родительского компонента установлены, вызываются методы жизненного цикла OnInitialized и OnInitializedAsync. Это идеальное место для загрузки данных из БД и назначения их свойствам, используемым в шаблоне компонента.
@code {
private string? message;
[Parameter]
public string Name { get; set; }
protected override void OnInitialized()
{
message = $"Привет, {Name}!";
}
}Как показано в этом примере, мы также можем использовать значения, параметров компонента в синхронном методе жизненного цикла OnInitialized.
Окончание следует…
Источник: https://www.telerik.com/blogs/blazor-basics-blazor-component-re-rendering
1👍8
Родительский компонент запускает создание дочернего компонента. Метод жизненного цикла SetParametersAsync и методы OnInitialized(Async) запускаются только для первой отрисовки. Метод OnParametersSet(Async) запускается, когда родительский компонент предоставляет другие значения параметров своему дочернему компоненту. Метод StateHasChanged может использоваться разработчиком для запуска повторной отрисовки компонента вручную.
👍8
День 2034. #BlazorBasics
Основы Blazor. (Ре-)Рендеринг Компонентов. Окончание
Начало
Методы OnParametersSet и OnParametersSetAsync
SetParametersAsync вызывается только при первом рендеринге компонента Blazor. А методы OnParametersSet и OnParametersSetAsync запускаются всякий раз, когда родительский компонент повторно отрисовывается, предоставляя другие значения для параметров дочернего компонента.
Переопределение этих методов предоставляет нам доступ к обновлению значений в компоненте на основе нового набора параметров.
После завершения методов OnParametersSet или OnParametersSetAsync запускается автоматическая повторная отрисовка компонента.
Метод StateHasChanged
Мы можем вызвать метод StateHasChanged класса ComponentBase, чтобы сообщить компоненту Blazor об изменении состояния компонента. Он запустит повторную отрисовку компонента, которая вызовет все применимые методы жизненного цикла в процессе отрисовки.
Не следует вызывать метод StateHasChanged в следующих сценариях:
- Обработка событий. Неважно, являются ли события синхронными или асинхронными. Класс ComponentBase запускает отрисовку для большинства обработчиков событий.
- Реализация методов жизненного цикла, таких как OnParametersSetAsync или OnInitialized (синхронных или асинхронных). Класс ComponentBase запускает отрисовку для большинства событий жизненного цикла.
Как правило, обычно вам не нужно вызывать метод StateHasChanged вручную. Однако, если вы ожидаете, что UI покажет обновленное значение, а этого не происходит, велики шансы, что вызов StateHasChanged в правильном месте исправит вашу проблему.
Подробнее о крайних случаях, когда вызов StateHasChanged требуется для получения желаемого поведения, читайте в документации по рендерингу компонентов.
Асинхронные и синхронные методы жизненного цикла
Для синхронного кода родительские компоненты всегда визуализируются перед дочерними компонентами. Асинхронный код более сложныq. При использовании асинхронных методов жизненного цикла порядок завершения родительского и дочернего компонента не является детерминированным, поскольку он зависит от кода инициализации. Однако Blazor обеспечивает доступность и корректное обновление параметров компонента при визуализации дочернего компонента.
Используйте синхронные методы жизненного цикла, когда не нужно ожидать асинхронных операций. Когда же нужно вызвать БД или использовать другие асинхронные операции, используйте асинхронные методы.
Итого
Компоненты Blazor используют методы жизненного цикла для предотвращения блокировки пользовательского интерфейса и позволяют разработчикам выполнять пользовательский код:
- SetParametersAsync позволяет выполнять пользовательский код, обрабатывающий параметры, предоставленные родительским компонентом, только для первого рендеринга компонента.
- OnInitialized и OnInitializedAsync часто используются для загрузки данных из БД или для инициализации свойств, отображаемых как часть шаблона компонента.
- OnParametersSet и OnParametersSetAsync вызываются, когда родительский компонент повторно отрисовывается, предоставляя другой параметр дочернему компоненту.
- Если UI не отображает ожидаемое значение на экране, вы можете вызвать StateHasChanged в соответствующем месте кода.
Источник: https://www.telerik.com/blogs/blazor-basics-blazor-component-re-rendering
Основы Blazor. (Ре-)Рендеринг Компонентов. Окончание
Начало
Методы OnParametersSet и OnParametersSetAsync
SetParametersAsync вызывается только при первом рендеринге компонента Blazor. А методы OnParametersSet и OnParametersSetAsync запускаются всякий раз, когда родительский компонент повторно отрисовывается, предоставляя другие значения для параметров дочернего компонента.
Переопределение этих методов предоставляет нам доступ к обновлению значений в компоненте на основе нового набора параметров.
@code {
private string? message;
[Parameter]
public string Name { get; set; }
protected override void OnParametersSet()
{
message = $"Привет, {Name}!";
}
}После завершения методов OnParametersSet или OnParametersSetAsync запускается автоматическая повторная отрисовка компонента.
Метод StateHasChanged
Мы можем вызвать метод StateHasChanged класса ComponentBase, чтобы сообщить компоненту Blazor об изменении состояния компонента. Он запустит повторную отрисовку компонента, которая вызовет все применимые методы жизненного цикла в процессе отрисовки.
Не следует вызывать метод StateHasChanged в следующих сценариях:
- Обработка событий. Неважно, являются ли события синхронными или асинхронными. Класс ComponentBase запускает отрисовку для большинства обработчиков событий.
- Реализация методов жизненного цикла, таких как OnParametersSetAsync или OnInitialized (синхронных или асинхронных). Класс ComponentBase запускает отрисовку для большинства событий жизненного цикла.
Как правило, обычно вам не нужно вызывать метод StateHasChanged вручную. Однако, если вы ожидаете, что UI покажет обновленное значение, а этого не происходит, велики шансы, что вызов StateHasChanged в правильном месте исправит вашу проблему.
Подробнее о крайних случаях, когда вызов StateHasChanged требуется для получения желаемого поведения, читайте в документации по рендерингу компонентов.
Асинхронные и синхронные методы жизненного цикла
Для синхронного кода родительские компоненты всегда визуализируются перед дочерними компонентами. Асинхронный код более сложныq. При использовании асинхронных методов жизненного цикла порядок завершения родительского и дочернего компонента не является детерминированным, поскольку он зависит от кода инициализации. Однако Blazor обеспечивает доступность и корректное обновление параметров компонента при визуализации дочернего компонента.
Используйте синхронные методы жизненного цикла, когда не нужно ожидать асинхронных операций. Когда же нужно вызвать БД или использовать другие асинхронные операции, используйте асинхронные методы.
Итого
Компоненты Blazor используют методы жизненного цикла для предотвращения блокировки пользовательского интерфейса и позволяют разработчикам выполнять пользовательский код:
- SetParametersAsync позволяет выполнять пользовательский код, обрабатывающий параметры, предоставленные родительским компонентом, только для первого рендеринга компонента.
- OnInitialized и OnInitializedAsync часто используются для загрузки данных из БД или для инициализации свойств, отображаемых как часть шаблона компонента.
- OnParametersSet и OnParametersSetAsync вызываются, когда родительский компонент повторно отрисовывается, предоставляя другой параметр дочернему компоненту.
- Если UI не отображает ожидаемое значение на экране, вы можете вызвать StateHasChanged в соответствующем месте кода.
Источник: https://www.telerik.com/blogs/blazor-basics-blazor-component-re-rendering
👍8
День 2035. #ЧтоНовенького
Прекращается Поддержка BinaryFormatter
Начиная с .NET 9, BinaryFormatter больше не будет включён в среду выполнения. Его API все ещё присутствует, но реализация всегда будет выдавать исключение PlatformNotSupportedException, независимо от типа проекта. Установка существующего сейчас флага обратной совместимости больше недостаточна для использования BinaryFormatter.
Есть два варианта решения:
1. Настоятельно рекомендуется отказаться от BinaryFormatter из-за связанных ним рисков безопасности и выбрать другой сериализатор.
2. Продолжить использовать BinaryFormatter. Для этого нужно добавить неподдерживаемый пакет совместимости System.Runtime.Serialization.Formatters, который заменяет реализацию, вызывающую исключение в среде выполнения, но подвержен описанным ниже уязвимостям.
Проблема с BinaryFormatter
Дело в том, что любой десериализатор, позволяющий иметь во входных данных информацию о создаваемых объектах, является проблемой безопасности. BinaryFormatter, включённый в первый выпуск .NET Framework в 2002 году, является таким десериализатором. Из-за известных рисков эта функциональность была исключена из .NET Core 1.0. Но в отсутствие чёткого пути миграции на что-то более безопасное, спрос среди клиентов привёл к тому, что BinaryFormatter был обратно включен в .NET Core 2.0. С тех пор команда .NET шла по пути к его удалению, постепенно отключая его по умолчанию в разных типах проектов, но позволяя потребителям включать его с помощью флагов, если было необходимо для обратной совместимости.
Миграция
Выбор другого сериализатора возможен, только если вы контролируете как производителя, так и потребителя сериализованных данных. Если это ваш случай, то в зависимости от ваших потребностей рекомендуются следующие сериализаторы:
- System.Text.Json (JSON)
- DataContractSerializer (XML)
- MessagePack (binary)
- protobuf-net (binary)
Если вы не контролируете производителя, либо десериализуете данные, которые были сериализованы ранее и сохранены (и нет возможности их преобразовать) вы можете использовать новый API для чтения данных BinaryFormatter без выполнения универсальной и уязвимой десериализации с помощью NuGet-пакета System.Formats.Nrbf.
Источник: https://learn.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-migration-guide/
Прекращается Поддержка BinaryFormatter
Начиная с .NET 9, BinaryFormatter больше не будет включён в среду выполнения. Его API все ещё присутствует, но реализация всегда будет выдавать исключение PlatformNotSupportedException, независимо от типа проекта. Установка существующего сейчас флага обратной совместимости больше недостаточна для использования BinaryFormatter.
Есть два варианта решения:
1. Настоятельно рекомендуется отказаться от BinaryFormatter из-за связанных ним рисков безопасности и выбрать другой сериализатор.
2. Продолжить использовать BinaryFormatter. Для этого нужно добавить неподдерживаемый пакет совместимости System.Runtime.Serialization.Formatters, который заменяет реализацию, вызывающую исключение в среде выполнения, но подвержен описанным ниже уязвимостям.
Проблема с BinaryFormatter
Дело в том, что любой десериализатор, позволяющий иметь во входных данных информацию о создаваемых объектах, является проблемой безопасности. BinaryFormatter, включённый в первый выпуск .NET Framework в 2002 году, является таким десериализатором. Из-за известных рисков эта функциональность была исключена из .NET Core 1.0. Но в отсутствие чёткого пути миграции на что-то более безопасное, спрос среди клиентов привёл к тому, что BinaryFormatter был обратно включен в .NET Core 2.0. С тех пор команда .NET шла по пути к его удалению, постепенно отключая его по умолчанию в разных типах проектов, но позволяя потребителям включать его с помощью флагов, если было необходимо для обратной совместимости.
Миграция
Выбор другого сериализатора возможен, только если вы контролируете как производителя, так и потребителя сериализованных данных. Если это ваш случай, то в зависимости от ваших потребностей рекомендуются следующие сериализаторы:
- System.Text.Json (JSON)
- DataContractSerializer (XML)
- MessagePack (binary)
- protobuf-net (binary)
Если вы не контролируете производителя, либо десериализуете данные, которые были сериализованы ранее и сохранены (и нет возможности их преобразовать) вы можете использовать новый API для чтения данных BinaryFormatter без выполнения универсальной и уязвимой десериализации с помощью NuGet-пакета System.Formats.Nrbf.
Источник: https://learn.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-migration-guide/
👍9
День 2036. #УрокиРазработки
Уроки 50 Лет Разработки ПО
Урок 20. Невозможно оптимизировать все желаемые атрибуты качества. Начало
«Приложение, которое я хотел бы получить, не должно иметь ошибок, не должно использовать много памяти или замедлять компьютер, должно быть полностью безопасным, мгновенно реагировать на каждую мою команду и быть абсолютно надёжным, работать на любом устройстве, мгновенно загружаться и быть бесплатным.»
Похоже на сказочное приложение. Желания клиента неразумны. Невозможно объединить в одном приложении все лучшие достижения и передовые возможности. Различные качественные характеристики неизбежно вступают в конфликт друг с другом: улучшение одной часто приводит к ухудшению другой. Поэтому важной частью анализа требований является определение наиболее важных характеристик, чтобы разработчики могли учесть это.
Нефункциональные требования не реализуются напрямую. Они служат лишь источником производной функциональности, архитектурных решений или подходов к проектированию и реализации. Некоторые нефункциональные требования ограничивают выбор вариантов, доступных дизайнеру или разработчику. Например, из-за требования функциональной совместимости, возможно, придётся использовать только стандартные API.
Ниже перечислены атрибуты качества, которые каждая команда разработчиков ПО должна учитывать, изучая значение понятия качества для их продукта:
1. Доступность
Смогу ли я использовать систему, когда и где мне нужно?
2. Соответствие стандартам
Соответствует ли система всем применимым стандартам в отношении функциональности, безопасности, сертификации и т.п.?
3. Эффективность
Экономно ли система использует ресурсы компьютера?
4. Возможность установки
Смогу ли я легко установить, удалить и переустановить систему и её обновления?
5. Целостность
Имеет ли система защиту от неточных данных, от их повреждения и потери?
6. Совместимость
Достаточно ли хорошо система взаимодействует с окружением для обмена данными и услугами?
7. Сопровождаемость
Смогут ли разработчики легко менять, исправлять и улучшать систему?
8. Производительность
Достаточно ли быстро система реагирует на действия пользователя и внешние события?
9. Переносимость
Можно ли легко перенести систему на другие платформы?
10. Надёжность
Отсутствуют ли сбои в работе системы?
11. Повторное использование
Смогут ли разработчики повторно использовать части системы в других продуктах?
12. Устойчивость
Реагирует ли система на ошибочные входные данные и непредвиденные условия работы?
13. Безопасность
Защищает ли система пользователей от вреда и имущество от повреждения?
14. Масштабируемость
Может ли система легко расширяться для обслуживания большего количества пользователей, данных или транзакций?
15. Защищённость
Защищена ли система от атак вредоносных программ, злоумышленников, неавторизованных пользователей и кражи данных?
16. Удобство использования
Смогут ли пользователи быстро научиться работать с системой и эффективно выполнять свои задачи?
17. Проверяемость
Смогут ли тестировщики определить, насколько правильно реализовано ПО?
По аналогии с функциональностью разработчики должны сбалансировать ценность некоего качества и стоимость его достижения. Например, всем хотелось бы, чтобы ПО всегда было доступно для использования, но достижение этого может стоить очень дорого. Если есть требование нулевого времени простоя, вариант – добавить резервную систему. При обновлении сначала обновляется резервная система, тестируется и переключается в рабочий режим, а бывшая основная система обновляется после. Иметь две независимые системы — дорого, но это дешевле, чем останавливать производство, если система выходит из строя.
Окончание следует…
Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
Уроки 50 Лет Разработки ПО
Урок 20. Невозможно оптимизировать все желаемые атрибуты качества. Начало
«Приложение, которое я хотел бы получить, не должно иметь ошибок, не должно использовать много памяти или замедлять компьютер, должно быть полностью безопасным, мгновенно реагировать на каждую мою команду и быть абсолютно надёжным, работать на любом устройстве, мгновенно загружаться и быть бесплатным.»
Похоже на сказочное приложение. Желания клиента неразумны. Невозможно объединить в одном приложении все лучшие достижения и передовые возможности. Различные качественные характеристики неизбежно вступают в конфликт друг с другом: улучшение одной часто приводит к ухудшению другой. Поэтому важной частью анализа требований является определение наиболее важных характеристик, чтобы разработчики могли учесть это.
Нефункциональные требования не реализуются напрямую. Они служат лишь источником производной функциональности, архитектурных решений или подходов к проектированию и реализации. Некоторые нефункциональные требования ограничивают выбор вариантов, доступных дизайнеру или разработчику. Например, из-за требования функциональной совместимости, возможно, придётся использовать только стандартные API.
Ниже перечислены атрибуты качества, которые каждая команда разработчиков ПО должна учитывать, изучая значение понятия качества для их продукта:
1. Доступность
Смогу ли я использовать систему, когда и где мне нужно?
2. Соответствие стандартам
Соответствует ли система всем применимым стандартам в отношении функциональности, безопасности, сертификации и т.п.?
3. Эффективность
Экономно ли система использует ресурсы компьютера?
4. Возможность установки
Смогу ли я легко установить, удалить и переустановить систему и её обновления?
5. Целостность
Имеет ли система защиту от неточных данных, от их повреждения и потери?
6. Совместимость
Достаточно ли хорошо система взаимодействует с окружением для обмена данными и услугами?
7. Сопровождаемость
Смогут ли разработчики легко менять, исправлять и улучшать систему?
8. Производительность
Достаточно ли быстро система реагирует на действия пользователя и внешние события?
9. Переносимость
Можно ли легко перенести систему на другие платформы?
10. Надёжность
Отсутствуют ли сбои в работе системы?
11. Повторное использование
Смогут ли разработчики повторно использовать части системы в других продуктах?
12. Устойчивость
Реагирует ли система на ошибочные входные данные и непредвиденные условия работы?
13. Безопасность
Защищает ли система пользователей от вреда и имущество от повреждения?
14. Масштабируемость
Может ли система легко расширяться для обслуживания большего количества пользователей, данных или транзакций?
15. Защищённость
Защищена ли система от атак вредоносных программ, злоумышленников, неавторизованных пользователей и кражи данных?
16. Удобство использования
Смогут ли пользователи быстро научиться работать с системой и эффективно выполнять свои задачи?
17. Проверяемость
Смогут ли тестировщики определить, насколько правильно реализовано ПО?
По аналогии с функциональностью разработчики должны сбалансировать ценность некоего качества и стоимость его достижения. Например, всем хотелось бы, чтобы ПО всегда было доступно для использования, но достижение этого может стоить очень дорого. Если есть требование нулевого времени простоя, вариант – добавить резервную систему. При обновлении сначала обновляется резервная система, тестируется и переключается в рабочий режим, а бывшая основная система обновляется после. Иметь две независимые системы — дорого, но это дешевле, чем останавливать производство, если система выходит из строя.
Окончание следует…
Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
👍10
День 2037. #УрокиРазработки
Уроки 50 Лет Разработки ПО
Урок 20. Невозможно оптимизировать все желаемые атрибуты качества. Окончание
Начало
Определение атрибутов качества
Разработчики должны знать, какие атрибуты качества наиболее важны. Недостаточно просто сказать: «Система должна быть надёжной» или «Система должна быть удобной для пользователя». На этапе выявления требований бизнес-аналитик должен выяснить, что именно заинтересованные стороны имеют в виду под надёжностью или удобством. По каким характеристикам можно судить об этом? Какие примеры ненадёжности или неудобства можно привести?
Чем точнее бизнес-аналитик сформулирует ожидания заинтересованных сторон в отношении качества, тем легче разработчикам будет сделать правильный выбор и оценить достижение целевых показателей. Когда возможно, формулируйте цели в области качества измеримыми и проверяемыми способами. Для тщательной проработки требований нужно время, но оно будет потрачено с пользой по сравнению с переделкой продукта после того, как он не оправдает ожиданий клиентов.
Проектирование для качества
Разработчики могут оптимизировать свой подход к решению для практически любого параметра качества в зависимости от степени его важности. На этапе исследования требований нужно определить наиболее важные атрибуты, чтобы направлять усилия разработчиков туда, где они наиболее важны для бизнеса, т.е. необходимо расставлять приоритеты для нефункциональных требований по аналогии с функциональными.
Улучшение одних атрибутов качества может ухудшить другие. Вот несколько примеров конфликтов:
- Многофакторная аутентификация более надёжна, но снижает удобство использования из-за дополнительных шагов и, возможно, задействованных устройств.
- Продукт, предназначенный для повторного использования, может оказаться менее эффективным, чем если бы код функциональности был оптимизирован для одного приложения.
- Оптимизация производительности может ухудшить переносимость системы.
- Внедрение решений, упрощающих обучение новых пользователей, может сделать систему неудобной для экспертов.
Но некоторые пары атрибутов качества создают эффект синергии. Проектирование системы с учетом высокой надёжности способствует улучшению:
- доступности (если система не даёт сбоев, то остаётся доступной для использования);
- целостности (снижается риск потери или повреждения данных из-за сбоя);
- устойчивости (меньше вероятность отказа продукта);
- безопасности (если механизмы безопасности продукта работают надёжно, то никто не пострадает).
Взаимозависимость атрибутов качества наглядно показывает, почему проектная группа должна заранее выяснить, что означает понятие качества для основных заинтересованных сторон, и направить работу на достижение этих целей. Заинтересованные стороны, не обсуждающие с бизнес-аналитиками эти вопросы, оказываются зависимыми от догадок и предположений разработчиков, и очень повезёт, если разработчики встроят функции, ценные для клиентов.
Архитектура и атрибуты качества
Поскольку необходимость компромиссов — частое явление, архитекторы должны знать, какие атрибуты наиболее важны, иначе не смогут принять решения, ведущие к желаемым результатам.
Возвращение на поздних стадиях разработки или после выпуска и переделка архитектуры системы в рамках исправления недостатков качества — дорогое удовольствие. Поэтапное построение систем при отсутствии предварительного знания наиболее важных целей в области качества может привести к проблемам, которые трудно исправить. Как это часто бывает с программными проектами, потратив чуть больше времени на то, чтобы лучше понять цели в области качества, можно прийти к менее дорогим и более надёжным решениям.
Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
Уроки 50 Лет Разработки ПО
Урок 20. Невозможно оптимизировать все желаемые атрибуты качества. Окончание
Начало
Определение атрибутов качества
Разработчики должны знать, какие атрибуты качества наиболее важны. Недостаточно просто сказать: «Система должна быть надёжной» или «Система должна быть удобной для пользователя». На этапе выявления требований бизнес-аналитик должен выяснить, что именно заинтересованные стороны имеют в виду под надёжностью или удобством. По каким характеристикам можно судить об этом? Какие примеры ненадёжности или неудобства можно привести?
Чем точнее бизнес-аналитик сформулирует ожидания заинтересованных сторон в отношении качества, тем легче разработчикам будет сделать правильный выбор и оценить достижение целевых показателей. Когда возможно, формулируйте цели в области качества измеримыми и проверяемыми способами. Для тщательной проработки требований нужно время, но оно будет потрачено с пользой по сравнению с переделкой продукта после того, как он не оправдает ожиданий клиентов.
Проектирование для качества
Разработчики могут оптимизировать свой подход к решению для практически любого параметра качества в зависимости от степени его важности. На этапе исследования требований нужно определить наиболее важные атрибуты, чтобы направлять усилия разработчиков туда, где они наиболее важны для бизнеса, т.е. необходимо расставлять приоритеты для нефункциональных требований по аналогии с функциональными.
Улучшение одних атрибутов качества может ухудшить другие. Вот несколько примеров конфликтов:
- Многофакторная аутентификация более надёжна, но снижает удобство использования из-за дополнительных шагов и, возможно, задействованных устройств.
- Продукт, предназначенный для повторного использования, может оказаться менее эффективным, чем если бы код функциональности был оптимизирован для одного приложения.
- Оптимизация производительности может ухудшить переносимость системы.
- Внедрение решений, упрощающих обучение новых пользователей, может сделать систему неудобной для экспертов.
Но некоторые пары атрибутов качества создают эффект синергии. Проектирование системы с учетом высокой надёжности способствует улучшению:
- доступности (если система не даёт сбоев, то остаётся доступной для использования);
- целостности (снижается риск потери или повреждения данных из-за сбоя);
- устойчивости (меньше вероятность отказа продукта);
- безопасности (если механизмы безопасности продукта работают надёжно, то никто не пострадает).
Взаимозависимость атрибутов качества наглядно показывает, почему проектная группа должна заранее выяснить, что означает понятие качества для основных заинтересованных сторон, и направить работу на достижение этих целей. Заинтересованные стороны, не обсуждающие с бизнес-аналитиками эти вопросы, оказываются зависимыми от догадок и предположений разработчиков, и очень повезёт, если разработчики встроят функции, ценные для клиентов.
Архитектура и атрибуты качества
Поскольку необходимость компромиссов — частое явление, архитекторы должны знать, какие атрибуты наиболее важны, иначе не смогут принять решения, ведущие к желаемым результатам.
Возвращение на поздних стадиях разработки или после выпуска и переделка архитектуры системы в рамках исправления недостатков качества — дорогое удовольствие. Поэтапное построение систем при отсутствии предварительного знания наиболее важных целей в области качества может привести к проблемам, которые трудно исправить. Как это часто бывает с программными проектами, потратив чуть больше времени на то, чтобы лучше понять цели в области качества, можно прийти к менее дорогим и более надёжным решениям.
Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
👍4
День 2038. #ЗаметкиНаПолях
Удалять или не Удалять
Мягкое удаление — это метод, используемый для того, чтобы пометить запись как удалённую, не удаляя её на самом деле. Во многих случаях вы будете использовать что-то вроде столбца IsDeleted, который устанавливается в true, когда запись удалена. Таким образом, запись на самом деле не будет удалена из БД, но будет помечена как удалённая.
Чаще всего «удалённые» записи не нужны, поэтому их отфильтровывают:
Вместо того, чтоб добавлять Where(…) к каждому запросу, можно использовать глобальный фильтр в Entity Framework:
И в каждом запросе из таблицы Users удалённые записи будут отфильтрованы. Конечно, вы можете изменить это, если хотите:
Подводные камни
1. Нужны ли удалённые записи?
Во многих случаях нет. А если они не нужны, зачем хранить их в базе? Они просто занимают место и замедляют запросы. То, что не нужно, можно удалить. При составлении отчётов может потребоваться просмотр удалённых записей, но как часто создаются отчёты?
2. Глобальные фильтры запросов
Это и хорошо, и плохо. Когда вы смотрите на запрос, он не говорит вам всей правды! Вы должны знать, что к запросу применяется глобальный фильтр. Это может сбивать с толку и приводить к ошибкам.
3. Производительность
Если в таблице много записей, запрос должен будет отфильтровывать удалённые каждый раз. Можно добавить индекс, но это повлияет на производительность вставки и обновления.
Альтернативы
1. Архивная таблица или схема
Так вы сможете сохранить удалённые записи, но они не будут находиться в той же таблице, что и активные. Таким образом можно поддерживать чистоту и быстроту таблицы активных записей. Также можно создать представление, которое объединит активные и архивные записи, если нужно увидеть их вместе в определённых обстоятельствах.
2. Архивная база данных
Так вы сможете поддерживать чистоту и быстроту БД активных записей. Это сложнее в настройке, но может быть хорошим решением, если у вас много удалённых записей. А если данных много, это может сэкономить деньги, если вы используете управляемые экземпляры в Azure или AWS. Например, Azure предлагает уровень архива, где даже терабайты данных стоят намного меньше, чем в активной БД (если вы не читаете оттуда много данных и вам не нужен «живой» архив).
Источник: https://steven-giesel.com/blogPost/a807373c-dcc6-42f9-995f-e69dcea1cd47/to-soft-delete-or-not-to-soft-delete
Удалять или не Удалять
Мягкое удаление — это метод, используемый для того, чтобы пометить запись как удалённую, не удаляя её на самом деле. Во многих случаях вы будете использовать что-то вроде столбца IsDeleted, который устанавливается в true, когда запись удалена. Таким образом, запись на самом деле не будет удалена из БД, но будет помечена как удалённая.
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsDeleted { get; set; }
}
Чаще всего «удалённые» записи не нужны, поэтому их отфильтровывают:
public async Task<IEnumerable<User>> GetUsers()
{
return await _context.Users
.Where(u => !u.IsDeleted).ToArrayAsync();
}
Вместо того, чтоб добавлять Where(…) к каждому запросу, можно использовать глобальный фильтр в Entity Framework:
protected override void
OnModelCreating(ModelBuilder mb)
{
mb.Entity<User>().HasQueryFilter(u => !u.IsDeleted);
}
И в каждом запросе из таблицы Users удалённые записи будут отфильтрованы. Конечно, вы можете изменить это, если хотите:
public async Task<IEnumerable<User>> GetAllUsers()
{
return await _context.Users
.IgnoreQueryFilters().ToArrayAsync();
}
Подводные камни
1. Нужны ли удалённые записи?
Во многих случаях нет. А если они не нужны, зачем хранить их в базе? Они просто занимают место и замедляют запросы. То, что не нужно, можно удалить. При составлении отчётов может потребоваться просмотр удалённых записей, но как часто создаются отчёты?
2. Глобальные фильтры запросов
Это и хорошо, и плохо. Когда вы смотрите на запрос, он не говорит вам всей правды! Вы должны знать, что к запросу применяется глобальный фильтр. Это может сбивать с толку и приводить к ошибкам.
3. Производительность
Если в таблице много записей, запрос должен будет отфильтровывать удалённые каждый раз. Можно добавить индекс, но это повлияет на производительность вставки и обновления.
Альтернативы
1. Архивная таблица или схема
Так вы сможете сохранить удалённые записи, но они не будут находиться в той же таблице, что и активные. Таким образом можно поддерживать чистоту и быстроту таблицы активных записей. Также можно создать представление, которое объединит активные и архивные записи, если нужно увидеть их вместе в определённых обстоятельствах.
2. Архивная база данных
Так вы сможете поддерживать чистоту и быстроту БД активных записей. Это сложнее в настройке, но может быть хорошим решением, если у вас много удалённых записей. А если данных много, это может сэкономить деньги, если вы используете управляемые экземпляры в Azure или AWS. Например, Azure предлагает уровень архива, где даже терабайты данных стоят намного меньше, чем в активной БД (если вы не читаете оттуда много данных и вам не нужен «живой» архив).
Источник: https://steven-giesel.com/blogPost/a807373c-dcc6-42f9-995f-e69dcea1cd47/to-soft-delete-or-not-to-soft-delete
👍17
День 2039. #Оффтоп
Баг в Течение Часа в Год
Было 8 ноября 2021, я работал сортировщиком ошибок в команде Google Docs. День начался как любой другой. Я заварил кофе и начал просматривать отчёты об ошибках за предыдущий день. Кое-что привлекло моё внимание.
Было необычно много сообщений об ошибках, и все они говорили об одном и том же. Пользователь создал ответ или новый комментарий в документе, но его метка времени на странице говорила, что он был создан «завтра». Быстро выявилась закономерность. Все ошибки появлялись:
- У пользователей в тихоокеанском часовом поясе (PT)
- Между 23:00 и 23:59 (PT) 7 ноября
Я бы посчитал это совпадением, но перевод на зимнее время случился в 2:00 (PT) 7 ноября!
Расследование
Баг меня заинтересовал, поэтому я решил заняться его устранением самостоятельно. Не потребовалось много времени, чтобы прийти к выводу, что ошибка, должно быть, в логике форматирования относительной даты Closure Library, которую использовал Docs. Код начинался с попытки вычислить количество дней между текущим временем и временем ввода через:
1. Получение текущего времени (new Date()).
2. Сброс часов, минут, секунд и миллисекунд объекта до нуля, чтобы получить время начала текущего дня.
3. Вычисление количества миллисекунд между временем начала текущего дня и временем ввода, деления его на количество миллисекунд в дне и округления в меньшую сторону.
Я некоторое время смотрел на код, и тут меня осенило! Введенное время между 23:00 и 23:59 (PT) 7 ноября было в поясе тихоокеанского стандартного времени (PST) после окончания летнего времени, но начало текущего дня (00:00) было в поясе тихоокеанского летнего времени (PDT) до окончания летнего времени. Поэтому в тот день между 00:00 и 23:00 не было 23 часов. Два времени были в двух разных часовых поясах с разницей в час, поэтому между двумя временами было 24 часа (целый день)! А что делал код, когда время ввода на день позже начала текущего дня? Он форматировал время как «завтра»…
Исправление
К счастью, класс Date в JS предоставляет удобный метод getTimezoneOffset(), который возвращает количество минут между часовым поясом объекта Date и часовым поясом UTC. Я использовал его для вычисления разницы. По сути, это удаляло любые различия, возникавшие только из-за изменения летнего/зимнего времени между текущим временем и временем начала текущего дня. Так что теперь количество часов между 00:00 и 23:00 в этот день вычислялось как 23!
Бонусный баг
Эта ошибка фактически распространялась и на время начала летнего времени. В 2021 году летнее время началось в 2:00 (по тихоокеанскому времени) 14 марта. Что же произойдёт, если оставить комментарий в 00:00 15 марта, когда текущее время было 23:00 14 марта?
Можно было бы ожидать, что между 00:00 и 23:00 вечера 14 марта будет 23 часа, но их всего 22. Из-за того, что летнее время начинается в 2:00, что на самом деле 3:00, между временем начала дня 14 и 15 марта всего 23 часа!
И что делает код для этого количества часов? Он вычисляет количество дней между двумя временами как ноль из-за округления в меньшую сторону и форматирует время как «сегодня»…
На самом деле этот баг не проявлялся в Google Docs, поскольку нельзя написать комментарий в будущем, и посмотреть его в прошлом.
Почему 1 час в год только по тихоокеанскому времени?
На самом деле эта ошибка теоретически могла произойти в любом часовом поясе. Возможно, все отчёты об ошибках в тот день были случайно получены от пользователей в тихоокеанском часовом поясе. Или, возможно, есть какой-то другой фактор, о котором я не подумал.
Источник: https://tomeraberba.ch/the-1-hour-per-year-bug
Автор оригинала: Tomer Aberbach
Баг в Течение Часа в Год
Было 8 ноября 2021, я работал сортировщиком ошибок в команде Google Docs. День начался как любой другой. Я заварил кофе и начал просматривать отчёты об ошибках за предыдущий день. Кое-что привлекло моё внимание.
Было необычно много сообщений об ошибках, и все они говорили об одном и том же. Пользователь создал ответ или новый комментарий в документе, но его метка времени на странице говорила, что он был создан «завтра». Быстро выявилась закономерность. Все ошибки появлялись:
- У пользователей в тихоокеанском часовом поясе (PT)
- Между 23:00 и 23:59 (PT) 7 ноября
Я бы посчитал это совпадением, но перевод на зимнее время случился в 2:00 (PT) 7 ноября!
Расследование
Баг меня заинтересовал, поэтому я решил заняться его устранением самостоятельно. Не потребовалось много времени, чтобы прийти к выводу, что ошибка, должно быть, в логике форматирования относительной даты Closure Library, которую использовал Docs. Код начинался с попытки вычислить количество дней между текущим временем и временем ввода через:
1. Получение текущего времени (new Date()).
2. Сброс часов, минут, секунд и миллисекунд объекта до нуля, чтобы получить время начала текущего дня.
3. Вычисление количества миллисекунд между временем начала текущего дня и временем ввода, деления его на количество миллисекунд в дне и округления в меньшую сторону.
Я некоторое время смотрел на код, и тут меня осенило! Введенное время между 23:00 и 23:59 (PT) 7 ноября было в поясе тихоокеанского стандартного времени (PST) после окончания летнего времени, но начало текущего дня (00:00) было в поясе тихоокеанского летнего времени (PDT) до окончания летнего времени. Поэтому в тот день между 00:00 и 23:00 не было 23 часов. Два времени были в двух разных часовых поясах с разницей в час, поэтому между двумя временами было 24 часа (целый день)! А что делал код, когда время ввода на день позже начала текущего дня? Он форматировал время как «завтра»…
Исправление
К счастью, класс Date в JS предоставляет удобный метод getTimezoneOffset(), который возвращает количество минут между часовым поясом объекта Date и часовым поясом UTC. Я использовал его для вычисления разницы. По сути, это удаляло любые различия, возникавшие только из-за изменения летнего/зимнего времени между текущим временем и временем начала текущего дня. Так что теперь количество часов между 00:00 и 23:00 в этот день вычислялось как 23!
Бонусный баг
Эта ошибка фактически распространялась и на время начала летнего времени. В 2021 году летнее время началось в 2:00 (по тихоокеанскому времени) 14 марта. Что же произойдёт, если оставить комментарий в 00:00 15 марта, когда текущее время было 23:00 14 марта?
Можно было бы ожидать, что между 00:00 и 23:00 вечера 14 марта будет 23 часа, но их всего 22. Из-за того, что летнее время начинается в 2:00, что на самом деле 3:00, между временем начала дня 14 и 15 марта всего 23 часа!
И что делает код для этого количества часов? Он вычисляет количество дней между двумя временами как ноль из-за округления в меньшую сторону и форматирует время как «сегодня»…
На самом деле этот баг не проявлялся в Google Docs, поскольку нельзя написать комментарий в будущем, и посмотреть его в прошлом.
Почему 1 час в год только по тихоокеанскому времени?
На самом деле эта ошибка теоретически могла произойти в любом часовом поясе. Возможно, все отчёты об ошибках в тот день были случайно получены от пользователей в тихоокеанском часовом поясе. Или, возможно, есть какой-то другой фактор, о котором я не подумал.
Источник: https://tomeraberba.ch/the-1-hour-per-year-bug
Автор оригинала: Tomer Aberbach
👍9👎2
День 2040. #УрокиРазработки
Уроки 50 Лет Разработки ПО
Урок 21. Проблемы легче предупредить, чем исправить
Время, потраченное на вдумчивое рассмотрение проекта, с лихвой окупается временем, которое не было потрачено на исправление проблем позже. Вам может понадобиться дополнительное время на то, чтобы усовершенствовать проект в условиях неопределённости, поэтому старайтесь распределять усилия в соответствии с характером проблемы. Однако даже приложив максимум усилий для создания безупречного проектного решения, позднее вы можете обнаружить недостатки и придётся их корректировать.
Технический долг и рефакторинг
Проекты, выполненные в спешке, могут привести к появлению технического долга — так называют недостатки, которые кто-то должен устранить в будущем, чтобы обеспечить надлежащее функционирование продукта и его расширяемость. Небольшой технический долг может быть приемлемым компромиссом, если экономия на проектировании и разработке кода ускоряет достижение текущей бизнес-цели. Однако недостатки остаются. Чем дольше команда откладывает их решение, тем более масштабной, дорогостоящей и разрушительной будет доработка. Как и любой кредит, технический долг следует рассматривать как временный и требующий постепенного погашения.
Доработка в целях уменьшения технического долга часто принимает форму рефакторинга — реорганизации существующего кода для улучшения его структуры без изменения функциональности. Вы можете упростить код, сделать его более удобным для сопровождения или расширения, повысить его эффективность, удалить повторяющиеся и ненужные части или внести другие улучшения.
Существенные изменения проекта могут потребовать значительных усилий по реорганизации, тогда как команды обычно предпочитают создавать новые полезные функции.
Внесение изменений в проект требует усилий, тогда как уровень ценности для клиента остаётся прежним, но этот труд помогает поддерживать стабильную основу, необходимую для дальнейшего развития продукта. Хорошее проектирование сводит к минимуму возникающий технический долг, а рефакторинг устраняет его. Разумный баланс этих двух факторов даёт наилучшие результаты. Недостаточное проектирование может привести к значительным доработкам; но на слишком детальную проработку проекта может потребоваться чересчур много времени, при этом хороший результат не гарантирован.
Постоянное совершенствование проектного решения упрощает работу с кодом. Однако на практике обычно выбирается другой путь: небольшой рефакторинг и большое внимание к целесообразности добавления новых функций.
Продумать или узнать все в начале проекта практически невозможно. Однако вы можете использовать свой опыт и опыт других, чтобы выбрать определённое направление. Сегодня вы можете принимать решения, которые завтра минимизируют потребность внесения изменений.
Цель проектирования в том, чтобы принять разумные решения сейчас и предотвратить ненужные изменения в будущем. Опираясь на свои рассуждения и мнение заинтересованных сторон, выбирайте такие решения, которые уменьшат вероятность появления необходимости изменять те или иные части продукта в будущем.
Архитектурные недостатки
Внесение небольших изменений в проект по мере продвижения — не слишком болезненный процесс, при этом продукт непрерывно и постепенно улучшается. Серьёзная реорганизация архитектуры ради увеличения устойчивости продукта или улучшения пользовательского опыта дастся гораздо сложнее.
Разработчики ПО всегда создают проект либо в процессе работы, либо в ходе тщательного обдумывания. Накопление технического долга из-за того, что у команды нет времени на надлежащее проектирование, просто отодвигает проблему в будущее, где её влияние продолжает расти. Инвестирование в проектное решение позволяет сэкономить силы и время на реструктуризацию и повторную реализацию в будущем, когда предпочтительнее работать над чем-то другим.
Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
Уроки 50 Лет Разработки ПО
Урок 21. Проблемы легче предупредить, чем исправить
Время, потраченное на вдумчивое рассмотрение проекта, с лихвой окупается временем, которое не было потрачено на исправление проблем позже. Вам может понадобиться дополнительное время на то, чтобы усовершенствовать проект в условиях неопределённости, поэтому старайтесь распределять усилия в соответствии с характером проблемы. Однако даже приложив максимум усилий для создания безупречного проектного решения, позднее вы можете обнаружить недостатки и придётся их корректировать.
Технический долг и рефакторинг
Проекты, выполненные в спешке, могут привести к появлению технического долга — так называют недостатки, которые кто-то должен устранить в будущем, чтобы обеспечить надлежащее функционирование продукта и его расширяемость. Небольшой технический долг может быть приемлемым компромиссом, если экономия на проектировании и разработке кода ускоряет достижение текущей бизнес-цели. Однако недостатки остаются. Чем дольше команда откладывает их решение, тем более масштабной, дорогостоящей и разрушительной будет доработка. Как и любой кредит, технический долг следует рассматривать как временный и требующий постепенного погашения.
Доработка в целях уменьшения технического долга часто принимает форму рефакторинга — реорганизации существующего кода для улучшения его структуры без изменения функциональности. Вы можете упростить код, сделать его более удобным для сопровождения или расширения, повысить его эффективность, удалить повторяющиеся и ненужные части или внести другие улучшения.
Существенные изменения проекта могут потребовать значительных усилий по реорганизации, тогда как команды обычно предпочитают создавать новые полезные функции.
Внесение изменений в проект требует усилий, тогда как уровень ценности для клиента остаётся прежним, но этот труд помогает поддерживать стабильную основу, необходимую для дальнейшего развития продукта. Хорошее проектирование сводит к минимуму возникающий технический долг, а рефакторинг устраняет его. Разумный баланс этих двух факторов даёт наилучшие результаты. Недостаточное проектирование может привести к значительным доработкам; но на слишком детальную проработку проекта может потребоваться чересчур много времени, при этом хороший результат не гарантирован.
Постоянное совершенствование проектного решения упрощает работу с кодом. Однако на практике обычно выбирается другой путь: небольшой рефакторинг и большое внимание к целесообразности добавления новых функций.
Продумать или узнать все в начале проекта практически невозможно. Однако вы можете использовать свой опыт и опыт других, чтобы выбрать определённое направление. Сегодня вы можете принимать решения, которые завтра минимизируют потребность внесения изменений.
Цель проектирования в том, чтобы принять разумные решения сейчас и предотвратить ненужные изменения в будущем. Опираясь на свои рассуждения и мнение заинтересованных сторон, выбирайте такие решения, которые уменьшат вероятность появления необходимости изменять те или иные части продукта в будущем.
Архитектурные недостатки
Внесение небольших изменений в проект по мере продвижения — не слишком болезненный процесс, при этом продукт непрерывно и постепенно улучшается. Серьёзная реорганизация архитектуры ради увеличения устойчивости продукта или улучшения пользовательского опыта дастся гораздо сложнее.
Разработчики ПО всегда создают проект либо в процессе работы, либо в ходе тщательного обдумывания. Накопление технического долга из-за того, что у команды нет времени на надлежащее проектирование, просто отодвигает проблему в будущее, где её влияние продолжает расти. Инвестирование в проектное решение позволяет сэкономить силы и время на реструктуризацию и повторную реализацию в будущем, когда предпочтительнее работать над чем-то другим.
Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
👍10