Опытный разработчик не так давно зашёл в .Net и поставил цель получить сертификат Microsoft. Свой ежедневный прогресс он описывает на канале .Net Разработчик. Заметки об изученном материале, советы по повышению производительности и поддержке мотивации, ин
public interface IAsyncInit
{
Task Initialization { get; }
}
При реализации этого паттерна следует начать инициализацию, задав значение свойства Initialization в конструкторе. Доступ к результатам асинхронной инициализации (вместе с любыми исключениями) предоставляется через свойство Initialization. Пример реализации:
class MyType : IAsyncInit
{
public MyType()
{
Initialization = InitAsync();
}
public Task Initialization
{ get; private set; }
private async Task InitAsync()
{
// асинхронно инициализируем экземпляр
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
Экземпляр этого типа может быть создан и инициализирован примерно так:
var instance = DIContainer.Resolve<MyType>(); var asyncInit = instance as IAsyncInit; if (asyncInit != null) await asyncInit.Initialization;По возможности рекомендуется применять асинхронные фабрики или ленивую инициализацию вместо этого решения. Эти решения предпочтительны, потому что в них исключается доступ к неинициализированному экземпляру. Если ваши экземпляры создаются библиотеками внедрения зависимостей/инверсии управления, связывания данных и т. д., и вы вынуждены открыть доступ к неинициализированному экземпляру, придётся использовать паттерн асинхронной инициализации. Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
{
status: 404,
body: {
error: {
code: "resource_missing",
doc_url: "https://api.com/docs/errors/resource-missing",
message: "Клиент 'cus_Jop8JpEFz1lsCL' не найден. Он существует в производственной среде, но в этом запросе использовалась тестовая среда.",
param: "id",
type: "invalid_request_error"
}
},
headers: {
'api-version': '3.1.1',
}
}
Здесь мы:
- Используем правильный код ответа HTTP,
- Оборачиваем ошибку в объект «error»,
- Добавляем полезную информацию: код ошибки и тип ошибки,
- Даём ссылку на документацию,
- Сообщаем версию API,
- Предлагаем вариант решения проблемы
В результате сообщение об ошибке максимально наполнено информацией, так что даже начинающие разработчики смогут решить проблему самостоятельно.
Источник: https://dev.to/stripe/designing-apis-for-humans-error-messages-94p{
status: 200,
body: {
message: "Ошибка"
}
}
Оно кажется странным. Давайте рассмотрим, что здесь не так.
1. Отправляйте правильный код ответа
Выше это ошибка или нет? В сообщении говорится, что да, но код ответа 200 говорит, что всё в порядке. Это не только сбивает с толку, но и опасно. Большинство систем мониторинга ошибок сначала смотрят на код ответа, а затем пытаются проанализировать тело сообщения. Эта ошибка, скорее всего, будет «помещена в папку «OK»» и проигнорирована.
Коды ответа предназначены для машин, сообщения об ошибках — для людей. Необходимо устанавливать соответствующий код ошибки при возврате ответа из API.
2. Добавьте описание
Большинство людей согласятся с тем, что сообщение «Ошибка» так же полезно, как и вообще отсутствие сообщения. Код состояния ответа уже должен сообщить вам, произошла ошибка или нет, сообщение должно быть точным и помогать решить проблему.
Может показаться заманчивым иметь расплывчатые сообщения, чтобы скрыть от конечного пользователя любые детали реализации, однако помните, кто ваша аудитория. API предназначены для разработчиков, и они захотят точно знать, что пошло не так. Разработчики приложений должны отображать дружелюбное сообщение об ошибке, если она появляется, конечному пользователю. Получение сообщения «Произошла ошибка» может быть приемлемым, если вы сами являетесь конечным пользователем приложения, поскольку от вас не ожидают отладки проблемы (хотя это всё равно сбивает с толку). А разработчика приложения такой ответ скорее всего просто выведет из себя.
Изменим сообщение из предыдущего примера:
{
status: 404,
body: {
error: {
message: "Клиент не найден"
}
}
}
- У нас есть соответствующий код ответа: 404, ресурс не найден.
- Сообщение ясно: был запрос, который пытался получить клиента, и он не удался, потому что клиент не может быть найден.
- Сообщение об ошибке заключено в объект ошибки, что немного упрощает работу с ошибкой. Даже без кода ответа вы можете просто проверять наличие body.error, чтобы увидеть, произошла ли ошибка.
Уже лучше, но ещё есть куда расти. Ошибка описывает проблему, но по сути она бесполезна.
Окончание следует…
Источник: https://dev.to/stripe/designing-apis-for-humans-error-messages-94p( _ , args) =>{…}
Я ожидаю параметр, но не буду с ним ничего делать.
— Layla #WomenOfDotNet (@LaylaCodesIt)
Короткий ответ: да. Но кому нужны простые ответы? Пустые переменные появились в C# недавно. А это значит, что не всё так просто, как кажется.
В C# есть несколько мест, где мы используем _ как пустую переменную. В примере ниже _ технически не является ей:
Func<int, int, int> f = (_, x) => 2 * x;Лямбда здесь имеет два параметра, первый называется
_, а второй называется x. C# принимает _ в качестве идентификатора в соответствии с правилами языка. Даже Visual Studio при наведении курсора на _ показывает подсказку «(parameter) int _». Мы можем продемонстрировать это, используя оба параметра:
Func<int, int, int> f2 = (_, x) => _ * x;Следующий код не работал в C#8, но C#9 позволяет так писать:
Func<int, int, int> f3 = (_, _) => 42;Здесь символы подчеркивания действительно являются пустыми переменными. (Правило состоит в том, что если лямбда имеет несколько параметров с именем
_, то все они отбрасываются). A Visual Studio при наведении курсора на _ показывает подсказку «(discard) int _». Однако это работает только для лямбда-выражений.
Зачем это нужно?
Иногда мы вынуждены принимать аргументы, которые не будем использовать. Неиспользуемые параметры выглядят так, как будто это может быть ошибкой:
void ShowNumber(int i)
{
Console.WriteLine(42);
}
Анализатор кода не выдаёт даже предупреждения, а выдаёт только сообщение IDE0060 уровня Message о неиспользуемой переменной. Однако читателю этого кода неочевидно, есть ли ошибка в коде метода или так и задумано. При этом, если вы фанат чистых сборок — без предупреждений и сообщений анализатора, — тогда изменения кода, приводящие к появлению новых сообщений, немедленно привлекут ваше внимание.
Поэтому принято соглашение об использовании _ в качестве имени параметра, который бы намеренно игнорировался. Если вы измените предыдущий пример, переименовав параметр i в _, вы увидите, что компилятор перестанет выдавать сообщение IDE0060, поскольку он знает об этом соглашении:
void ShowNumber(int _)Но, если у вас есть два параметра, которые вы хотите игнорировать:
void ShowNumber(int _, int _)это вызовет ошибку компиляции CS0100 из-за дублирующего имени параметра
_, т.к. пустые переменные можно использовать только в лямбдах.
Использование _ - это просто соглашение. Это валидное имя для параметра, поэтому у нас не может быть двух параметров с именами _ по той же причине, по которой мы не можем иметь два параметра с именем i.
Всё становится ещё интереснее, когда _ используется в качестве имени локальной переменной. Например, что, по-вашему, выведет код с картинки ниже в C#9+?
Источник: https://endjin.com/blog/2022/09/csharp-lambda-discardspublic void Draw(Shape shape)
{
switch (shape.Type)
{
case ShapeType.Circle:
DrawCircle(shape);
break;
case ShapeType.Square:
DrawSquare(shape);
break;
default:
throw new
ArgumentOutOfRangeException();
}
}
Здесь, чтобы ввести новую форму, вам нужно будет изменить метод Draw. Чтобы исправить это, вы можете создать абстрактный класс Shape, а затем перенести логику рисования в подклассы:
public abstract class Shape
{
public abstract void Draw();
}
public class Circle : Shape
{
public override void Draw()
{ … }
}
Теперь, если нужно добавить новую фигуру, вы просто создаете подкласс и переопределяете метод Draw. Так вы закрыли класс Shape и открыли точку расширения в нём.
2. Бертран Мейер же говорит об обратной совместимости.
Когда есть несколько взаимозависимых модулей, вы не можете просто изменить свой модуль, когда хотите, вам нужно учитывать его клиентов.
Например, для библиотеки с открытым методом
CreateCustomer(string email)вы не можете просто добавить новый обязательный параметр:
CreateCustomer(string email, string accountNumber)Это будет критическим изменением для клиентского кода, который уже привязан к исходной версии метода. Это можно делать во время разработки, но не после публикации. Если нужно внести изменение после публикации, вы создаёте новый модуль (версию модуля). Однако вы по-прежнему можете изменять реализацию, если она не меняет API. Вся тема управления версиями веб-API — это, по сути, принцип OCP Мейера. Ещё один важный момент: версия OCP Мейера имеет смысл только в контексте нескольких команд разработчиков, когда каждый модуль разрабатывается разными командами. Если вы являетесь и автором, и «клиентом» кода, нет необходимости придерживаться таких сложных схем. Итак, две разновидности OCP, несмотря на то что имеют одно и то же имя, различаются по основному назначению. Код со switch, приведённый ранее, нарушает интерпретацию OCP Мартина, но не противоречит интерпретации Мейера. Интерпретация Мартина шире. Она направлена на сокращение количества изменений в целом, возможность расширить поведение ПО с небольшим изменением исходного кода или без него. Интерпретация Мейера направлена только на сокращение критических изменений: изменений, которые могут вызвать проблемы при совместной работе нескольких команд. Продолжение следует… Источник: https://enterprisecraftsmanship.com/posts/ocp-vs-yagni/
pi_3LKQhvGUcADgqoEM3bh6pslEЭтот формат более понятен для человека:
pi_3LKQhvGUcADgqoEM3bh6pslE └─┘└──────────────────────┘ └─ Префикс └─ Случайные символыНичего не зная об идентификаторе, мы можем сразу же понять, что здесь мы говорим об объекте PaymentIntent, благодаря префиксу
pi_. Когда вы создаёте PaymentIntent через API, вы фактически создаёте или ссылаетесь на несколько других объектов, включая Customer (cus_), PaymentMethod (pm_) и Charge (ch_). С помощью префиксов вы можете сразу различить все эти разные объекты:
var pi =
Stripe.PaymentIntents.Create(@"{
Amount = 1000,
Currency = 'usd',
Customer = 'cus_MJA953cFzEuO1z',
PaymentMethod = 'pm_1LaXpKGUcADgqo'
}");
Это помогает сотрудникам Stripe так же, как и разработчикам, интегрирующимся со Stripe. Например, вот фрагмент кода, который нужно отладить:
var pi = Stripe.PaymentIntents.Retrieve( id: id, stripeAccount: "cus_1KrJdMGUcADgqoEM" );Код пытается получить
PaymentIntent из подключённой учетной записи, однако, даже не глядя на код, вы можете сразу заметить ошибку: вместо идентификатора учетной записи (acct_) используется идентификатор клиента (cus_). Без префиксов это было бы намного сложнее отлаживать.
Полиморфный поиск
При создании PaymentIntent вы можете дополнительно указать параметр paymentMethod, чтобы указать, какой тип платежного инструмента вы хотите использовать. Вы можете указать здесь идентификатор источника (src_) или карты (card_) вместо идентификатора PaymentMethod (pm_):
var pi =
Stripe.PaymentIntents.Create(@"{
Amount = 1000,
Currency = 'usd',
Customer = 'cus_MJA953cFzEuO1z',
// Здесь может быть
// PaymentMethod, Card или Source ID
PaymentMethod = 'card_1LaRQ7GUcA'
}");
Без префиксов не было бы возможности узнать, какой объект представляет идентификатор, т.е. мы не знаем, из какой таблицы запрашивать данные объекта. Одним из способов может быть требование дополнительного параметра типа.
Это сработало бы, но усложняет API без дополнительной выгоды. Вместо того, чтобы иметь PaymentMethod в виде простой строки, теперь это хэш идентификатора. Всякий раз, когда вы используете идентификатор, вам нужно знать, какой тип объекта он представляет, что делает объединение этих двух типов информации в одном источнике гораздо лучшим решением, чем требование дополнительных параметров типа.
Предотвращение человеческой ошибки
Есть и другие менее очевидные преимущества, одно из которых — простота работы с идентификаторами, когда вы можете определить их тип по первым нескольким символам. Например, на сервере Stripe Discord используется функция Discord AutoMod, чтобы автоматически помечать и блокировать сообщения, содержащие живой секретный ключ API Stripe, который начинается с sk_live_. Утечка такого конфиденциального ключа может иметь серьёзные последствия для бизнеса. Поскольку ключи начинаются с sk_live_, написать регулярное выражение для фильтрации случайных утечек несложно.
Говоря о ключах API, префиксы live и test — это встроенный уровень защиты, который защищает вас от их смешивания. Те, кто особенно заботится о безопасности, могут настроить проверки, чтобы убедиться, что вы используете ключ только для соответствующей среды:
if (!app.Environment.IsDevelopment())
{
if (Regex.IsMatch("sk_live", "<API_KEY>"))
throw new Exception("Live key detected! Aborting!");
}
Источник: https://dev.to/stripe/designing-apis-for-humans-object-ids-3o5aStringSyntax, который используется в .NET 7 для более чем 350 параметров типа string, string[] и ReadOnlySpan<char>, свойств и полей, чтобы обозначить для клиента, какой синтаксис ожидается для передачи или установки. Теперь любой метод, который хочет указать, что строковый параметр, принимает регулярное выражение, добавить атрибут на параметр:
void MyMethod( [StringSyntax(StringSyntaxAttribute.Regex)] string input)Visual Studio 2022 обеспечит ту же проверку синтаксиса, подсветку синтаксиса и IntelliSense, что и для всех других методов, связанных с Regex. Строковые параметры, свойства и поля во всех основных библиотеках .NET теперь помечены атрибутами, чтобы обозначить, являются ли они регулярными выражениями, JSON, XML, строками составного формата, URL, строками числового формата и так далее. Подробнее об этом в недавнем видео от Ника Чапсаса https://youtu.be/Y2YOaqSAJAQ Источник: https://devblogs.microsoft.com/dotnet/regular-expression-improvements-in-dotnet-7/#stringsyntaxattribute-regex
edges.Count == 3 по своей сути верно для всех треугольников.
Другое важное свойство инвариантов состоит в том, что они определяют класс предметной области: благодаря им этот класс является тем, чем он является. Следовательно, вы не можете нарушить эти инварианты. Если вы это сделаете, доменный класс просто перестанет быть тем, что вы от него ожидаете, а станет чем-то другим. Например, если вы добавите четвёртую сторону к треугольнику, он станет четырёхугольником.
Наличие инвариантов — это то, что требует введения правил валидации. Без таких инвариантов, как edges.Count == 3, вам не нужно было бы проверять входные данные от внешних клиентов.
Таким образом, разница между валидацией и инвариантами — это просто вопрос точки зрения. Одни и те же бизнес-правила рассматриваются как инварианты моделью предметной области и как правила валидации сервисами приложений.
Это различие приводит к различному обращению с нарушениями этих бизнес-правил. Нарушение инварианта в модели предметной области — это исключительная ситуация, и на неё следует генерировать исключение и полностью останавливать текущую операцию (принцип отказоустойчивости).
С другой стороны, нет ничего исключительного в том, что внешний ввод неверен. Для этого и нужны прикладные сервисы: они разделяют (фильтруют) правильные и неправильные запросы. Вы не должны генерировать исключения в таких случаях и вместо этого должны использовать класс Result со статусом операции.
Можно предположить, что разница между валидациями и инвариантами в том, что валидации могут меняться в зависимости от бизнес-правил. Например, треугольник имеет 2 инварианта:
- ровно 3 стороны,
- каждая сторона больше нуля.
Но в нашей модели предметной области есть ещё условие:
- каждая сторона долна быть больше 10 см.
Это правило валидации (в отличие от инвариантов) можно изменить или удалить из нашей модели предметной области.
Действительно, интуитивно эти два условия не кажутся одинаковыми: наличие 3 сторон и требование, чтобы все стороны были больше 10 см. Одно условие необходимо для треугольников, а другое явно нет. Но это только потому, что мы привносим наш реальный опыт в область моделирования предметной области.
Какова цель моделирования предметной области? Максимально приблизиться к физическому миру? Сделать модель максимально реалистичной?
Нет.
Цель в том, чтобы построить модель, полезную для нашей конкретной проблемы. Не для всех возможных областей проблем и определённо не для какой-то сферической проблемы в вакууме. Для нашей конкретной.
Следовательно, если нашему приложению необходимо, чтобы все треугольники имели стороны больше 10 см, если работа с треугольниками меньших размеров не помогает нам достичь наших целей, то эти меньшие треугольники могут просто не существовать для целей нашего приложения. Т.е., если концепция бесполезна для модели, вы вообще не должны включать её в модель.
Да, треугольники со сторонами меньше 10 см могут существовать в других доменах, но в нашем конкретном их нет. Так же, как и четырёхугольники, пятиугольники и другие фигуры (при условии, что наше приложение работает только с треугольниками). Поэтому между этими условиями наличия ровно 3 сторон и всех сторон больше 10 см нет разницы. Оба являются инвариантами, составляющими понятие треугольника в нашем конкретном приложении.
Конечно, требования могут измениться, и условие 10 см может превратиться в 5 или 20 (или даже стать настраиваемым), но это регулярный процесс уточнения, когда вы лучше понимаете домен по мере продвижения проекта. Это не означает, что исходное условие не было инвариантом. Было. Так же, как новое является инвариантом сейчас.
Источник: https://khorikov.org/posts/2022-06-06-validation-vs-invariants/