Рекомендации по написанию кастомных запросов
Рекомендации по написанию кастомных запросов¶
Кастомные запросы (расширение на CardRequestExtension) - механизм, позволяющий составлять запросы, не привязанные к событиям системы, для реализации какой-то специфичной логики (подгрузка данных, сообщение о событии и т.д.). Это полезный, но в то же время опасный механизм.
Необходимо понимать, что кастомные реквесты чаще всего избыточны и данные можно получать вместе с какими-то стандартными запросами системами.
Как определить, необходим ли кастомный реквест? Допустим, необходимо получить данные с сервера на клиент. Нужно ответить для себя на следующие вопросы: “Что вообще необходимо получить?” и “Как часто могут изменяться данные?”.
-
Данные меняются очень редко. Это, например, настройки. В таком случае используют расширения на инициализацию клиента (Init stream). При получении данных на клиент информация сохраняется в какой-то объект (можете создать свой тип), зарегистрированный в Unity как Singleton. При необходимости в расширениях можно получить из контейнера этот объект и взять из него данные.
-
Данные связаны с карточкой. В таком случае необходимо передавать данные в Info карточки в Get-расширении. Это, в том числе, видимость объектов карточки или доп. условия. Например, в Info можно передавать флаг, при наличии которого отображается плитка.
-
Нажатие на кнопку/тайл. В таком случае рекомендацией является сохранение карточки с передачей в Info информации о том, что действие совершено (кнопка нажата) и специальные расширения на сохранение должны это обработать.
Если ситуация не попадает под описанное выше и вы понимаете, что кастомный реквест действительно необходим, тогда важно правильно структурировать и описывать API, создаваемое вами для этих запросов.
Рассмотрим пример, как рекомендуется оформлять кастомные запросы. Основная идея предлагаемого подхода основана на extension методах языка C#
Для начала, нужен основной класс с хелперным экстеншенами следующего содержания:
public static class CardRequestExtensions
{
public const string RequestObjectKey = "RequestObject";
public const string ResponseObjectKey = "ResponseObject";
public static async Task<(TResp, ValidationResult)> SendCardRequestAsync<TReq, TResp>(
this ICardRepository repo,
Guid reqID,
TReq request,
Func<Dictionary<string, object>, TResp> responseFactory)
where TReq: StorageObject
where TResp: StorageObject
{
var cardRequest = new CardRequest
{
RequestType = reqID,
Info = new Dictionary<string, object>
{
[RequestObjectKey] = request.GetStorage(),
}
};
var response = await repo.RequestAsync(cardRequest);
TResp responseObject = null;
if (response.Info.TryGetValue(ResponseObjectKey, out var respObj)
&& respObj is Dictionary<string, object> respStorage)
{
responseObject = responseFactory(respStorage);
}
// Всегда нужно помнить про ValidationResult и всегда его проверять.
return (responseObject, response.ValidationResult.Build());
}
public static TReq GetRequestObject<TReq>(
this CardRequest request,
Func<Dictionary<string, object>, TReq> requestFactory)
where TReq: StorageObject
{
if (request.Info.TryGetValue(RequestObjectKey, out var respObj)
&& respObj is Dictionary<string, object> respStorage)
{
return requestFactory(respStorage);
}
return null;
}
public static void SetResponseObject<TResp>(
this CardResponse response,
TResp responseObject)
where TResp: StorageObject
{
response.Info[ResponseObjectKey] = responseObject.GetStorage();
}
}
Тут выполнение запроса и работа с “поставить/достать” объекты. Кстати, StorageObject - очень важный базовый класс для очень многих типов в тессе и нужно уметь его правильно использовать в своем коде. Это некоторая обертка над словарем Dictionary, которая позволяет писать строготипизированные типы-декораторы над хранилищем и упростить и унифицировать сериализацию. Большинство объектов системы (Card, CardTask и др) наследуют данный класс.
Теперь возникает потребность в описании конкретного реквеста:
public static class SomeProjectSomeModuleRequestExtensions
{
#region Request #1
public sealed class SomeRequestObject : StorageObject
{
public SomeRequestObject()
: base(new Dictionary<string, object>())
{
this.Init(nameof(this.ID), default);
}
public SomeRequestObject(Dictionary<string, object> storage)
: base(storage)
{
}
public SomeRequestObject(SerializationInfo info,
StreamingContext context)
: base(info, context)
{
}
public Guid ID
{
get => this.Get<Guid>(nameof(this.ID));
set => this.Set(nameof(this.ID), value);
}
}
public sealed class SomeResponseObject : StorageObject
{
public SomeResponseObject()
: base(new Dictionary<string, object>())
{
this.Init(nameof(this.Name), default);
}
public SomeResponseObject(Dictionary<string, object> storage)
: base(storage)
{
}
public SomeResponseObject(
SerializationInfo info,
StreamingContext context)
: base(info, context)
{
}
public string Name
{
get => this.Get<string>(nameof(this.Name));
set => this.Set(nameof(this.Name), value);
}
}
public static readonly Guid SomeRequestID = new Guid("...");
public static readonly Func<Dictionary<string, object>, SomeRequestObject> RequestFactory = p => new SomeRequestObject(p);
public static readonly Func<Dictionary<string, object>, SomeResponseObject> ResponseFactory = p => new SomeResponseObject(p);
public static async Task<(SomeResponseObject, ValidationResult)> SendSomeRequestAsync(
this ICardRepository repo,
SomeRequestObject requestObject) =>
await repo.SendCardRequestAsync(SomeRequestID, requestObject, ResponseFactory);
#endregion
#region Request #2
//...
#endregion
}
Объявляются строгие типы запроса и ответа, которые могут содержать (практически) любые данные, а также константа requestID и три однострочные функции.
Описание логики запроса:
public class SomeRequestExtension : CardRequestExtension
{
public override Task AfterRequest(ICardRequestExtensionContext context)
{
var requestObject = context.Request.GetRequestObject(SomeProjectSomeModuleRequestExtensions.RequestFactory);
var name = requestObject.ID.ToString();
context.Response.SetResponseObject(new SomeProjectSomeModuleRequestExtensions.SomeResponseObject
{
Name = name,
});
return Task.CompletedTask;
}
}
Выполнение запроса:
public class SomeClass
{
private ICardRepository repo;
public async Task Foo()
{
var responseObject = await this.repo.SendSomeRequestAsync(
new SomeProjectSomeModuleRequestExtensions.SomeRequestObject
{
ID = Guid.NewGuid(),
});
// Обработка результата
}
}
Итак, как видно ни вызывающей стороне, ни стороне выполнения запроса не приходится иметь дело с деталями сериализации, наборами полей и т.д. Все скрыто в наших объектах. Эти объекты могут быть хорошо описаны, на свойствах может висеть <summary> и т.д. Таким образом, мы не задумываемся о том, какие точно поля нам нужно передавать и по какому ключу. Задумываемся лишь о том, какие данные нужно передать в объекте.
Почему именно extension методы? Потому что можно создавать несколько классов с методами расширения, работать с ними параллельно, не пересекаться между разработчиками, а на практике использования (пример 3 и 4) ничего не меняется.
Приведенный код является демонстрацией подхода, под ваши нужды он может быть модифицирован.
При необходимости можно расширять не ICardRepository, а собственный тип ICustomRequester (который тоже должен быть зарегистрирован в Unity), который внутри себя содержит доступный репозиторий и еще какие-то необходимые зависимости.
Модели для взаимодействия клиента и сервера не обязательно должны быть вложенными типами. Их можно создавать в отдельных файлов, однако указывать все объекты в отдельном файле не рекомендуется (ущерб универсальности).