Как стать автором
Обновить

Фотографируем объекты в C#: хроника и сопоставление снимков, реконструкция состояния по снимку

Время на прочтение4 мин
Количество просмотров6.1K
Всего голосов 10: ↑9 и ↓1+8
Комментарии21

Комментарии 21

В режиме редактирования оригинала при изменении сущности в диалоговом окне соответствующие значения немедленно обновляются и в главном

За такие диалоговые окна хочется начать бить уже сейчас и больно. Редактировать табличные данные в чём то по мимо ячеек таблицы — грех

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

В примере для статьи диалоговое окно добавлено в демонстрационных целях. :-)
Если таблица отображает сущность и нужна валидность сохраненной сущности — то редактирование ячейки потребует отдельного действия для сохранения, а все изменения будут временными и это будет менее прозрачно для пользователя.
Это не так. В таблице может быть не полный набор полей, например.
Это дело вкуса. Мне вот редактирование внутри табицы кажется неудобным. Хорошо иметь это опционально для ввода большого объёма данных или массового обновления, но в большинстве случаев формы будут удобнее.
Очень нравится вариант, когда активная ячейка превращается в форму ввода.
За такие диалоговые окна хочется начать бить уже сейчас и больно. Редактировать табличные данные в чём то по мимо ячеек таблицы — грех
У персоны может оказаться более одного телефона, e-mail и т.п. в общем таблица оказывается не совсем таблицей, а целой связкой таблиц… Плюс достаточно частая ситуация, когда персональная настройка отображения таблицы подразумевает скрытие части колонок. Да и в конце концов ширина может оказаться такой, даже в варианте «карточки» это будет многотабовая карточка.

в таком случае treeview с колонками либо detailed row для активной строки таблицы (что в разы хуже).

Ни то ни другое совершенно не увяжется в ситуации, когда к примеру фигурирует фото (несколько) персоны и кучка иных атрибутов…
А вот по сути картотека с многостраничными «досье» (грид и карточки) — очень даже ничего.
Если он вносит какие-либо корректировки в данные, то при обработке формы хорошим тоном является запрос-подтверждение перед окончательным применением внесённых правок.


Первый раз этот бред слышу. Появилась форма, отредактировал, нажал ОК — значит хотим сохранить. Cancel — отмена, что тут ещё подтверждать??

Заголовок тоже весьма закрученный вокруг простой идеи: «форма редактирования и откат изменений».

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

Объект Person… наворотить геттеров-сеттеров — много ума не надо, студенты уже так сделали мильён раз и это ни разу не интересно. Вот как редактировать, когда объекты недоступны? Дали тебе бинарь с классами — что хочешь, то и делай!

Наверное, этот «Replication Framework» где-то и нужен, но в данной задаче это полная фигня.
Под подтверждением подразумевается нажатие кнопки Ок.

Соль в том, что не нужно самому реализовывать рутинную логику копирования и сравнения сущностей, поскольку всю эту работу берёт на себя фреймворк. В случае единичной сущности, как в примере, выигрыш в коде не столь очевиден, но когда в программе сущностей много, разница становится заметна — практически всю логику по копированию можно заменить парой обобщённых методов.
Если реализуется INotifyPropertyChanged, как показано в примерах, то ничего сравнивать уже не надо, мы итак знаем, что было изменено. А для копирования можно взять тот же AutoMapper, который уже давно вырос в серьёзный и проверенный временем тул. Не вижу причин использовать фреймворк, по крайне в этой задаче совершенно точно он не нужен, даже с большой натяжкой. Его использование только всё усложнит.
По INotifyPropertyChanged, пускай изначальное значение свойства было abc, потом пользователь отредактировал его на abcd, после чего на другое значение и так n раз, но в конечном счёте решил вернуть abc и нажал Ok. В итоге нотификация сработала, но значение осталось прежним, не изменилось. Конечно, можно запоминать исходные данные и потом выполнять сравнение, но это же тот самый подход от которого мы хотим отойти…

Да, можно использовать AutoMapper, различные сериализаторы, библиотеку Compare net objects, но Replication Framework, если разобраться детально, во многих отношениях стремится обобщить их функциональность и решать более широкий круг проблем.

Не совсем ясно, в чём именно состоит усложнение, если использовать библиотеку для задачи из публикации. Конечно, нужно изучить и понять новый api, но он ведь не настолько обширный и сложный.

Или всё-таки что-то показалось не достаточно понятным?
Мне вот что непонятно. Если мне нужно знать изменения объекта, то я реализую INotifyPropertyChanged. И как минимум два варианта есть, узнать конечные изменения:

1. Объект хранит начальное состояние.
2. Объект, который много раз менялся, я присваиваю с помощью AutoMapper объекту, реализующему INotifyPropertyChanged. Т.е. получу абсолютно то же самое, что делает фреймворк, один в один.

В целом, использование сравнения по всем полям для выяснения изменений является более накладной процедурой, чем просто отслеживать эти изменения. Хранить кучу копий в памяти, чтобы хранить граф изменений? Вы про Event Sourcing слышали? Он решает именно эту задачу и очень экономно, без создания избыточных копий, только здесь фреймворк мало чем поможет.

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

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

И процедура вычисления изменений в бизнес-логике может быть гораздо сложнее, чем сравнить все поля. Например, один крыжик отключает группу полей совсем, и сравнивать их не надо вообще. А некоторые изменения просто не принимаются, потому что всё равно не будут сохранены (поля только для чтения).

Хотя сам по себе фреймворк имеет право на жизнь. Но не в данном примере точно. Пример нужно подобрать более уместный.
Конкретно в примере INotifyPropertyChanged был введен, чтобы немедленно отображать изменения в гриде при редактировании оригинальной сущности в диалоге (демонстрационная цель). Конечно, если разработчику доступна реализация этого интерфейса, то для контроля изменений он может завязаться на событийную модель, это его выбор. И, действительно, для ряда задач это будет наиболее оптимальным решением, например, при обновления состояния интерфейса или слежениии за изменением определённых свойств. Но не всегда реализация этого интерфейса доступна, поэтому приходится применять другие решения.

RF позволяет довольно гибко сопоставлять снимки объектов, и не только по всем полям. Результатом операции сопоставления является IEnumerable, что позволяет реализовывать даже весьма экзотические сценарии (прекращать сравнение при достижении n различий, пропускать ненужное, вычислять «похожесть» снимков, например, количество одинаковых полей делить на общее, сопоставлять объекты разных типов). Конечно, если логика сравнения бизнес-сущностей в определённом случае очень специфическая, то, возможно, имеет смысл реализовать её вручную, но ведь это тоже не самый распространённый сценарий.

Снимки полностью изолированы от исходного объекта, поэтому при сравнении состояний с многопоточностью проблем нету.

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

Больше всего меня смущает вот что (из вашей документации): «но иногда полезно иметь возможность вернуть [откатить] граф и входящие в него объекты к какому-то определённому состоянию зафиксированному ранее» — вот бы увидеть пример, когда это бывает полезно, хранить граф 1000 объектов == 1000 изменений. И как я буду откатываться, куда и зачем? К 467-му изменению назад? Какой-нибудь действительно практический и живой пример очень не помешал.

Спасибо.
Ну, если даже взглянуть на пример из статьи, то в нём редактирование сущности в диалоговом окне можно осуществить двумя способами (флаг Create copy): с созданием новой копии объекта и без его копирования с непосредственным редактированием оригинала. Так вот, если изменить оригинал, то правки сразу отразятся в главном окне, но если нажать Cancel, то при скрытии диалога произойдёт откат (реконструкция) до состояния, зафиксированного на снимке.

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

Данная функциональность позволяет вести хронологию изменений графа и сопоставлять снимки в произвольных комбинациях. Например, делать что-то вроде бэкапов или кнопки Undo. Что касается вопроса про граф из 1000 объектов, поясню. Пусть дан такой граф, в какой-то момент делаем его снимок и явным образом помещаем исходные объекты в ReplicationCache (он удерживает экземпляры от сборки мусора и хранит ссылку-идентификатор каждого экземпляра). Далее с графом может происходить что угодно: изменяться структура, добавляться и удаляться объекты, — но когда у нашего снимка будет вызван метод ReconstructGraph, то все объекты из ReplicationCache вернутся к состоянию на момент создания снимка и образуют исходный граф из тысячи объектов.

Создание снимка — это своего рода промежуточная сериализация в Json-подобную структуру данных из словарей, но с которой значительно более удобно работать, чем с жёсткой Json-строкой. Но при необходимости снимки можно преобразовывать в строку и восстанавливать без искажений.

Благодарю за интерес! Если возникают вопросы, то свободно их задавайте.
Спасибо, про работу реконструкции теперь ясно. Только ещё небольшое уточнение.

По поводу «1000 объектов» я имел в виду не 1000 объектов в самом графе объекта, а 1000 копий. Мне показалось, что ReplicationCache хранит полную копию графа при каждом снимке, это означает, что 1000 снимков (после каждого изменения) образует 1000 копий, а это очень много, ведь нам нужны лишь изменения, чтобы откатиться.

Как например делается Undo, с помощью паттерна Команда. Команда хранит представляет собой операцию и данные (т.е. изменения). Не полное состояние объекта, а только информацию, которая изменяет объект. Например, команда изменения ФИО хранит только ФИО, а не всё +100500 полей и целый граф объекта. Откат этого действия, вернёт только предыдущее значение ФИО. Какая-то более универсальная команда может просто хранить только изменившиеся поля. Это мне понятно. Я так делал.

В чём смысл хранить для этого снимки? Если говорить по Undo? Может вы из своей практики приведёте пример, где действительно понадобилось хранить кучу исторических копий? Не считая примера с сохранением записи и всего одной копией. Интересует, когда это может понадобится больше одной и зачем? Вы ведь не просто так реализовали свой фреймворк? Основываясь на каком-то опыте.

Буду благодарен пояснения, если я вас ещё не утомил :)
Хорошо :) расскажу подробнее про историю создания.

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

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

1) Часто на практике приходилось копировать и сравнивать объекты, писать довольно утомительную логику, где из-за копи-пэйста закрадывались малозаметные ошибки, поэтому начали возникать мысли всё это дело обобщить. А однажды появилась необходимость сравнить два огромных графа с настройками (там происходила миграция форматов в новой версии программы и нужно было удостовериться, что все опции смигрировались успешно), в результате чего был разработан своеобразный рекурсивный алгоритм сравнения на основе рефлексии.

2) В какой-то момент начали то и дело всплывать ограничения, накладываемые многими сериализатооами на графы, например, далеко не все могут обработать корректно множественные и циклические ссылки, иногда искажают информацию о типах, каким-то не достаёт гибкости. Захотелось написать такой сериализатор, который съест практически любой граф и восстановит его без искажений, идентично оригиналу, при этом позволит настраивать весь процесс…

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

Другими словами, в обычных сериализаторах логика получения состояния объекта и записи его в файл очень связная, иногда зависимая от формата сериализации, а в RF состояние — это отдельная абстракциях, с которой можно заметно эффективнее работать.

Примерно так и возник фреймворк. Если появятся ещё вопросы, то буду рад ответить! ;)

Теперь картина сложилась, спасибо :)

Насчёт сериализатора, если класс не содержит публичного конструктора без параметров, ваш фреймворк может десериализовать объект? Это самое важно, если честно, так как мы используем контракты для сериализации и десериализации, классы должны реализовать специальный конструктор (наподобие Exception(SerializationInfo..)). А конструктор без параметров для целей сериализации это нарушение инкапсуляции, чего бы мы не хотели.

Насчёт изменений. Хранить все изменения это круто, сейчас мы активно используем Event Sourcing и храним все изменения всех данных с начала времён, поэтому нам интересно хранить только изменения, а не изменённые копии, это слишком накладно и медленно. Мощный механизм сравнения графов может быть действительно в этом случае полезным, так как можно упростить задачу.

Теперь понятно, что кеш снимков это побочный эффект, который наверное как-то где-то можно применить, но пока непонятно где и как :) Всё же Undo реализуется на командах, так как это именно откат действий, а не изменённых данных по своей сути.
В текущей реализации по умолчанию библиотека использует класс Activator и конструктор без параметров для создания новых копий объектов, однако есть возможность переопределять это поведение в профилях репликации, в том числе использовать параметризированные конструкторы для конкретных типов (например, так сейчас работают Regex и StringBuilder).

Вообще стандартные Data Contract сериализаторы от Майкрасофт используют метод FormatterServices.GetUninitializedObject(Type), который создаёт непроинициализированный объект и вовсе не вызывает конструктор, но этот метод недоступен в публичном API для портабельных сборок, только в полном dot net.

Если заинтересуетесь деталями реализации, то спрашивайте, можете также отправить запрос на адрес makeman@tut.by для получения ознакомительного доступа к коду проекта. :)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории