Синхронизатор данных. Разработчику на заметку

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

    Если вы решили написать собственный синхронизатор, то скорее всего столкнётесь с рядом вопросов. В этой статье мы поделимся опытом написания такого компонента и рассмотрим требования, предъявляемые к нему. В основу этих требований легли всевозможные пожелания, полученные нами от пользователей, и реальные сценарии использования синхронизатора событий планировщика XtraScheduler. Потому в качестве примеров кода будем приводить фрагменты кода от указанного продукта.

    Для начала определим, какие объекты будут участвовать в процессе синхронизации.
    Это два набора данных (исходный и целевой/конечный) и синхронизатор, который выполняет ряд операций над этими наборами, в результате которых целевой набор должен измениться в соответствии с реализуемым сценарием.

    Объект синхронизатор

    Сценарий синхронизации будет определять, каким функционалом будет обладать ваш синхронизатор. Если синхронизация подразумевает только одну «главную» копию и будет выполняться полным замещением содержимого другой, то подойдет простой вариант вида импортер/экспортер. Если же планируется выполнять синхронизацию наборов независимых записей, то придется делать более сложную реализацию синхронизатора.

    Базовый класс

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

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

    public abstract class SynchronizerBase {
         //...
    
        protected SynchronizerBase(StorageBase storage) {...}
    
        public event ExchangeExceptionEventHandler OnException;
    
        protected internal abstract void SynchronizeCore();
        public virtual void Terminate() {...}
    
        public virtual void Synchronize() {
            Storage.BeginUpdate();
            try {
                ResetTermination();
                SynchronizeCore();
            }
            finally {
                Storage.EndUpdate();
            }
         }
    }


    Разделение обязанностей

    В зависимости от сценариев работы с наборами данных, вы можете реализовать соответствующих наследников SynchronizerBase, которые «умеют» выполнять последовательность заложенных в сценарии действий, заранее «зная», какой набор является «главным». Такие специализированные классы будут гораздо проще в использовании, чем настройка и использование единственного, но «умеющего всё и сразу».

    Таким образом, вы можете создать несколько наследников, например, ImportSynchronizer и ExportSynchronizer, реализующих основную логику синхронизации для каждого из сценариев. Эти классы могут остаться абстрактными, если в дальнейшем вы планируете реализовывать их конкретных наследников для различных наборов данных.

    Например, в XtraScheduler у нас получилась следующая схема базовых классов:



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

    Чтобы не делать объект синхронизатора слишком нагруженным, имеет смысл организовать архитектуру, отделив реализацию алгоритма синхронизации от интерфейса самого синхронизатора. Выделите подкласс в классе-синхронизаторе, не забыв при этом наладить взаимодействие между этими объектами.

    public class ImportSynchronizeManager : ImportManager {
         public ImportSynchronizeManager(AppointmentExchanger synchronizer) : base(synchronizer) {
         }
         protected internal override void PerformExchange() {
             Appointments.BeginUpdate();
             try {
                 PrepareSynchronization();
                 SynchronizeAppointments();
                 DeleteNonSynchronizedAppointments();
             }
             finally {
                Appointments.EndUpdate();
             }
        }
    }
    public class ExportSynchronizeManager : ExportManager {
        public ExportSynchronizeManager(AppointmentExchanger synchronizer) : base(synchronizer) {
    }
        protected internal override void PerformExchange() {
            PrepareSynchronization();
            SynchronizeOutlookAppointments();
            CreateNewOutlookAppointments();
        }
    }


    Операции над объектами наборов данных

    В зависимости от сценария синхронизации, наборы данных могут модифицироваться по разному. Но, как правило, все действия с объектами наборов сводятся к трём основным действиям:
    • создать новую копию объекта на целевом наборе на основании объекта из исходного набора данных;
    • обновить соответствующий объект в целевом наборе с учетом изменений объекта в исходном наборе;
    • удалить «лишние» объекты на целевом наборе, которые отсутствуют в исходном.

    Все описанные выше операции можно свести к следующей таблице:



    Описанные действия должны быть реализованы в том или ином виде в каждом из наследников вашего синхронизатора. При этом необходимо учесть важный момент, какую копию данных считать «главной». В зависимости от выбора исходный и целевой наборы могут меняться местами. Именно поэтому классы ImportSynchronizer и ExportSynchronizer будут выполнять противоположные действия над наборами данных.

    Поддержка событий

    Несомненно, при выполнении любого действия над объектами наборов пользователь захочет иметь возможность получить доступ к этим объектам «до» и «после» выполнения операции. Организуйте пару событий Synchronizing и Synchronized в базовом классе.

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

    public class AppointmentEventArgs : EventArgs {
        public AppointmentEventArgs(Appointment apt) {
        }
        public Appointment Appointment { get { ... } }
        }
    public class AppointmentCancelEventArgs : AppointmentEventArgs {
        public AppointmentCancelEventArgs(Appointment apt) : base(apt) {
        }
        public bool Cancel { get { ... } }
    }
    public class AppointmentSynchronizingEventArgs : AppointmentCancelEventArgs {
        public AppointmentSynchronizingEventArgs(Appointment apt) : base(apt) {
        }
        public SynchronizeOperation Operation { get { ... } }
    }
    public class OutlookAppointmentSynchronizingEventArgs : AppointmentSynchronizingEventArgs {
        public OutlookAppointmentSynchronizingEventArgs(Appointment apt, _AppointmentItem olApt)
                : base(apt) {
        }
        public _AppointmentItem OutlookAppointment { get { ... } }
    }


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

    iCalendarImporter importer;
    //...
    importer.OnException += new ExchangeExceptionEventHandler(importer_OnException);
    
    void importer_OnException(object sender, ExchangeExceptionEventArgs e) {
        iCalendarParseExceptionEventArgs args = e as iCalendarParseExceptionEventArgs;
        if (args != null) {
            ShowErrorMessage(String.Format("Cannot parse line {0} with text '{1}'",
            args.LineIndex, args.LineText));
        } 
        else
            ShowErrorMessage(e.OriginalException.Message);
            e.Handled = true; // prevent this exception from throwing
        }
    }


    Сохранность данных

    Поддержите для выполнения каждой операции над объектами наборов возможность отмены. Это можно сделать путём добавления свойства Cancel в аргументы события SynchronizingEventArgs.

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

    AppointmentImportSynchronizer synchronizer;
    //...
    synchronizer.AppointmentSynchronizing += new AppointmentSynchronizingEventHandler(synchronizer_AppointmentSynchronizing);
    
    void synchronizer_AppointmentSynchronizing(object sender, AppointmentSynchronizingEventArgs e) {
        // ...
        if (ShouldCancelOperation) 
            e.Cancel = true;
    }


    Покрытие всех сценариев манипуляции с объектами

    Предусмотрите ситуацию не только отменять предложенное «по умолчанию» действие над объектом, но и дать возможность выполнить другую возможную операцию.

    Например, добавьте в аргументы дополнительное свойство SynchronizeOperation { Create, Replace, Delete } и дайте пользователю возможность указать желаемое значение. Тем самым, вы получите возможность удалить объект на целевом наборе данных вместо замещения его копией из исходного набора.

    Такой подход даёт возможность более точно обрабатывать «конфликты правок» в наборах данных.

    void synchronizer_AppointmentSynchronizing(object sender, AppointmentSynchronizingEventArgs e) {
        switch (e.Operation) {
            case SynchronizeOperation.Replace:
                if (ShouldDeleteInsteadReplace.Checked)
                    e.Operation = SynchronizeOperation.Delete;
            break;
    }
    


    Элементы UI

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

    Кэшировние данных

    Иногда имеет смысл загрузить набор данных, сохранив его объекты в синхронизаторе. Это имеет смысл, когда желательно избежать повторных обращений к реальному набору данных. К тому же, в процессе выполнения синхронизации целевой набор данных может меняться, что потенциально может проявиться в проблемах, связанных с итерированием по этому набору из кода синхронизатора.

    Параметризация набора данных

    Рано или поздно пользователь захочет синхронизировать не весь набор, а только его часть, ограниченную, к примеру, временным интервалом или специфическим параметром.

    Решением будет являться написание провайдера исходных данных с возможностью пользователю «подсунуть» свой провайдер, определяющий свою логику. При этом синхронизатор должен получать данные не напрямую, а через провайдер. В этом случае придётся использовать описанное выше кэширование данных внутри синхронизатора.

    void DoFilteredImport() {
        OutlookImport importer = new OutlookImport(this.schedulerStorage1);
        TimeInterval interval = CalculateCurrentWeekInterval();
        ParameterizedCalendarProvider provider = new ParameterizedCalendarProvider(interval);
        importer.SetCalendarProvider(provider);
        importer.Import(System.IO.Stream.Null);
    }


    Завершение процесса

    Предусмотрите возможность завершить процесс синхронизации по желанию пользователя. К примеру, заведите метод Terminate в базовом классе SynchronizerBase. Такая функция может быть полезна, когда при синхронизации больших наборов данных возникло исключение или объект набора не удовлетворяет неким условиям и необходимо немедленно прервать дальнейшее выполнение. В таком случае пользователю уже не придётся дожидаться окончания процесса.

    void synchronizer_OnException(object sender, ExchangeExceptionEventArgs e) {
        // ...
        AppointmentImportSynchronizer synchronizer = (AppointmentImportSynchronizer)sender;
        synchronizer.Terminate();
        e.Handled = true;
    }


    Расширяемость

    Может случиться так, что пользователь захочет синхронизировать свойства, отличные от тех, которые синхронизирует ваш компонент. Предоставьте возможность определять это на уровне задания пользовательских свойств или объектов и предоставьте всё необходимое API для этого.

    
    void exporter_AppointmentExporting(object sender, AppointmentExportingEventArgs e) {
        iCalendarAppointmentExportingEventArgs args = e as iCalendarAppointmentExportingEventArgs;
        AddEventAttendees(args.VEvent, “test@corp.com”);
    }
    private void AddEventAttendee(VEvent ev, string address) {
        TextProperty p = new TextProperty("ATTENDEE", String.Format("mailto:{0}", address));
        p.AddParameter("CN", address);
        p.AddParameter("RSVP", "TRUE");
        ev.CustomProperties.Add(p);
    }


    Дайте возможность пользователям наследоваться от вашего синхронизатора для переопределения какой-либо функциональности. Предусмотрите это при проектировании классов и методов.

    public class MyOutlookImportSynchronizer : OutlookImportSynchronizer {
        public MyOutlookImportSynchronizer(SchedulerStorageBase storage) : base(storage) {
        }
        protected override OutlookExchangeManager CreateExchangeManager() {
            return new MyExchangeManager();
        }
    }


    Вспомогательные классы

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

    public class OutlookExchangeHelper {
        public static string[] GetOutlookCalendarNames();
        public static string[] GetOutlookCalendarPaths() ;
        public static List<OutlookCalendarFolder> GetOutlookCalendarFolders();
    }
    public class OutlookUtils {
        public static _Application GetOutlookApplication();
        public static void ReleaseOutlookObject(object obj);
    }
    public class iCalendarConvert {
        public static TimeSpan ToUtcOffset(string value);
        public static string FromUtcOffset(TimeSpan value) ;
        public static string UnescapeString(string str);
    }


    Очень надеемся, что приведённый выше «сборник советов» поможет вам наилучшим образом определить объём необходимой вам функциональности перед написанием синхронизатора и даст возможность избежать ошибок в процессе его реализации.
    Метки:
    • +19
    • 12,7k
    • 7
    DevExpress 68,19
    Компания
    Поделиться публикацией
    Похожие публикации
    Комментарии 7
    • +5
      Интересно было почитать, но в реальной жизни не вижу смысла это применять…

      Тут даже не речь об изобретении велосипеда, а скорее изобретение колеса, когда все уже давно изобрели велосипед.
      • +5
        Спасибо. Есть задача по синхронизации данных, и теперь будет памятка что надо сделать :)
        • +1
          Жаль усилий. Лучше копай в сторону Sync Framework ( msdn.microsoft.com/en-us/sync/default.aspx ). Всё уже «украдено» до нас.
          • +2
            Да возможно, если, конечно, ваш проект ограничивается только .NET и не критично распространять N-ое количество библиотек платформы синхронизации.
            Более того, если не использовать готовые службы синхронизации, то все равно придется реализовывать функционал поставщиков и хранилищ данных.
            • 0
              Или всё писать с нуля?:)
          • +2
            Все зависит от конкретной задачи. Иногда написать «с нуля» в рамках своего проекта будет «дешевле», чем поддерживать сторонний функционал, заточенный под массу разнообразных сценариев. Чем более универсальное решение, тем сложнее охватить все аспекты его применения, а простота — залог успеха =)
            • 0
              Ну, тогда, конечно стоит переписать .Net под свой проект)
              В конкретной задаче писать инфрастуктуру синхронизации избыточно, есть Sync Framework. Он потому и называется Framework, что содержит необходимую и достаточную базу для развития любого проекта синхронизации.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое