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

По следам свежих комментариев к первой части рассмотрим законченный пример, демонстрирующий применение Red Architecture для решения нетривиальной задачи.
У нас есть клиетское приложение — редактор таблиц, в нём отображается лист таблицы. Экран у пользователя настолько большой, что на нём помещается 1 000 000 000 (один миллиард) табличных ячеек. Всё усложняется тем, что наш табличный редактор подключен к облаку для возможности совместного редактирования таблицы, поэтому изменения в любой из одного миллиарда ячеек “где-то в облаке” должны быть сразу же отображены нашему пользователю.
Паттерн Red Architecture позволяет реализовать данную функцию просто и с высокой производительностью.
Прежде всего, нам нужно немного усовершенствовать класс v
Вариант, когда каждая из миллиарда ячеек проверяет каждое полученное событие на соответствие самой себе — не подходит. Миллиард вызовов функций-обработчиков + миллиард сравнений guid’ов при каждом изменении какой-то одной ячейки — это слишком даже для высокопроизводительных пользовательских устройств.
Рассмотрим решение этой проблемы.
Теперь ключи (идентифицирующие логические цепочки) в классе v будут не элементами перечисления, а строками. Для краткости и лёгкого восприятия будем писать в псевдокоде:
Мы объявляем ключ, который по факту является форматной строкой, для чего? Дело в том, что ячейка в любом случае должна быть некоторым образом идентифицирована на табличном листе. Мы предполагаем, что информация об обновлении ячейки, пришедшая “из облака”, содержит данные идентифицирующие ячейку (иначе как мы её найдём в листе чтобы проапдейтить?), такие как имя листа (List1) и адрес ячейки (D9). Также мы предполагаем, что каждая ячейка, отображённая на экране пользователя, тоже “знает” путь к себе, а именно те же имя листа и свой адрес (иначе как она оповестит систему о том, что изменения произошли именно в ней, а не в какой-то другой ячейке?).
Далее, нам нужно добавить ещё один аргумент в метод h(). Теперь обработчики подписываются не на все ключи которые есть в системе, а на конкретный ключ, который передаётся первым аргументом:
Для хранения обработчиков мы используем приватную коллекцию типа HashMap, содержащую пары “один ко многим” — один ключ, на который могут подписаться один и более обработчиков; а в методе Add(), “рассылающем” события по подписчикам, мы используем только функции-обработчики подписанные на данный ключ. Для контейнера с потенциальным миллиардом элементов стоит подыскать подходящую для такого объёма данных реализацию, поэтому мы используем HashMap — коллекцию, которая неявно конвертирует строковые ключи в числовые хеш значения. В случае с миллиардом элементов, HashMap позволит нам найти нужный элемент бинарным поиском за не более чем 30 операций сравнения чисел. Такая задача даже на низкопроизводительном оборудовании будет выполнена почти мгновенно.
Вот и всё! На этом изменения “инфраструктуры” Red Architecutre, а именно класса v, закончены. И теперь мы можем приступить к рассмотрению логики приёма и отображения апдейта ячейки.
Для начала нам нужно зарегистрировать ячейку для приёма апдейта. Код регистрации ячейки представлен в методе OnAppear():
При появлении ячейки на экране в методе OnAppear() мы “регистрируем” её для получения событий с уникальным ключом thisCellUpdateKey, который формируется в конструкторе объекта и является производным от форматной строки-ключа v.OnCellUpdate, и который позволяет позже передать данные именно этой ячейке, не вызывая функции обработчики у других ячеек.
А в методе обработчика OnEvent() мы проверяем ключ на соответствие текущей ячейке (на самом деле, в случае с OnCellUpdate эта проверка не обязательна, но поскольку в обработчике мы можем обрабатывать более одного ключа — всё же желательна) и в случае соответствия пришедшего ключа ключу текущей ячейки апдейтим отображаемые данные this.CellContent = data.Content;
Теперь рассмотрим логику получения и передачи данных ячейке
Допустим, информация об апдейте ячейки к нам приходит из “облака” через сокет. В этом случае логика приёма и передачи данных в ячейку может выглядеть следующим образом:
Таким образом, мы всего одним вызовом (не считая логики внутри класса v, которая не относится к какой-либо конкретной логической цепочке) передали данные из места их получения — в место их использования — в одну конкретную ячейку из миллиарда. Во всей логической цепочке всего несколько строк кода, и один ключ OnCellUpdate, по которому находится весь код связанный с этой функцией.
Представим, что к нам в команду приходит новый разработчик, первая задача для него — некоторым образом анимировать апдейт ячейки, или, например, при апдейте ячейки выводить не только новые данные, но и дату/время изменения.
Чтобы понять насколько это будет тяжело для него, попробуем ответить на несколько вопросов:
Схематично цепочка передачи данных по ключу v.OnCellUpdate выглядит следующим образом

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

По следам свежих комментариев к первой части рассмотрим законченный пример, демонстрирующий применение 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 части мы решим проблемы многопоточности.