Replacing direct dependencies with messaging
Sometimes a module just needs to notify others that some events/changes have occurred in it, and it doesn't matter what happens to this information later.
In this case, the modules do not need to “know about each other” at all, that is, contain direct links and interact directly, but it is enough just to exchange messages (messages) or events (events).
Sometimes it seems that module communication via messaging is much weaker than direct dependency. Indeed, because the methods are not called, there is no information about the classes. But this is nothing more than an illusion.
Instead of method names, logic begins to be tied to message types, their parameters, and transmitted data. The connectivity of such modules is smeared.
It used to be like: we call methods - there is connectivity, we don’t call methods - there is no connectivity. Now imagine that module A began to send slightly different data in its messages. And at the same time, all modules dependent on these messages will not work correctly.
Suppose, earlier, when adding a new user, the authorization module sent the message USER_ADDED, and after the update, it began to send this message when trying to register and additionally indicate successful registration or not in the parameters.
Therefore, it is very important to implement the message mechanism very competently. There are various templates for this.
Observer. It is used in the case of one-to-many dependency, when many modules depend on the state of one - the main one. It uses the mailing mechanism, which means that the main module simply sends the same messages to all its subscribers, and the modules interested in this information implement the “subscriber” interface and subscribe to the mailing list.
This approach is widely used in systems with a user interface, allowing the core of the application (model) to remain independent while informing its associated interfaces that something has changed and needs to be updated.
Here the message format is standardized at the operating system level, whose developers must take care of backward compatibility and good documentation.
The organization of interaction through the distribution of messages has an additional “bonus” - the optional existence of “subscribers” to “published” (that is, sent out) messages. A well-designed system like this allows modules to be added/removed at any time.
Messaging bus
You can organize the exchange of messages and use the Mediator pattern for this in a different way .
It is used when there is a many-to-many dependency between modules. The mediator acts as an intermediary in communication between modules, acting as a communication center and eliminating the need for modules to explicitly refer to each other.
As a result, the interaction of modules with each other (“all with all”) is replaced by the interaction of modules only with an intermediary (“one with all”). The mediator is said to encapsulate the interaction between multiple modules.
This is the so-called smart intermediary . It is there that developers most often begin to add their crutches, which influence the behavior of individual modules by turning on / off receiving certain messages.
A typical real-life example is airport traffic control. All messages from aircraft go to the controller's control tower instead of being sent directly between aircraft. And the controller already makes decisions about which planes can take off or land, and, in turn, send messages to the planes.
Important! Modules can send each other not only simple messages, but also command objects. Such interaction is described by the Command template . The bottom line is to encapsulate a request to perform a specific action as a separate object.
In fact, this object contains a single execute() method , which then allows you to pass this action to other modules for execution as a parameter and generally perform any operations with the command object that can be performed on ordinary objects.
Law of Demeter
The Law of Demeter forbids the use of implicit dependencies: "Object A must not be able to directly access object C if object A has access to object B and object B has access to object C."
This means that all dependencies in the code must be “explicit” - classes / modules can only use “their dependencies” in their work and should not climb through them to others. A good example is a three-tier architecture. The interface layer should work with the logic layer, but should not interact directly with the database layer.
Briefly, this principle is also formulated in this way: "Interact only with immediate friends, and not with friends of friends." This achieves less coherence of the code, as well as greater visibility and transparency of its design.
The Law of Demeter implements the already mentioned “principle of minimum knowledge”, which is the basis of loose coupling and consists in the fact that an object / module should know as few details as possible about the structure and properties of other objects / modules and anything in general, including its own components .
An analogy from life: if you want the dog to run, it is stupid to command its paws, it is better to give the command to the dog, and she will deal with her paws herself.
Composition instead of inheritance
This is a very large and interesting topic and it deserves at least a separate lecture. A lot of copies were broken on this topic on the Internet until a consensus was reached - we use inheritance to a minimum, composition - to the maximum.
The point is that inheritance actually provides the strongest connection between classes, so it should be avoided. This topic is well covered in Herb Sutter's article " Prefer Composition Over Inheritance ".
When you start learning design patterns, you will come across a whole bunch of patterns that govern the creation of an object or its internal structure. By the way, I can advise in this context to pay attention to the Delegate / Delegate pattern and the Component pattern, which came from games .
We'll talk more about patterns a little later.
GO TO FULL VERSION