Перейти к содержанию

Рекомендации по написанию кастомных запросов

Рекомендации по написанию кастомных запросов

Кастомные запросы (расширение на 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 void AfterRequest(ICardRequestExtensionContext context) { var requestObject = context.Request.GetRequestObject(SomeProjectSomeModuleRequestExtensions.RequestFactory);

var name = requestObject.ID.ToString();

context.Response.SetResponseObject(new SomeProjectSomeModuleRequestExtensions.SomeResponseObject { Name = name, }); } }

Выполнение запроса:

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), который внутри себя содержит доступный репозиторий и еще какие-то необходимые зависимости.

Модели для взаимодействия клиента и сервера не обязательно должны быть вложенными типами. Их можно создавать в отдельных файлов, однако указывать все объекты в отдельном файле не рекомендуется (ущерб универсальности).

Back to top