Хочу познакомить тебя с молодой, но многообещающей библиотекой Replication Framework для платформы .NET (возможно, при наличии достаточного интереса к теме в дальнейшем будет также реализована Java-версия). Библиотека является портабельной (portable) и может быть использована в любом проекте под Microsoft .NET или Mono.
Назначение библиотеки — глубинное копирование любых объектов и сколь угодно сложных графов, их обобщённое сравнение, сериализация и десериализаци без искажений, трекинг мутаций и манипулирование состоянием.
Прежде всего определимся с терминологией и основными сущностями
• Снимок (Snapshot) — это мгновенный слепок состояния объекта, изолированный от источника и достаточно статичный в процессе выполнения программы, благодаря чему защищённый от случайных мутаций. Это как чертёж или эскиз по которому можно в дальнейшем воссоздать новый объект [граф] с прежним состоянием или же установить определённое состояние уже имеющемуся.
Снимки можно делать с различных ракурсов, то есть по-разному интерпретировать состояние объектов, к примеру, собирать значения абсолютно всех свойств и полей экземпляров или же только публичных, но зачастую лишь тех членов, что отмечены специальным атрибутом DataMember. То, каким образом делать снимок, зависит от ReplicationProfile [профиля репликации] и в особенности от его внутреннего списка MemberProviders [провайдеров членов].
* По умолчанию, если класс имеет атрибуты DataContract или CollectionDataContract, то на снимок транслируются лишь члены с атрибутом DataMember, в ином же случае на снимок попадают все поля и свойства класса как публичные, так и нет.
var snapshot0 = instance0.CreateSnapshot(); /* use default ReplicationProfile */
var customReplicationProfile = new ReplicationProfile
{
MemberProviders = new List<MemberProvider>
{
//new MyCustomMemberProvider(), /* you may override and customize MemberProvider class! */
new CoreMemberProviderForKeyValuePair(),
//new CoreMemberProvider(BindingFlags.Public | BindingFlags.Instance, Member.CanReadWrite),
new ContractMemberProvider(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, Member.CanReadWrite)
}
};
var snapshot1 = instance1.CreateSnapshot(customReplicationProfile );
Snapshot.DefaultReplicationProfile = customReplicationProfile;
В общих чертах снимок представляет собой json-подобную структуру данных, в которой сложные составные объекты разобраны на примитивы и преобразованы в словари, где ключом является имя члена (свойства или поля), а значением — соответствующий примитив (string, int, DateTime, etc.). Все коллекции, включая массивы, — это особый род объектов, у которых помимо обычных свойств есть ещё одно неявное для операции перечисления (foreach), а его значение является эквивалентом json-массива.
• Реконструкция (Reconstruction) — операция перевода графа объектов в исходное состояние на основе снимка и уже имеющихся закэшированных экземпляров объектов. Обычно в процессе исполнения программы объекты и состоящие из них графы видоизменяются, то есть мутируют, но иногда полезно иметь возможность вернуть [откатить] граф и входящие в него объекты к какому-то определённому состоянию зафиксированному ранее.
var cache = new Dictionary<object, int>();
var snapshot0 = graph0.CreateSnapshot(cache);
/* modify 'graph0' by any way */
var graphX = snapshot0.ReconstructGraph(cache);
/* graphX is the same reference that graph0, all items of the graph reverted to the previous state */
* Следует помнить, что кэшированные объекты удерживаются от сборки мусора, а во время реконструкции все они возвращаются в исходное состояние.
• Репликация (Replication) — операция глубинного копирования графа объектов на основе снимка, в результате которой создаётся новая копия графа изолированная от исходной.
var snapshot0 = graph0.CreateSnapshot(cache);
/* modify 'graph0' by any way */
var graph1 = snapshot0.ReplicateGraph(cache);
/* graph1 is a deep copy of the source graph0 */
Копирование бывает двух видов — поверхностное и глубинное. Пускай даны объекты А и Б, причём А содержит ссылку на Б (граф А=>Б). При поверхностном копировании объекта А будет создан объект А', который также будет ссылаться на Б, то есть в итоге получится два графа А=>Б и А'=>Б. У них будет общая часть Б, поэтому при изменении объекта Б в первом графе, автоматически его состояние будет мутировать и во втором. Объекты же А и А' останутся независимы. Но наибольший интерес представляют графы с замкнутыми (циклическими) ссылками. Пускай А ссылается на Б и Б ссылается на А (А<=>Б), при поверхностном копировании объекта А в А' получим весьма необычный граф А'=>Б<=>А, то есть в итоговый граф попал изначальный объект, который подвергался клонированию. Глубинное же копирование предполагает клонирования всех объектов, входящих в граф. Для нашего случая А<=>Б преобразуется в А'<=>Б', в итоге оба графа совершенно изолированы друг от друга. В некоторых случаях достаточно поверхностного копирования, но далеко не всегда.
• Сопоставление (Juxtaposition) — рекурсивная операция сравнения эталонного снимка объекта со снимком текущего образца.
var snapshot0 = instance0.CreateSnapshot(); /* etalon */
var snapshot1 = instance1.CreateSnapshot(); /* sample */
var juxtapositions = snapshot0.Juxtapose(snapshot1).ToList();
var differences = juxtapositions.Where(j=>j.State == Etalon.State.Different);
Сравнение объектов — обширная тема в программирования, а сопоставления — это попытка её обобщить на весь класс задач. Насколько попытка удачна, ты можешь оценить самостоятельно.
Пускай нам задан экземпляр любого объекта. В начальный момент времени мы делаем снимок его состояния, спустя какое-то время можем снимок повторить и, сопоставляя оба снимка, выявить все интересующие нас мутации, которые произошли с данным экземпляром, то есть реализовать трекинг состояния. Стоит отметить, что на практике скопировать объект не всегда возможно или просто, чтобы следить за изменениями, но всегда легко сделать его снимок.
В случае, когда у нас два и более экземпляров объектов, например: эталонный и рабочие, — они могут быть как одного типа, так и разных. Их снимки можно делать в произвольные моменты времени и сопоставлять в любых комбинациях без ограничений. Сопоставление происходит по именам членов (свойств и полей).
* Что немаловажно, результатом операции сопоставления является IEnumerable<Juxtaposition>
, что даёт возможность прервать процесс рекурсивного сопоставления в любой момент по достижении определённых условий, а не производить его полностью, это в свою очередь значимо для производительности.
Перейдём к практике и обратим внимание на ключевые моменты
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Art.Replication.Diagnostics
{
[DataContract]
public class Role
{
[DataMember] public string Name;
public string CodePhrase;
[DataMember] public DateTime LastOnline = DateTime.Now;
[DataMember] public Person Person;
}
public class Person
{
public string FirstName;
public string LastName;
public DateTime Birthday;
public List<Role> Roles = new List<Role>();
}
public static class DiagnosticsGraph
{
public static Person Create()
{
var person0 = new Person
{
FirstName = "Keanu",
LastName = "Reeves",
Birthday = new DateTime(1964, 9 ,2)
};
var roleA0 = new Role
{
Name = "Neo",
CodePhrase = "The Matrix has you...",
LastOnline = DateTime.Now,
Person = person0
};
var roleB0 = new Role
{
Name = "Thomas Anderson",
CodePhrase = "Follow the White Rabbit.",
LastOnline = DateTime.Now,
Person = person0
};
person0.Roles.Add(roleA0);
person0.Roles.Add(roleB0);
return person0;
}
}
}
using Art;
using Art.Replication;
using Art.Replication.Replicators;
using Art.Replication.MemberProviders;
using Art.Serialization;
using Art.Serialization.Converters;
public static void CreateAndSerializeSnapshot()
{
var person0 = DiagnosticsGraph.Create();
var snapshot0 = person0.CreateSnapshot();
string rawSnapsot0 = snapshot0.ToString();
Console.WriteLine(rawSnapsot0);
Console.ReadKey();
}
{
#Id: 0,
#Type: "Art.Replication.Diagnostics.Person, Art.Replication.Diagnostics, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
FirstName: "Keanu",
LastName: "Reeves",
Birthday: "1964-09-02T00:00:00.0000000+03:00"<DateTime>,
Roles: {
#Id: 1,
#Type: "System.Collections.Generic.List`1[[Art.Replication.Diagnostics.Role, Art.Replication.Diagnostics, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
#Set: [
{
#Id: 2,
#Type: "Art.Replication.Diagnostics.Role, Art.Replication.Diagnostics, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
Name: "Neo",
LastOnline: "2017-06-14T14:42:44.0000575+03:00"<DateTime>,
Person: {
#Id: 0
}
},
{
#Id: 3,
#Type: "Art.Replication.Diagnostics.Role, Art.Replication.Diagnostics, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
Name: "Thomas Anderson",
LastOnline: "2017-06-14T14:42:44.0000575+03:00"<DateTime>,
Person: {
#Id: 0
}
}
]
}
}
• Класс Person имеет атрибут DataContract, поэтому все его поля с атрибутом DataMember, кроме CodePhrase, попали на снимок.
• Каждому объекту ставится в соответствие свой идентификатор #Id: 0, если ссылка на объект встречается в графе объектов более одного раза, то вместо повторной репликации подставляется следующая конструкция.
Person: {
#Id: 0
}
Это защищает от множественной репликации одного и того же экземпляра объекта, а в случаях циклических ссылок от захода в бесконечную рекурсию и Stack Overflow Exception (примечание: далеко не все сериализаторы справляются с подобными ситуациями).
• К каждому объекту добавляется полная информация о типе по ключу #Type.
• Некоторые примитивы также содержат информацию о типе
Birthday: "1964-09-02T00:00:00.0000000+03:00"<DateTime>
. Она необходима для восстановления (десериализации) снимка без искажений.• Коллекция List<Role> сериализована как объект, но у неё есть свойство #Set, которое используется для перечисления вложенных объектов.
Однако не стоит думать, что серализация и десериализация поддерживаются библиотекой лишь в таком полном формате, возможно также использование и более классического json путём настройки профилей репликации и сохранения (могут возникнуть незначительные искажения присущие обычным сериализаторам).
public static void UseClassicalJsonSettings()
{
Snapshot.DefaultReplicationProfile.AttachId = false;
Snapshot.DefaultReplicationProfile.AttachType = false;
Snapshot.DefaultReplicationProfile.SimplifySets = true;
Snapshot.DefaultReplicationProfile.SimplifyMaps = true;
Snapshot.DefaultKeepProfile.SimplexConverter.AppendTypeInfo = false;
Snapshot.DefaultKeepProfile.SimplexConverter.Converters
.OfType<NumberConverter>().First().AppendSyffixes = false;
}
public static void CreateAndSerializeSnapshotToClassicJsonStyle()
{
UseClassicalJsonSettings();
var person0 = DiagnosticsGraph.Create();
var snapshot0 = person0.CreateSnapshot();
string rawSnapsot0 = snapshot0.ToString();
Console.WriteLine(rawSnapsot0);
var person0A = rawSnapsot0.ParseSnapshot().ReplicateGraph<Person>();
Console.WriteLine(person0A.FirstName);
Console.ReadKey();
}
{
FirstName: "Keanu",
LastName: "Reeves",
Birthday: "1964-09-02T00:00:00.0000000+03:00",
Roles: [
{
Name: "Neo",
LastOnline: "2017-06-14T18:31:20.0000205+03:00",
Person: {
#Id: 0
}
},
{
Name: "Thomas Anderson",
LastOnline: "2017-06-14T18:31:20.0000205+03:00",
Person: {
#Id: 0
}
}
]
}
О сохранении и восстановление состояния без искажений
Конечно, всегда удобно иметь возможность сохранять состояния объектов (графов) в строку или массив байт для последующего восстановления. Как правило для этого используются механизмы сериализации. Они довольно функциональны, однако при их ближайшем рассмотрении зачастую выявляется ряд жёстких ограничений, которые накладываются конкретным сериализатором на объекты, например, бывает необходимо наличие атрибутов или аннотаций, специальных методов, отсутствие замкнутых ссылок в графе, наличие конструктора без параметров или что-то ещё.
Но существуют также два неявных минуса, которые присущи многим сериализаторам. Во-первых, как упоминалось чуть ранее, при наличии в графе нескольких ссылок на один и тот же экземпляр объекта некоторые сериализаторы сохраняют его повторно, из-за чего при десериализации уже получается несколько копий одного и того же объекта (граф значительно видоизменяется). Во-вторых, в некоторых случаях может происходить потеря информации о типе объекта, что ведёт к искажённому восстановлению типов объектов при десериализации, например, long превращается в int, Guid в строку или наоборот.
public class Distorsion
{
public object[] AnyObjects =
{
Guid.NewGuid(), Guid.NewGuid().ToString(),
DateTime.Now, DateTime.Now.ToString("O"),
123, 123L,
};
}
Replication Framework использует свой собственный json-сериализатор, который сохраняет метаданные о типах объектов, поддерживает множественные и циклические ссылки в графе, благодаря чему возможна полная десериализация без искажений.
Основные сценарии использования
Репликация:
public static void Replicate()
{
var person0 = DiagnosticsGraph.Create();
var snapshot0 = person0.CreateSnapshot();
var person1 = snapshot0.ReplicateGraph<Person>();
person1.Roles[1].Name = "Agent Smith";
Console.WriteLine(person0.Roles[1].Name); // old graph value: Thomas Anderson
Console.WriteLine(person1.Roles[1].Name); // new graph value: Agent Smith
Console.ReadKey();
}
Реконструкция:
public static void Reconstract()
{
var person0 = DiagnosticsGraph.Create();
var cache = new Dictionary<object, int>();
var s = person0.CreateSnapshot(cache);
Console.WriteLine(person0.Roles[1].Name); // old graph value: Thomas Anderson
Console.WriteLine(person0.FirstName); // old graph value: Keanu
person0.Roles[1].Name = "Agent Smith";
person0.FirstName = "Zion";
person0.Roles.RemoveAt(0);
var person1 = (Person)s.ReconstructGraph(cache);
Console.WriteLine(person0.Roles[1].Name); // old graph value: Thomas Anderson
Console.WriteLine(person1.Roles[1].Name); // old graph value: Thomas Anderson
Console.WriteLine(person0.FirstName); // old graph value: Keanu
Console.WriteLine(person1.FirstName); // old graph value: Keanu
Console.ReadKey(); // result: person0 & person1 is the same one reconstructed graph
}
Сопоставление:
public static void Justapose()
{
// set this settings for less details into output
Snapshot.DefaultReplicationProfile.AttachId = false;
Snapshot.DefaultReplicationProfile.AttachType = false;
Snapshot.DefaultReplicationProfile.SimplifySets = true;
Snapshot.DefaultReplicationProfile.SimplifyMaps = true;
var person0 = DiagnosticsGraph.Create();
var person1 = DiagnosticsGraph.Create();
person0.Roles[1].Name = "Agent Smith";
person0.FirstName = "Zion";
var snapshot0 = person0.CreateSnapshot();
var snapshot1 = person1.CreateSnapshot();
var results = snapshot0.Juxtapose(snapshot1);
foreach (var result in results)
{
Console.WriteLine(result);
}
Console.ReadKey();
}
<Different> [this.FirstName] {Zion} {Keanu}
<Identical> [this.LastName] {Reeves} {Reeves}
<Identical> [this.Birthday] {9/2/1964 12:00:00 AM} {9/2/1964 12:00:00 AM}
<Identical> [this.Roles[0].Name] {Neo} {Neo}
<Identical> [this.Roles[0].LastOnline] {6/14/2017 9:34:33 PM} {6/14/2017 9:34:33 PM}
<Identical> [this.Roles[0].Person.#Id] {0} {0}
<Different> [this.Roles[1].Name] {Agent Smith} {Thomas Anderson}
<Identical> [this.Roles[1].LastOnline] {6/14/2017 9:34:33 PM} {6/14/2017 9:34:33 PM}
<Identical> [this.Roles[1].Person.#Id] {0} {0}
О производительности
На текущий момент библиотека имеет достаточно хорошую производительность, но стоит понимать, что использование обобщённого промежуточного механизма снимков накладывает дополнительные издержки как по памяти, так и по скорости выполнения в некоторых задачах. Однако на деле не всё столь однозначно, поскольку механизм снимков может давать и выигрыш в ряде сценариев.
Проигрыш:
— большее потребление памяти при сериализации объектов
— приблизительно в 2-2.5 раза меньшая скорость сериализации и последующей десериализации (зависит от настроек сериализации и рода тестов)
Выигрыш:
— копирование графа посредством снимка без применения сериализации и десериализации (не нужно конвертировать примитивы в строку или массив байт, за счёт чего достигается ускорение)
— лучшее использование памяти при частичном хранении на снимках состояния больших объектов вместо их полного копирования
* Сравнение производительности производилось с BinaryFormatter, Newtonsoft.Json, а также с DataContractJsonSerializer.
Несколько слов в заключение о Replication Framework
Разработано решение в маленькой студии творческого программирования «Мэйклофт» [Makeloft]. Сейчас проект находится на стадии предварительной версии, но и её возможности впечатляют, хотя реализован только лишь базовый функционал. На разработку было потрачено очень много сил и времени, поэтому фреймворк является бесплатным лишь для учебных и некоммерческих проектов.
На текущий момент коммерческая лицензия на использование в отдельном проекте стоит 15$ (при покупке лицензии предоставляется доступ к исходным кодам, а при необходимости более подробные консультации по техническим тонкостям, например, как реплицировать объекты с параметризированными конструкторами). Вероятно, в дальнейшем с развитием решения цена вырастет. Если планируется использование фреймворка на постоянной основе во множестве проектов, то о стоимости такой лицензии можно договориться лично.
Скачать триал-версию можно с Nuget, она функциональна до сентября 2017 года. Проект с примерами кода из статьи можно скачать отсюда. Если библиотека оставит хорошее впечатление и ты решишь использовать её в каком-либо своём решении, то отправь, пожалуйста, запрос на получение бесплатной или платной лицензии по адресу makeman@tut.by. В запросе укажи название и род проекта, в котором планируется использование библиотеки.
Большое спасибо за внимание! Смело задавай вопросы и пиши пожелания!
Комментарии (9)
Ogoun
16.06.2017 15:35+1Пускай нам задан экземпляр любого объекта. В начальный момент времени мы делаем снимок его состояния, спустя какое-то время можем снимок повторить и, сопоставляя оба снимка, выявить все интересующие нас мутации, которые произошли с данным экземпляром, то есть реализовать трекинг состояния.
Еще было бы здорово при получении разницы объектов уметь накладывать ее на объект более ранней версии. Тогда можно было бы хранить начальное состояние объекта и слепки. И получать нужную версию накладывая постепенно диффы на начальный объект. Типа Event Soursing.
И было бы здорово указывать каким образом хранить сериализованный объект, json конечно хорош, но сразу в бинарном виде было бы тоже полезно. Или в бинарном сжатом. То есть добавить абстракцию для представления сериализованного вида.Makeman
16.06.2017 16:56Насколько понял первое пожелание — это реконструкция наоборот :)
В принципе, её можно делать и сейчас, но с некоторыми ограничениями. Для стабильных графов она будет работать очевидным образом, то есть примитивные свойства объектов (типов string, int, double, DateTime, etc.) примут значения с любого снимка.
Нюансы появятся с мутирующими графами, когда объекты удаляются и вставляются в граф. Как упоминалось в публикации, для выполнения реконструкции необходимо наличие кэша Dictionary[object, int], который содержит сами объекты, подвергающиеся реконструкции, и их уникальные идентификаторы. Если вдруг объекта не оказывается в кэше, то репликатор создаёт новый экземпляр и добавляет его в кэш (id генерируется инкрементом). Так вот, если на снимках не будет конфликтов в идентификаторах, то «реконструкция наоборот» отработает успешно, в противном случае может возникнуть исключение о несоответствии типов, или же нарушиться структура графа.
Это напоминает мерж коммитов из рабочей ветки в основную, если изменения фиксировать и накатывать последовательно, то всё мержится автоматом, но если применять коммиты вразнобой, то начинаются конфликты. Приблизительно та же ситуация и со снимками в RF.
В библиотеке изначально разделена логика трансляции снимка (ReplicationProfile) и его сохранения (KeepProfile), то есть в дальнейшем без больших затруднений можно добавить сериализацию в бинарный формат, просто на данном этапе разработки было решено ограничиться json, как наиболее человекоориентированным.
Спасибо за интерес и пожелания!
Whity314
16.06.2017 15:47+1Для сравнения объектов есть отличная либа https://github.com/GregFinzer/Compare-Net-Objects
Makeman
16.06.2017 15:57Да, существует такая альтернативная библиотека для сравнения, но Replication Framework предлагает более обобщённый подход…
Например, необходимо проследить изменения в процессе жизни одного экземпляра объекта [графа]. Не всегда есть возможность сделать его точную копию в начальный момент времени, то есть у нас экземпляр только один! Чтобы воспользоваться Compare-Net-Objects необходимо на вход подать два различных экземпляра объекта, чтобы производить между ними сравнение (если же мы подадим один и тот же экземпляр, то в итоге получим равенство). Replication Framework для сравнения нуждается лишь в снимках объектов, а их можно делать в любые моменты времени без ограничений, что даёт возможность организации трекинга состояния, поскольку снимки одного и того же объекта в различные моменты времени вполне могут отличаться.
Makeman
16.06.2017 16:10Также, как упоминалось в публикации, результатом выполнения операции сопоставления в RF является IEnumerable, что позволяет использовать Linq для динамического анализа результатов сравнения и значимо для производительности.
В Compare-Net-Objects можно задать пороговое количество различий, когда сравнение прекращается, после чего все они будут занесены в коллекцию public List[Difference] Differences класса ComparisionResult. Но подход с IEnumerable более гибкий, поскольку можно учитывать не только количество различий, но и другие факторы, например, их качество (насколько различие является существенным).
wlbm_onizuka
Я бы не хотел, но придется вас огорчить.
Для хранения и передачи данных все используют DTO, поэтому хитроумные сериализаторы не нужны
Makeman
Использование DTO — это лишь один из частных случаев в разработке.
Хотелось бы акцентировать внимание на том, что помимо сериализации библиотека предназначена для более широкого круга задач.
К примеру, имеется два DTO объекта, и их нужно сравнить. Можно, конечно, написать свою логику сравнения, но проще и быстрее сделать сопоставление снимков, как показано в статье.
olegchir
то то и видно, как все пишут мапперы мапперов мапперов для DTO проходящих через всякие хибернейты и реакты
+ отдельный вопрос про нереляционные базы данных и управляемую денормализацию
Makeman
Стоит ещё отметить, что использование отдельных DTO — в некоторых случаях вынужденная мера, накладываемая ограничениями сериализаторов.
Как правило, имеется некоторая модель и её нужно передать или сохранить, но граф модели иногда может иметь довольно сложную структуру, из-за которой не получается его сразу засериализовать. Как выход, приходится выделять отдельные DTO-сущности и делать на них маппинги, генерировать большое количество однотипного кода, хотя в определённых случаях напрямую сериализовать модели вполне себе удобно и практично.