В первой части представлена концепция 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.
Вы прочли вторую часть, первая часть здесь.
В следующий раз в рамках Red Architecture мы решим проблемы многопоточности.
Автор: anagovitsyn