Replication Framework • глубинное копирование и обобщённое сравнение связных графов объектов

в 16:08, , рубрики: .net, C#, cloning, coping, deep copy, deserialization, deserializer, java, json, Makeloft, Replication Framework, serialization, serializer, Программирование, Промышленное программирование, метки: , , , ,

Приветствую, читатель!

Хочу познакомить тебя с молодой, но многообещающей библиотекой Replication Framework для платформы .NET (возможно, при наличии достаточного интереса к теме в дальнейшем будет также реализована Java-версия). Библиотека является портабельной (portable) и может быть использована в любом проекте под Microsoft .NET или Mono.

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

image

* Примечание для модераторов. В основе своей публикация несёт обучающий характер, а не рекламный, пожалуйста, не убирайте её из соответствующих хабов по разработке. Если же какие-то участки текста, на ваш взгляд, несут чёткий оттенок пиара, то укажите на них в личных сообщения автору, они будут откорректированы. Спасибо за понинимание!

Прежде всего определимся с терминологией и основными сущностями

Снимок (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 путём настройки профилей репликации и сохранения (могут возникнуть незначительные искажения присущие обычным сериализаторам).

Сериализация объекта в классический 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();
        }

Классический json

{
  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. В запросе укажи название и род проекта, в котором планируется использование библиотеки.

Большое спасибо за внимание! Смело задавай вопросы и пиши пожелания!

Автор: Makeman

Источник

Поделиться

* - обязательные к заполнению поля