I want to share my experience in designing the architecture of the program. Architecture is a very important thing for projects with a complex internal structure and numerous internal connections. An error in the choice of a solution method can strongly affect on the further development of the project, leading to an avalanche-like increase in complexity and errors. It is even possible that it is easier to write everything from scratch than to unravel the tangle of interconnections.

For example, I’ll take a relatively simple single-user application architecture. For example, a communicator is a program for network communication that supports many different protocols, is able to change the appearance and must be open to adding new features and further development.
Where to begin
For starters, I recommend to follow the rules of code design. This is also very important, as neat code is easier to read, many design errors are easier to find and fix. A good example can be found
here . I recall the main thing:
1. "Talking" names of variables, functions, classes and their methods. To one name was clear what it is. An exception can be made for purely local variables - cycle counters, intermediate values.
')
2. The content of the blocks (conditions, cycles) with indentation, the beginning and end of the block should be at the same level.
3. The comment explains the code, but does not reflect the mood of the encoder.
In my examples, I will use a pseudo-language similar to JavaScript without reference to the actual syntax, because
its purpose is not to give ready-made code, but to show an idea . Code highlighted using
Source Code HighlighterFirst try
Let's try to do it in a simple way. For example, the program has:
* Main window
* Settings window

It would seem that everything is simple. In the module of the main window, we will write the code for connecting to the server, the function of sending messages to the server and parsing messages from the server. Settings take from the settings window. And in the settings window we make a record-read settings from the file. And now we take a step further.
* Messaging window
* Contact list window

At the same time, contact list windows can be nested both in the main window and in each other. This is also solved, you can link it all together. But already at the slightest change in one element, one has to keep track of all its interrelations. And the more elements of the program there are, the more confusing the interconnections will be. And if you add support for plug-ins, different protocols and languages, then the whole project will be a cancer.
How to be
Let's try to reduce the number of relationships and streamline them.
We build relationships not directly, but in the form of a tree. At the root of the tree is the core of the program, all other relationships will pass through it. Direct relationships between different tree branches should be avoided. If we need a new functionality, we add its definition to the kernel. The implementation will be in a separate branch.

Separate user interface (UI), logic and data. The UI is a data view, not a repository. Logic binds UI and data. For example, in the settings window there is no data, all settings are stored in the config, and the settings window only displays them.
It is desirable to avoid direct calling the code (calling the kernel function that calls the module function), and using messages (creating a message to be picked up by the addressee) or a queue of commands (putting the command in the queue, the kernel will process it). This will make the program multi-tasking and more stable with internal errors and failures.

We observe the openness and backward compatibility of interfaces as far as possible. If we have a certain global function with a fixed set of parameters, then changing its definition will entail reworking all the modules that use it. Inside the program it is not critical, in modern IDE there are good tools for this. And if we change the interface of the plug-in, then all the old practices will stop working.
Ways to preserve the compatibility of functions set. You can simply create new functions that are similar to the old ones, and leave the old ones. For example, ExecCommand (CmdText) and ExecCommand2 (Cmd, Params). You can pass only two parameters - a pointer to the structure with parameters and a version of the structure. You can use open structures. I personally stopped at the option of passing parameters as a command line.
How to do it
Generally available data and objects are defined in the kernel. For example, a config in which program settings are stored and which should be accessible from anywhere. At the same time, the config should be made open so that we can freely add new options. I use the config in the form of an associative array (name-value), where the names and values ​​are in the form of strings. This gives a 100% guarantee of compatibility and security at the cost of some loss of performance (by searching for a value by name and converting a string of value to the desired type). You also need to use SQL through the kernel, so that the components do not have a binding to a specific implementation of the SQL server and you can change the SQL server without redoing the entire system. Alternatively, you can get objects through the kernel to work with arbitrary tables and queries.
//
function SomeIPClient.Connect();
{
// ,
// ,
//
UserName = Core.Config[ 'UserName' ];
Password = Core.Config[ 'Password' ];
IPConnection.Open(Host, Port, UserName, Password);
}
We implement the MVC pattern (model-representation-behavior). For example, there is a text entry field and a "Send" button. Pressing the button should not launch the sending code immediately - sending may take a considerable amount of time and the program will “hang” all this time. And if the sending code takes data from the input field, it may turn out that the user has managed to change this data or close the window altogether. Therefore, all actions performed by the user need to be reduced to messages or commands. To do this, you can provide in the kernel a function that will take the name and parameters of the command, the sender and the recipient. Or a set of functions for various elementary actions - calling the settings window, minimizing or closing the program, playing sounds, etc. ... In simple programs, you can do without messages, only functions. But still, all publicly accessible functions must be in the kernel, so that there are no direct interconnections between the various components of the program.
//
function OnCloseWindowClick();
{
Core.CloseWindow(CurrentWindowID);
}
function ExecCmd(CommandText);
{
if (CommandText = 'WINDOW_CLOSE' ) CloseWindow();
}
//
function Core.CloseWindow(WindowID);
{
Core.CmdQueue.Add( 'WINDOW_CLOSE ' + WindowID);
}
In this example, when we press the close button of the window, the window is not immediately closed, but a command is sent to the kernel with the window identifier specified. On this, by the way, the execution of the chain of code ends, the command itself can be processed by another thread within the program. At the same time, the kernel can send a message to all (or only interested) modules that such a window is closing, to perform some general actions related to closing the window. And only then informs the window to close it. Full flexibility and control, at the cost of some overhead projector when processing commands.
Autonomous program components, for example, a communication module with a server and a communication protocol parser can interact with each other not through the kernel, but directly. But you need to dock them to the core consistently. That is, a certain average interface of the module is connected to the core, which does not depend on the features of its internal implementation. This may be, for example, a heir to a global class that accepts kernel messages and executes commands. And already behind this class there may be at least a whole separate program of any degree of complexity. This achieves high scalability and flexibility of the entire system - you can add new functionality, and you don’t have to rewrite the half-programs, it’s enough to ensure compatibility of the new functionality with the kernel.
//
IPClient = class () // "" ,
{
integer ID;
string Host;
string Port;
virtual function Connect();
virtual function Disconnect();
virtual function SendData(Data);
}
//
SomeIPClient = class (Core.CIPClient)
{
function Connect();
function Disconnect();
function SendData(Data);
function ParseServerData(Data);
}
In this example, the component defines a successor to the global class Core.IPClient, in which communication with the server is implemented. And there may be many such components, the kernel will distinguish them by their ID. Component objects are stored in dispatchers (managers) - lists of objects with methods for accessing and managing stored components. Thus, we can add all new and new components without a headache - the dispatcher will take care of working with them. Component structure is a separate big topic.
//
//
//
// ,
// . .
function StartComponent()
{
NewComponent = New CSomeComponent;
Core.ComponentManager.Add(NewComponent); //
NewWindow = New CSomeComponentWindow;
Core.WindowManager.Add(NewWindow); //
}
//
//
// .
// , 'WINDOW_CLOSE 15';
function Core.WindowManager.ProccesCmd(CmdText)
{
Cmd = GetParam(CmdText, 0); // 'WINDOW_CLOSE'
WindowID = GetParam(CmdText, 1); // (ID ) '15'
Window = Self.GetWindowByID(WindowID); // ID
Window.ExecCmd(Cmd); //
}
As a result, we have a kernel that collects the objects of the components and sends messages and commands between them. In addition, each component can work in a separate thread, and if an error occurs in the component, then the entire program will not fall down from this - the component can be closed and restarted. You can even connect-disconnect and debug components on the go. Of course, such an architecture also has disadvantages. These are mainly unproductive losses of machine time for processing internal commands. Therefore, to speed up some operations, you can use either a separate priority queue of commands, or a direct function call. Or give the module a direct link to the object of another module so that it can work with it directly. This will complicate the interconnection scheme and decrease reliability, but will increase the speed.