Комментарии 28
Да, согласен, выигрыш небольшой, но он есть и достаётся нам совершенно бесплатно, потому что ни трудозатраты, ни сложность кода не увеличиваются.
Я не очнеь понимаю, каким образом у вас трудозатраты не увеличиваются? Можете объяснить еще раз ваше решение?
Имелось ввиду по сравнению с ручным копированием.
Мне казалось, ручное копирование применяется сравнительно редко именно в силу высокой стоимости поддержки, нет?
Безусловно, ручное копирование это наихудший вариант.
Если модельки имеют одноимённые свойства, то использование сторонних mapper'ов выглядит привлекательным, особенно с кодогенерацией.
Но если mapper'у приходится помогать с конфигурацией, то уже не всё так однозначно.
А какие есть варианты лучше, именно с точки зрения поддержки?
Автомаппинг.
У меня довольно мало случаев, когда маппинг 1 к 1: либо названия меняются, либо нужно преобразование с подтягиванием каких-то данных делать. Чисто теоретически, сложилось ощущение, то автомаппинг настраивать надо и это как-то не сильно выгоднее, чем присвоить поле руками.
В каких реальных кейсах помогает автомапинг? есть примеры кода?
Мне не чтобы докопаться, мне чтобы работу свою облегчить.
В каких реальных кейсах помогает автомапинг?
В тех, где большая часть имен совпадает (это именно тогда, когда я его использую).
Еще автомаппинг помогает тогда, когда есть одинаковые правила преобразования (например, один и тот же тип в разных местах иерархии преобразуется в один и тот же тип в целевой иерархии).
есть примеры кода?
Нет, это то, что я делал по работе, следовательно, под NDA. А если переписать для примера, оно будет неотличимо от любого примера, который уже есть в интернете.
Вместо перемапливания создаём класс или структуру суть которой сводится к public int Id => person.Id;
. Я так даже делал когда генерировал отчёт и обёртка уходила в генератор экселины.
А бесплатность была про то, что код маппинга всё равно писать и без разницы будет это обёртка или копирование в отдельный объект.
Я же главную проблему с обёртками вижу в связи обёртки с оборачиваемым типом. В ситуации, когда зависимая библиотека выставляет контракт и ничего не знает про наши типы, обёртку в неё можно будет передать только как наследника интерфейса. В дикой природе такое не встречается и обычно либы принимают какие-то свои ДТОхи.
обычно либы принимают какие-то свои ДТО
в теории можно заэкстендить dto, добавить ссылку на обьект, заоверрайдить геттеры
Кажись нет: не получится оверрайдить не виртуальные функции и после каста к исходной дто, значения будут читаться из исходных полей.
Если DTO и геттер - не sealed.
Вот пример где DTO и геттер - не sealed. Один фиг не получится поле заоверрайдить, если оно не виртуальное.
Если не прав, жду ссылку на код.
Всё верно.
Вообще идею с интерфейсами я подсмотрел у дяди Боба в схеме чистой архитектуры. Там помимо "большой круглой штуки" в центре есть схема справа снизу, на которой как раз сценарий использования (Use Case) и 2 интерфейса к нему: Use Case Input Port и Use Case Output Port.
Use Case Input Port позволяет абстрагироваться от типа входных параметров. Это позволяет использовать модель запроса WEB API (RequestDto) в качестве также и в качестве параметров сценария использования. Не работает с MediatR, но прекрасно работает при инъектировании сценария использования в контроллер через DI и вызова обработчика напрямую. Такой подход и интерфейс сценария использования ( IQueryHandler I CommandHandler ) описывал Максим Аршинов в статье "Быстрорастворимое проектирование".
Use Case Output Port позволяет скрыть детали реализации результата сценария использования от вызывающего кода.
Однако если взять описываемый вами случай, т.е. стороннюю библиотеку принимающую конкретный DTO в качестве параметра метода, то да - тут без вариантов.
А бесплатность была про то, что код маппинга всё равно писать и без разницы будет это обёртка или копирование в отдельный объект.
Я обычно не пишу код маппинга, для этого инструменты есть.
Я же главную проблему с обёртками вижу в связи обёртки с оборачиваемым типом.
Это да, это крупная проблема.
Я люблю использовать обёртки структуры над элементарными типам. Например, если в бд есть число наличия товара и 0 - обозначает "под заказ", то обёртка "доступное количество" будет содержать метод для проверки под заказ ли этот товар, чтобы по коду не расползалисьavailableQuantity == 0.
Ну или ноль в специальной константе находится, но сути это не меняет. Т.е. знание о том, что значит 0 находится ровно в одном месте и это хорошо.
На самое интересное, что структура над интом в результате компиляции исчезает из ассемблерного кода: по функциям путешествует обычный инт, а функции инлайнятся. Прямо zero cost abstraction.
public interface ICreateTodoListCommandArgs
Непонятно зачем нужен этот интерфейс. Какие у него могут быть реализации кроме CreateTodoListRequest?
если TResult будет составным
Ни резу такого не видел. Можете привести несколько примеров из реальной жизни?
А такая реализация, согласитесь, уже не выглядит адекватной.
Хотелось бы более адекватную аргументацию :)
Непонятно зачем нужен этот интерфейс. Какие у него могут быть реализации кроме
CreateTodoListRequest?
Этот интерфейс нужен для разделения моделей уровня бизнес-логики и API. И да, скорее всего в первой версии будет только одна реализация CreateTodoListRequest. Но что если в какой-то момент захочется сменить фрейморк, например с контроллеров переехать на Minimal API, или вообще куда-нить в сторону gRPC. Классы генерируемые на основе proto-файлов являются partial, т.е. их можно доопределить реализацией интерфейса. А с dto только копирование. Таким образом использование интерфейса даёт чуть больше свободы.
Ни резу такого не видел. Можете привести несколько примеров из реальной жизни?
Я "игрался" с проектом Jason Taylor и у него есть вот такое:
public class TodosVm
{
public IReadOnlyCollection<LookupDto> PriorityLevels { get; init; } = Array.Empty<LookupDto>();
public IReadOnlyCollection<TodoListDto> Lists { get; init; } = Array.Empty<TodoListDto>();
}
Если с данными БД или JSON все более менее ясно, то, например, с Protobuf все совсем неоднозначно. Десериализовать всё - не всегда нужно. Декодировать каждый раз из Protobuf - не эффективно. Все равно приходится по выборочному списку декодировать данные в память.
Мне не понятен ход ваших мыслей - в статье не затрагиваются вопросы сериализации \ десериализации данных
В статье рассматривается передача данных между слоями приложения. Для микросервисной архитектуры - это, в том числе, данные gRPC. В принципе, даже протокол связи с СУБД вполне имеет право возвращать и принимать данные в Protobuf.
Если на пальцах. У Вас рассматривается получение данных их БД в бинарном виде и передача их в сериализованном JSON. А теперь представьте, что они у Вас и на входе, и на выходе - в Protobuf
А. Тогда давайте уточним - под gRPC и Protobuf наверно имеется ввиду сериализованный объект, т.е. массив байт? В этом случае при передаче между слоями будет копироваться только ссылка на массив, что практически бесплатно. Правда в этом случае не совсем понятна роль приложения - принять массив байт и сохранить как есть..?
Однако массив байт десериализовать в объект и затем попытаться этот объект передать в другой слой приложения, то как раз возникают описанные в статье 3 варианта реализации: через интерфейс, обёртку или копирование в dto целевого слоя.
под gRPC и Protobuf наверно имеется ввиду сериализованный объект, т.е. массив байт?
Имеется ввиду именно gRPC и Protobuf, который не только сериализованный объект, но еще и содержащий бинарные данные в сжатом виде.
при передаче между слоями будет копироваться только ссылка на массив
Это бессмысленно. Если сервис получил gRCP вызов, для ответа на который он должен сделать несколько gRPC вызовов к другим сервисам (в частности - к БД), то вернуть то он должен только, условно, одну десятую полей из полученных ответов. Причем с какой-то трансформацией или анализом некоторых из них.
Однако массив байт десериализовать в объект и затем попытаться этот объект передать в другой слой приложения
Так в том то и проблема, что десериализовывать полностью все ответы от всех gRPC запросов неэффективно, раз нам нужны только десять полей из сотни. Благо Protobuf, в отличии от JSON или XML, позволяет с минимальными издержками обращаться только к нужным полям сериализованных данных, не декодируя не нужные поля.
В этом случае и приходим к тому, что я написал: "Все равно приходится по выборочному списку декодировать данные в память."
То есть, те поля из полученных сообщений, которые можно передавать в исходном виде даже не надо декодировать и снова кодировать. Декодировать в память требуется только те поля, которые нужно анализировать или трансформировать. И тут без выборочного списка уже не обойтись.
JSON явно устарел и активно вытесняется такими бинарными форматами, как Protobuf, Thrift и т.п., не требующих полной десериализации и хранящих данные в полях в сжатом виде.
... лучше не стало.
Имеется ввиду именно gRPC и Protobuf, который не только сериализованный объект, но еще и содержащий бинарные данные в сжатом виде.
Давайте всё-таки определимся с терминологией.
Protobuf = Protocol Buffers - независимый от платформы и языка механизм сериализации \ десериализации структурированных данных. Вот тут также упоминается, что компрессия (сжатие) не используется.Однако полученный массив байт можно сжимать.
gRPC = google Remote Procedure Call = Удалённый вызов процедур. В качестве основного способа сериализации \ десирализации по умолчанию используется Protocol Buffers, однако при желании (2) можно использовать и другие форматы, например тот же JSON или Avro.
Можете привести пример кода с частичной десериализацией Protobuf ?
А то google ничего внятного по запросу не выдаёт и я с такой реализацией не сталкивался.
компрессия (сжатие) не используется
Не совсем так. По Вашей же ссылке есть даже примеры:
Variable-width integers, or varints, are at the core of the wire format. They allow encoding unsigned 64-bit integers using anywhere between one and ten bytes, with small values using fewer bytes.
То есть нет компрессии в общепринятом понимании, но есть кодирование сокращающее объем данных, что можно называть сжатием.
можно использовать и другие форматы, например тот же JSON или Avro.
Можно. Но использование JSON будет не эффективным. Альтернативно используют Thrift.
Можете привести пример кода с частичной десериализацией Protobuf ?
В отличии от JSON, Protobuf и Thrift не требуют полной десериализации. То есть, если в принятом сообщении есть 10 структур (message) с любым уровнем вложенности каждая, а декодировать нужно только одну из них, то не нужные структуры можно просто пропустить, десериализируя (декодируя) только нужную. А передать в ответ на gRPC вызов пропущенные структуры можно без их декодирования в исходном виде при помощи gRPC::GenericStub.
В принципе, клиент может быть скомпилирован так, что он из этих 10 структур будет знать лишь две-три. Тогда о наличии в сообщении остальных структур он даже знать не будет, автоматически пропуская их. То есть, если в том же JSON Вам встречается не известная или не нужная структура данных, Вы все равно вынуждены её десериализовать хотя бы для того, чтобы найти, где же она заканчивается. В Protobuf и Thrift такой проблемы нет, так как в сообщении указывается длина бинарных данных всей структуры и её можно просто пропустить. Но данные кодируются в формате, отличном от форматов соответствующих типов данных в используемых языках программирования, что требует их кодирования. И так же декодирования, но только если доступ к ним нужен в программном коде.
После ваших маппингов - в большинстве случаев будет запрос в базу, который будет на 2-3 порядка более длительный. Так что берёшь любой маппер (Mapperly, Mapster, AutoMapper - пофигу) и радуешься жизни. А пока что это напоминает преждевременную оптимизацию :)
Передача данных между слоями приложения