📜 ⬆️ ⬇️

Red Architecture - a red help button for complex and intricate systems - part 2 (example with a billion cells)

The first part introduces the concept of Red Architecture - an approach that simplifies the interaction between components in complex systems, and is intended primarily for client applications. For a complete understanding of the current article, you need to get acquainted with this concept here .



In the footsteps of recent comments on the first part, we consider a complete example demonstrating the use of Red Architecture for solving a non-trivial task.
')
We have a client application - a table editor, it displays a sheet of a table. The user’s screen is so large that it holds 1,000,000,000 (one billion) table cells. Everything is complicated by the fact that our spreadsheet editor is connected to the cloud so that the table can be edited together, so changes to any of one billion cells “somewhere in the cloud” should be immediately reflected to our user.

The Red Architecture pattern allows you to implement this function simply and with high performance.

First of all, we need to slightly improve the class v

The variant, when each of a billion cells checks each received event for compliance with itself, is not appropriate. A billion handler function calls + a billion guid comparisons each time a single cell changes - this is too much even for high-end user devices.

Consider solving this problem.

Now the keys (identifying logical chains) in class v are not enumeration elements, but strings. For brevity and easy perception, we will write in pseudocode :

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

We declare the key, which in fact is a format string, for what? The point is that in any case the cell should be identified in a certain way on the table sheet. We assume that cell update information coming from the cloud contains data identifying the cell (otherwise, how do we find it in the sheet to update?), Such as the name of the sheet (List1) and the address of the cell (D9). We also assume that each cell displayed on the user's screen also “knows” the path to itself, namely the same sheet name and address (otherwise, it will inform the system that the changes occurred in it, and not in what is another cell?).

Next, we need to add another argument to the h () method. Now handlers do not subscribe to all keys that are in the system, but to a specific key, which is passed by the first argument:

 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); } } } 

To store handlers, we use a private HashMap collection containing one-to-many pairs — one key to which one or more processors can subscribe; and in the Add () method that “sends” events to subscribers, we use only handler functions subscribed to this key. For a container with a potential billion items, it is worthwhile to find an implementation suitable for this amount of data, so we use HashMap, a collection that implicitly converts string keys to numeric hash values. In the case of a billion elements, HashMap will allow us to find the desired element by binary search in no more than 30 numbers comparison operations. Such a task, even on low-performance equipment, will be performed almost instantly.

That's all! On this change, the “infrastructure” of Red Architecutre, namely class v, is completed. And now we can begin to consider the logic of receiving and displaying the cell update.

First we need to register a cell for receiving the update. The cell registration code is presented in the OnAppear () method:

 class TableCellView { // List and Address are components of identifier for this cell in system (ie 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); vm(thisCellUpdateKey, OnEvent); } } 

When a cell appears on the screen in the OnAppear () method, we “register” it to receive events with the unique key thisCellUpdateKey , which is formed in the object's constructor and is derived from the format string v.OnCellUpdate , and which allows later to transfer data to this particular cell, without calling the function handlers in other cells.

And in the OnEvent () handler method, we check the key for matching the current cell (in fact, in the case of OnCellUpdate this check is not necessary, but since we can process more than one key in the handler, it is still desirable) and in the case of matching the received key to the key the current cell update the displayed data this.CellContent = data.Content;

Now consider the logic of receiving and transmitting data to a cell.

Suppose information about cell update comes to us from the “cloud” through a socket. In this case, the logic of receiving and transmitting data in the cell may look as follows:

 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); } } } 

Thus, we are just one call (not counting the logic inside the class v, which does not belong to any particular logical chain) transferred data from the place of their receipt - to the place of their use - in one specific cell out of a billion. In the entire logical chain there are only a few lines of code, and one OnCellUpdate key, which contains all the code associated with this function.

Imagine that a new developer comes to our team, the first task for him is to animate a cell update in some way, or, for example, when updating a cell, display not only new data, but also the date / time of the change.
To understand how hard it will be for him, let's try to answer a few questions:


Schematically, the data transfer chain for the v.OnCellUpdate key looks as follows



This could be finished, but ... The task came to us so that we not only displayed, but also cached the incoming data. Do we have to change something in the already written or, worse, rewrite everything? Not! In Red Architecture, objects are completely unrelated. The task came to us - to add a function, exactly in this form, this task will be reflected in the code - we will add code that caches data without any changes to what is already written. Namely:

 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() { vh(v.OnUpdateCell, OnEvent); } // destructor ~db() { vm(v.OnUdateCell, OnEvent); } } 

All the logic associated with the cell update, be it mapping or caching, is still simple and is identified in the code by the key v.OnUpdateCell.

You have read the second part, the first part here . In 3 parts we solve the problems of multithreading.

Source: https://habr.com/ru/post/334204/


All Articles