Pull to refresh

Red Architecture — красная кнопка помощи для сложных и запутанных систем — часть 2 (пример с миллиардом ячеек)

Reading time 6 min
Views 3.5K
В первой части представлена концепция Red Architecture — подход, упрощающий взаимодействие между компонентами в сложных системах, и предназначенная в первую очередь для клиентских приложений. Для полного понимания текущей статьи необходимо познакомиться с данной концепцией здесь.



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

У нас есть клиетское приложение — редактор таблиц, в нём отображается лист таблицы. Экран у пользователя настолько большой, что на нём помещается 1 000 000 000 (один миллиард) табличных ячеек. Всё усложняется тем, что наш табличный редактор подключен к облаку для возможности совместного редактирования таблицы, поэтому изменения в любой из одного миллиарда ячеек “где-то в облаке” должны быть сразу же отображены нашему пользователю.

Паттерн Red Architecture позволяет реализовать данную функцию просто и с высокой производительностью.

Прежде всего, нам нужно немного усовершенствовать класс v

Вариант, когда каждая из миллиарда ячеек проверяет каждое полученное событие на соответствие самой себе — не подходит. Миллиард вызовов функций-обработчиков + миллиард сравнений guid’ов при каждом изменении какой-то одной ячейки — это слишком даже для высокопроизводительных пользовательских устройств.

Рассмотрим решение этой проблемы.

Теперь ключи (идентифицирующие логические цепочки) в классе v будут не элементами перечисления, а строками. Для краткости и лёгкого восприятия будем писать в псевдокоде:

class v {
   // value got from this format string will look like OnCellUpdateForList_List1_Cell_D9
   public const string KeyOnCellUpdate = “OnCellUpdateForList_%s_Cell_%s”;
}

Мы объявляем ключ, который по факту является форматной строкой, для чего? Дело в том, что ячейка в любом случае должна быть некоторым образом идентифицирована на табличном листе. Мы предполагаем, что информация об обновлении ячейки, пришедшая “из облака”, содержит данные идентифицирующие ячейку (иначе как мы её найдём в листе чтобы проапдейтить?), такие как имя листа (List1) и адрес ячейки (D9). Также мы предполагаем, что каждая ячейка, отображённая на экране пользователя, тоже “знает” путь к себе, а именно те же имя листа и свой адрес (иначе как она оповестит систему о том, что изменения произошли именно в ней, а не в какой-то другой ячейке?).

Далее, нам нужно добавить ещё один аргумент в метод h(). Теперь обработчики подписываются не на все ключи которые есть в системе, а на конкретный ключ, который передаётся первым аргументом:

class v {
   // for instance, OnCellUpdateForList_List1_Cell_D9
   public const string KeyOnCellUpdate = “OnCellUpdateForList_%s_Cell_%s”;

    private var handlers = new HashMap<String, List<HandlerMethod> >();

    void h(string Key, HandlerMethod h) {
        handlers[Key] += h; 
   }

   void Add(string Key, data d) {
      for_each(handler in handlers[Key]) {
        handler(Key, d);
     }
  }
}

Для хранения обработчиков мы используем приватную коллекцию типа HashMap, содержащую пары “один ко многим” — один ключ, на который могут подписаться один и более обработчиков; а в методе Add(), “рассылающем” события по подписчикам, мы используем только функции-обработчики подписанные на данный ключ. Для контейнера с потенциальным миллиардом элементов стоит подыскать подходящую для такого объёма данных реализацию, поэтому мы используем HashMap — коллекцию, которая неявно конвертирует строковые ключи в числовые хеш значения. В случае с миллиардом элементов, HashMap позволит нам найти нужный элемент бинарным поиском за не более чем 30 операций сравнения чисел. Такая задача даже на низкопроизводительном оборудовании будет выполнена почти мгновенно.

Вот и всё! На этом изменения “инфраструктуры” Red Architecutre, а именно класса v, закончены. И теперь мы можем приступить к рассмотрению логики приёма и отображения апдейта ячейки.

Для начала нам нужно зарегистрировать ячейку для приёма апдейта. Код регистрации ячейки представлен в методе OnAppear():

class TableCellView {
    // List and Address are components of identifier for this cell in system (i.e. GUID consisting of two strings)
    private const string List;
    private const string Address;

    handler void OnEvent(string key, object data) {
        string thisCellUpdateKey = string.Format( /* format string */ v.OnCellUpdate, /* arguments for format string */ this.List, this.Address); 
    if(key == thisCellUpdateKey)
        // update content of this cell
        this.CellContent = data.Content;
   }

    // constructor
    TableCellView(string list, string address) {
        this.List = list;
        this.Address = address;
   }

    // cell appears on user’s screen - register it for receiving events
    void OnAppear() {
        string thisCellUpdateKey = string.Format( /* format string */ v.OnCellUpdate, /* arguments for  format string */ this.List, this.Address); 
        v.Add(thisCellUpdateKey, OnEvent);
   }

    // don't forget to "switch off" the cell from receiving events when cell goes out of user's screen
    void OnDisappear() {
        string thisCellUpdateKey = string.Format( /* format string */ v.OnCellUpdate, /* arguments for  format string */ this.List, this.Address);
        v.m(thisCellUpdateKey, OnEvent);
    }
}

При появлении ячейки на экране в методе OnAppear() мы “регистрируем” её для получения событий с уникальным ключом thisCellUpdateKey, который формируется в конструкторе объекта и является производным от форматной строки-ключа v.OnCellUpdate, и который позволяет позже передать данные именно этой ячейке, не вызывая функции обработчики у других ячеек.

А в методе обработчика OnEvent() мы проверяем ключ на соответствие текущей ячейке (на самом деле, в случае с OnCellUpdate эта проверка не обязательна, но поскольку в обработчике мы можем обрабатывать более одного ключа — всё же желательна) и в случае соответствия пришедшего ключа ключу текущей ячейки апдейтим отображаемые данные this.CellContent = data.Content;

Теперь рассмотрим логику получения и передачи данных ячейке

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

class SomeObjectWorkingWithSocket {

    void socket.OnData(data) {
       if(data.Action == SocketActions.UpdateCell) {
         string cellKey = string.Format( /* format string */ v.OnCellUpdate, /* arguments for format string */ data.List, data.Address);
         v.Add(cellKey, data);

         // This call for objects which process updates for any of the cells, for instance, caching data objects
         v.Add(v.OnCellUpdate, data);
       }
   }
}

Таким образом, мы всего одним вызовом (не считая логики внутри класса v, которая не относится к какой-либо конкретной логической цепочке) передали данные из места их получения — в место их использования — в одну конкретную ячейку из миллиарда. Во всей логической цепочке всего несколько строк кода, и один ключ OnCellUpdate, по которому находится весь код связанный с этой функцией.

Представим, что к нам в команду приходит новый разработчик, первая задача для него — некоторым образом анимировать апдейт ячейки, или, например, при апдейте ячейки выводить не только новые данные, но и дату/время изменения.
Чтобы понять насколько это будет тяжело для него, попробуем ответить на несколько вопросов:

  • Сколько времени займёт поиск кода, который нужно «патчить» для решения этой задачи? — Весь связанный код найдётся моментально, главное сказать разработчику, чтобы он поискал по коду v.OnCellUpdate.
  • Сколько времени такая задача займёт у нового человека в нашем случае? — Если удаётся обойтись уже существующим API для решения вопросов отображения и анимации, то 1-2 дня точно хватит.
  • Сколько шансов у нового разработчика сделать что-то не так? — Мало: код простой, разобраться в нём несложно.

Схематично цепочка передачи данных по ключу v.OnCellUpdate выглядит следующим образом



На этом можно было бы закончить, но… К нам пришла задача чтобы мы не только отображали, но и кешировали пришедшие данные. Неужели нам придётся что-то менять в уже написанном или, хуже того, всё переписывать? Нет! В Red Architecture объекты совершенно не связаны друг с другом. К нам пришла задача — добавить функцию, ровно в таком виде эта задача и будет отражена в коде — мы добавим код кеширующий данные без каких-либо изменений того, что уже написано. А именно:

class db {
    handler void OnEvent(string key, object data) {
       if(key == v.OnUpdateCell)
           // cache updates in db
           db.Cells.update("content = data.content WHERE list = data.List AND address = data.Address");
   }

   // constructor
   db() {
      v.h(v.OnUpdateCell, OnEvent);
   }

   // destructor
   ~db() {
      v.m(v.OnUdateCell, OnEvent);
   }
}

Вся логика, связанная с апдейтом ячейки, будь то отображение или кеширование, по-прежнему проста и идентифицируется в коде ключом v.OnUpdateCell.

Вы прочли вторую часть, первая часть здесь. В 3 части мы решим проблемы многопоточности.
Tags:
Hubs:
+5
Comments 6
Comments Comments 6

Articles