At the beginning of a few words about the title, why Red?
It's simple. Any phenomenon that claims a certain level of integrity requires an identifier. People refer to such an identifier in discussions and immediately it becomes clear what they mean. In the case of architecture, you should not attempt to describe the essence in the title, any architecture is a complex thing. Therefore - just Red!
Probably someone will have a question - why do we need another architecture?
The main essence of all architectural innovations in reducing links in the code, the so-called. code decoupling. And as a result, improved testability, support, simplify the introduction of new functions, etc. But so far no architecture has been recognized as “ideal”, there are still many non-applied problems over which programmers are puzzled. Suggestions for improving architectures will continue until the “ideal” architecture is found, that is, it is likely to continue forever.
The task of the Red Architecture is to reduce the complexity of implementing the application logic to a minimum, while leaving intact the possibility of application and all the advantages of other design patterns.
')
Red Architecture has one class required for its implementation, and four conventions.
The only class in Red Architecture is characterized by the following features:
- able to broadcaste (transfer to registered handler functions) data in key / value format
- able to add and remove handlers (handler functions), the list of which sends some data in the key / value format
- contains an enumeration containing all the keys that can be used to pass data to handler functions.
You can say that a similar pattern already exists and is called KVO, NSNotification and NSNotificationCenter, Delegate pattern, etc. But, first, this class has a number of key differences. Secondly, the main thing in the Red Architecture is not the content of this class at all, but how it is used in conjunction with the conventions presented here. This we consider further.
Specific example:
using System; using System.Collections.Generic; using System.Collections.ObjectModel; namespace Common { public enum k {OnMessageEdit, MessageEdit, MessageReply, Unused, MessageSendProgress, OnMessageSendProgress, OnIsTyping, IsTyping, MessageSend, JoinRoom, OnMessageReceived, OnlineStatus, OnUpdateUserOnlineStatus } public class v : ObservableCollection<KeyValuePair<k, object> > { static vi; static v sharedInstance() { if (i == null) i = new v(); return i; } public static void h(System.Collections.Specialized.NotifyCollectionChangedEventHandler handler) { i.CollectionChanged += handler; } public static void m(System.Collections.Specialized.NotifyCollectionChangedEventHandler handler) { i.CollectionChanged -= handler; } public static void Add(k key, object o) { i.OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Add, new List<KeyValuePair<k, object>>(new KeyValuePair<k, object>[] { new KeyValuePair<k, object>(key, o) }))); } protected v() { } } }
Small class inherited from ObservableCollection. From the name of the base class it follows that it is possible to follow the changes in this collection, such as adding and deleting elements. In fact, in class
v only the mechanism for notification of changes in the collection is used. No data is added to the collection or deleted - there is a
“Consume now or never” agreement in Red Architecture, which does not imply storing data at the stage of its transfer along a logical chain using the
v class. The
Add () method only sends a notification containing “added” data to all functions to subscribers. Also in the class there are functions
h () to add a new subscriber and
m () to remove it.
In addition, there is an enumeration
k in the class. And if the class itself is a key element of the entire architecture, then this enumeration is a key element of this class. Looking at the names of the elements of the enumeration (which in the best case can be supplemented with detailed comments) you can easily understand which functions are implemented in the application. Moreover, if you are interested in how each of these functions is implemented, it will be enough for you to search by project, for example “k.OnMessageEdit”. You will be surprised by the small amount of code that you find, and its content will make you want to immediately join the development, since it is very likely that you will immediately understand the logic of the function in question at each stage - from receiving the result of the query to displaying the result in the UI. At the same time, you are unlikely to be interested in what layers are in the application, and in general, how the code is physically organized. Even in the current file you are unlikely to be interested in the nearby context.
But let's return to the further consideration of the example.
For each element of listing k, there are two ways to use it:
1. The key is “added” to the class v by initiating data distribution with this key to subscribers.
2. Data sent with this key is processed by objects - subscribers.
We give a detailed explanation of paragraph 1:
By the way, in the listing of
k far from all the functions implemented in the application can be reflected. Those. I want to draw attention to one important aspect of the Red Architecture - to begin the transition to the Red Architecture does not require large-scale code reorganizations. You can start applying it right here and now. Red Architecture coexists peacefully with other design patterns thanks to a logic building method that is not related to layers or the physical organization of a program. The only necessary infrastructure element of the Red Architecture is a software object that implements an interface similar to that of the class
v we are considering.
The class
v considered here is not optimal. Let's look at what should be improved.
Probably not worth inheriting from the base class, which does not use its main purpose - the storage of elements. It would be more honest to write your own class in which there would be only the necessary functionality. However, the variant presented in the example is absolutely working, so if you do not have prejudices about the “ideality” of the code, then you can leave the current version.
The optimized version of the class v, without the base class and solving the problems of multithreading is considered in
3 parts .
In addition, instead of a simple dictionary, as a value to a key from enumeration k, one should resort to more strict typing: use structures, models created specifically for data exchange on a given key. This will save from possible typos and confusion that are possible with the string keys of a regular dictionary.
Perhaps another improvement can be made. Currently, the class
v is the only one and contains all the keys of the functions implemented using the Red Architecture. It would be more appropriate to separate the individual groups of keys into their own classes like
v . For example, the keys
MessageEdit and
OnMessageEdit have an obvious connection - the key
MessageEdit is used in the logical chain of sending the edited message, the key
OnMessageEdit is used in the processing of data arrival. For them, you can create a separate class on the template class v. Thus, only “interested” program objects can subscribe to this logical chain. In the current example being considered, notifications of “adding” data are sent to all subscribers, including those who are not interested in any of the keys.
If you don’t want to “propagate” the class
v , then you can change the
h () method by adding the key to which this object is signed with the first parameter.
Now we will give examples to item 2, namely the possible variants of data processing by functions by handlers:
void OnEvent(object sener, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Add) { var newItem = (KeyValuePair<k, object>)e.NewItems[0]; if (newItem.Key == k.OnMessageSendProgress) { var d = (Dictionary<string, object>)newItem.Value; if ((string)d["guid"] == _guid && (ChatMessage.Status)d["status"] == ChatMessage.Status.Deleted) { Device.BeginInvokeOnMainThread(() => { FindViewById<TextView>(Resource.Id.message).Text = "<deleted>"; }); } } else if (newItem.Key == k.OnMessageEdit) { var d = (Dictionary<string, object>)newItem.Value; if ((string)d["guid"] == _guid) { Device.BeginInvokeOnMainThread(() => { FindViewById<TextView>(Resource.Id.message).Text = (string)d["message"]; }); } } } }
In the given example, the
OnEvent () function is a handler located directly in the class object of a list cell (table). We are only interested in the "add" data, to filter only these events, we first add the
if condition
(e.Action == NotifyCollectionChangedAction.Add) . Next, we obtain the data that came with the event, and check that the key of this data matches the key for which the function is intended to process the data:
var newItem = (KeyValuePair<k, object>)e.NewItems[0]; if (newItem.Key == k.OnMessageSendProgress)
After passing the condition on the compliance of the key, we already know the exact format of the data that came to us. In the example under consideration, this is a dictionary:
var d = (Dictionary <string, object>) newItem.Value;
Now, all we have to do is make sure that the incoming data correspond to this cell in the table if ((string) d ["guid"] == _guid And the status of this message indicates that it was deleted:
&& (ChatMessage.Status) d ["status"] == ChatMessage.Status.Deleted) . After this, we replace the value of the text field in the current cell with the string "\ <deleted \>":
Device.BeginInvokeOnMainThread(() => { FindViewById<TextView>(Resource.Id.message).Text = "<deleted>"; });
Notice the use of Device.BeginInvokeOnMainThread () - we must remember that OnEvent () can be called not only in the main thread, but, as we all know, redrawing UI elements is possible only in the main thread. Therefore, we must call
FindViewById<TextView>(Resource.Id.message).Text = "<deleted>";
in the main thread.
The presence in the proposed example
else if (newItem.Key == k.OnMessageEdit) suggests that nothing prevents us from processing more than one key, if necessary in this particular case.
Architectural conventions:
- The result of the operation (for example, a network request or a function call) is interpreted at the place of its receipt. For example, if no data is received as a result of a query error, the raw data of the query result is returned. Instead, the result is interpreted into a structure that can be displayed by objects that initiated or process the response from this request. Subscriber objects should not know where the data came from, but they “must understand” the data format that comes to them.
For this agreement we give an example:
string s = Response.ResponseObject["success"].ToString(); success = Convert.ToBoolean(s); if (success) { var r = Response.ResponseObject["data"].ToString(); if(r.Contains("status"))
Here we see the processing of the status field from the request response. First, the status is converted to the internal type ChatMessage.Status, and only after that
v.Add () is
called where the data already converted to the internal format is sent to, which the handlers are waiting for.
The same applies to errors and null values ​​obtained, etc. If we get null, for example, as a result of an error, we do not do
v.Add (k.SomeKey, null) . Instead, we interpret null here - at the place where it was received for usable clear information, and send it with this key, for example:
v.Add (k.SomeKey, {errorCode: 10, errorMessage: “Error! Try later.”}) ;
- Data consumption is now or never. (
Consume now or never ). The received data (as a result of a function call or request) is not stored in memory for future processing. They must be “consumed” immediately upon receipt, otherwise they will simply not be processed. Data consumers are function handlers. For example, if a list cell requested data for its display, and the data came after the cell went out of scope on the user's smartphone’s screen, such data would not be processed (or, if it was a web request, they could be stored in cache, in order not to make another request in the future, when this cell reappears on the user's screen and requests data for its display).
- Static states are never duplicated, are not stored in RAM. The idea is that static states (for example, data in a local or web database) do not need to be duplicated, it is not necessary to create dynamic copies of static states in RAM. The presence of two copies of the state often causes the need for synchronization between them, and thus complicates and makes the code less resistant to errors.
Data models are mainly used to transfer data within an application; they are not used to store the state of objects or list items.
For example, a list of items on the user's screen can be represented in RAM by an array of data models that store the state of each displayed table cell. Those. state duplication occurs as the data displayed by the cell already exists in the local or web database. Instead, the list of elements should be represented by an array of unique identifiers of cell data (for example, guides of elements in the database), by which you can “pull” complete data from a local or remote database.
& nbsp Many will find this agreement controversial, since “pulling” data from the database subsystem is less productive than reading data from memory. However, from an architectural point of view, this option is definitely better for the reasons described above. Before modifying the performance of a solution, you need to be convinced of its (performance) deficiency when directly using the database subsystem. It is possible that your release or product manager will find the performance sufficient, and at best, will not notice any delays at all. Still, I want to draw attention to the fact that the Red Architecture is primarily intended to simplify the life of client application developers. And the majority of modern client hardware, such as smartphones, tablets, not to mention computers and laptops, have sufficient performance not to cause discomfort to users of “correctly” written applications.
- In the Red Architecture it is not necessary to apply the well-known approach, when the internal structure of the program is described in terms of objects or phenomena from real life. This architecture suggests the possibility of successful implementation of tasks when the internal structure of the application does not reflect objects or phenomena from the real-life domain. Instead, the application is actually divided into small parts of logic, which “do not know” about each other, and “communicate” with each other indirectly, using a special software component for this, in our case class
v . The parts of logic are chained by transmitting data to each other using the keys available in class
v in enumeration
k .
Conclusion
This architecture implements all the advantages described, for example, in “Clean Architecture” (Clean Architecture) + has additional advantages:
- Basic principles are easy to understand and apply.
- Solves the problem of dependence of one object on the life cycle of another. For example, the controller makes a request for data, but at the time of their receipt, the controller or view in which the data should be processed / reflected no longer exists. Or in Android, when you flip the screen, fragments are recreated, which for many developers is a matter that needs to be specifically worked out. In Red Architecture there are no such problems, since the life cycle of an object is important only for itself. Other system objects that send data with keys processed by this object do not know anything about it.
- Easy to find / understand where / how the features of the application are implemented by keys in the Main class
- Interfaces and / or adapters are not used for interaction between parts of the program, which saves objects from having to “follow” the state and life cycle of each other, check weak references, delegates to null, and so on.
- This architecture has no requirement for organizing code into layers or modules. You can use any pattern of physical code organization.
- Due to the absence of a tie-in for layers (business, data layer, application layer, etc.), a much deeper level of separation (duties) in the code is achieved. Other architectures, such as Clean Architecture, describe the interaction between application layers, while the Red Architecture describes the interaction between program parts “through” layers of logic or the physical organization of code.
- The parts of the logical chains of the system are small, they communicate with each other indirectly, and therefore they “do not know” about each other.
- The context for decision making by the developer is relatively small. By context is meant a set of environments - software entities and logic that need to be understood in order to make the right decision at this particular place in the code. This factor reduces the requirements for the experience and professional level of developers, facilitates their entry into the development, simplifies code maintenance, etc.
For examples, the code from the C # Xamarin application is used, but the Red Architecture pattern will help simplify any client application, regardless of platform.
The title of the article does not accidentally mention the terms of one of the theories that make up the “body” of Agile, namely from the Theory of entanglement. Red Architecture helps to more effectively implement Agile principles regardless of the framework, be it Scrum or Kanban.
The
second part describes an example of using
Red Architecture when updating cell data in a spreadsheet editor connected to the cloud for co-editing and displaying a billion cells on one sheet.