Pull to refresh

Comments 28

Да, согласен, выигрыш небольшой, но он есть и достаётся нам совершенно бесплатно, потому что ни трудозатраты, ни сложность кода не увеличиваются.

Я не очнеь понимаю, каким образом у вас трудозатраты не увеличиваются? Можете объяснить еще раз ваше решение?

Имелось ввиду по сравнению с ручным копированием.

Мне казалось, ручное копирование применяется сравнительно редко именно в силу высокой стоимости поддержки, нет?

Безусловно, ручное копирование это наихудший вариант.
Если модельки имеют одноимённые свойства, то использование сторонних mapper'ов выглядит привлекательным, особенно с кодогенерацией.
Но если mapper'у приходится помогать с конфигурацией, то уже не всё так однозначно.

А какие есть варианты лучше, именно с точки зрения поддержки?

У меня довольно мало случаев, когда маппинг 1 к 1: либо названия меняются, либо нужно преобразование с подтягиванием каких-то данных делать. Чисто теоретически, сложилось ощущение, то автомаппинг настраивать надо и это как-то не сильно выгоднее, чем присвоить поле руками.

В каких реальных кейсах помогает автомапинг? есть примеры кода?

Мне не чтобы докопаться, мне чтобы работу свою облегчить.

В каких реальных кейсах помогает автомапинг?

В тех, где большая часть имен совпадает (это именно тогда, когда я его использую).

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

есть примеры кода?

Нет, это то, что я делал по работе, следовательно, под NDA. А если переписать для примера, оно будет неотличимо от любого примера, который уже есть в интернете.

Вместо перемапливания создаём класс или структуру суть которой сводится к public int Id => person.Id;. Я так даже делал когда генерировал отчёт и обёртка уходила в генератор экселины.

А бесплатность была про то, что код маппинга всё равно писать и без разницы будет это обёртка или копирование в отдельный объект.

Я же главную проблему с обёртками вижу в связи обёртки с оборачиваемым типом. В ситуации, когда зависимая библиотека выставляет контракт и ничего не знает про наши типы, обёртку в неё можно будет передать только как наследника интерфейса. В дикой природе такое не встречается и обычно либы принимают какие-то свои ДТОхи.

обычно либы принимают какие-то свои ДТО

в теории можно заэкстендить dto, добавить ссылку на обьект, заоверрайдить геттеры

Кажись нет: не получится оверрайдить не виртуальные функции и после каста к исходной дто, значения будут читаться из исходных полей.

Если DTO и геттер - не sealed.

Я же не говорю, что если не sealed, то получится. sealed - не единственный способ это сломать, просто самый наглядный. Ну и да, на самом деле, разницы между не-virtual-геттером, и геттером override 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.

В EF Core 8 как раз разрешили использовать структуру в качестве ComplexType (ValueObject).

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, который не только сериализованный объект, но еще и содержащий бинарные данные в сжатом виде.

Давайте всё-таки определимся с терминологией.

  1. Protobuf = Protocol Buffers - независимый от платформы и языка механизм сериализации \ десериализации структурированных данных. Вот тут также упоминается, что компрессия (сжатие) не используется.Однако полученный массив байт можно сжимать.

  2. 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 - пофигу) и радуешься жизни. А пока что это напоминает преждевременную оптимизацию :)

Sign up to leave a comment.

Articles