Component Coupling
In the previous article we saw the problem of cohesion between software components and we talked a lot about the concept of orthogonality. In this article instead we will begin to talk about the rules and principles that will be useful for managing the association between software components. But before starting I want to talk to you about another aspect of orthogonality that we have not seen yet. It is an aspect not directly linked to software components cohesion but which involves all of us who design and make software systems. This is the concept of orthogonality in relation to the “Project Team”.
Have you noticed how some project teams are very efficient, with all knowing perfectly well what to do and all contributing effectively to the result, while members of other teams are constantly fighting and overlapping in development activities? This is often a problem of orthogonality.?
When teams are organized with a lot of overlap, members are confused about responsibilities. Any change requires a meeting of the entire team, as each of them could be affected.
How can teams be organized into groups with well-defined responsibilities and minimal overlap? There is no simple answer. It depends both on the project and on the people we have at our disposal. A good strategy is to separate the team into sub-teams according to the tiered architecture of our products. For example we could have a sub-team that deals with the software that manages the hardware and the software for Abstraction Operating System (SOAL), we could then have the sub-team that deals with the infrastructure and therefore the sub-team dealing with the application and GUI (Graphic User Interface).
An informal measure of the orthogonality of a project team structure can be obtained. Just see how many people need to be involved in discussing each requested change. The larger the number, the less orthogonal the group is. Clearly, an orthogonal team is more efficient. That said, we also encourage sub-teams to constantly communicate with each other.
Let's finally close the topic of orthogonality and start talking about how we could minimize coupling between software component. As for the components aggregation at the base of everything there was the concept of orthogonality, also for dependencies management we can identify a concept that is the basis of every principle and every rule. This concept is that of "Shy Programming".
Revolutionaries, spies and such are often organized into small groups of people. The individuals in each group may know each other but they have no knowledge of those in the other groups. If a group is discovered, no one will be able to reveal the names of others outside of their own group. Eliminating interactions between these groups protects everyone. I believe this is a good principle to apply to coding as well. We have to organize the code into components and limit the interaction between them. If one module is then compromised and needs to be replaced, the other modules should be able to continue to run.
In this article we will see two techniques of "shy code": “Demeter's Law” and "Metadata-Driven Programming".
We could have seen these techniques in those previous articles where we talked about association between classes (.1), but since we didn't do it then we have to do it now.
Let's start with Demeter's law (.2).
We consider the traversing relationships between objects, this can quickly lead to a combinatorial explosion of dependency relationships. If n objects all know about each other, then a change to just one object can result in the other n - 1 objects needing changes.
We can see symptoms of this phenomenon mainly in two ways:
?-) "Simple" changes to one module that propagate through unrelated modules in the system.
-) Developers who are afraid to change code because they aren't sure what might be affected.
Systems with many unnecessary dependencies are very hard and expensive to maintain, and tend to be highly unstable. In order to keep the dependencies to a minimum, we could use the Law of Demeter to design our methods and functions.
Demeter's law was formulated as an attempt to provide a definition of good programming style in the object oriented paradigm (.3).
There are different formulations of Demeter's law, more or less formal and more or less restrictive; some of the formulations are intended to allow automatic verification, by an appropriate instrument, of compliance with the law itself. However, for our purposes it is sufficient to analyze the consequences of an informal formulation, but simple to understand, even if not directly automatable through a tool.
The fundamental idea of Demeter's law is to restrict the space of the methods that can be recalled within each single method.
The purpose of this restriction is to reduce dependencies between classes: when we read the code for a method of a class, it is important to know the structure of the class itself, and to know at least the interface of every other class used within method.
If a method obtains access to sub-parts of a parameter, we will need to know at least the interface of that sub-part to follow the logical behavior of the code, and so on. It is therefore evident that calling methods of other classes is a form of coupling, which if too high decreases the comprehensibility of the code.
Demeter's law (formulation "by objects") : A method M of class C should only call methods of class C, methods of immediate subparts of C, methods of arguments of M, methods of objects created internally of M, or methods of global objects.
The law is very simple to understand: within each method, we can freely call the methods of the same class, the methods of the subcomponents of the class, the methods of any objects passed as parameters, the methods of objects created locally or of global objects . In fact, as we can see, it is apparently not a very restrictive law; however, it makes the programs more understandable for the reader.
Let's see some examples of violating the law. In figure 1, a computer is described in terms of its components, each of which has a "SelfTest()" method. The "Peripheral" component instead has two methods to expose its constituents (serial / parallel sub-component). The interesting part is the "SelfTest()" method for the Computer class, which calls the same method on the sub-parts: while it is permissible to call "SelfTest()" on the elementary sub-parts (RAM, CPU), the call on the sub-parts of the "Peripheral" component does not respect Demeter's law. The solution in the case in question is trivial, providing the Peripheral class with a "SelfTest()" method, as well as the positive influence of this modification on the maintainability of the code (if, for example, Peripheral were extended to manage more than one serial or parallel).
Figure 1 : Computer’s Burn-in Test.
Let's see a second example, in the snippet in figure 2, a book consists of a front cover, a list of chapters, and a back cover; both covers and chapters have a "Print()" method, and the list of chapters allows you to access the single element of the list.
The violation is again due to calling a method for each sub-components of one of the components ("ChaptersList").?
Figure 2 : Demeter’s Law violation.
We can fix this code in order to comply Demeter's law in two ways (figure 3) : by declaring a "Print(int i)" method for the "ChapterList" class, which prints the single chapter, or by declaring for the same class a "Print()" method which prints the entire list of chapters.
The first solution (Solution 1 figure 3) is a bad solution, as it respects the law a formal level, but does not use the law to improve the code structure. The second solution (Solution 2 figure 3) corresponds to “thinking in object oriented terms”: instead of accessing the single chapter, we make the list of chapters an "autonomous object", able to print itself without an external control.
The topic of "autonomous objects" is very interesting and deserves a whole article, perhaps one of the next.
Figure 3: fix Demeter’s Law violation
So far we have observed on two concrete examples the possible violations of the law, and the possibilities of bringing the code back within the limits of the Demeter's Law, more generally we can state the following fact:?“It has been proved that any object-oriented program can be modified in order to comply with Demeter's law” (.4).
Now let's see in more detail what are the main advantages of following Demeter's Law:
-) "Coupling control" : as we have seen above, compliance with Demeter's law reduces coupling between classes.
-) "Information hiding" : as a consequence of reduced coupling, we also have an effective hiding of the structure.
-) "Information localization" : as a result of the reduced coupling, there is also a reduction in the points where it is necessary to access certain information, which is therefore used in a more localized way. This is beneficial in terms of understanding the code.
?So using The Law of Demeter will make our code more adaptable and robust, but at a cost: in practice, this means that we will be writing a large number of wrapper methods that simply forward the request on to a delegate. These wrapper methods will impose both a runtime cost and, which may be significant in some applications.
As with any technique, we must balance the pros and cons for our particular application. In database schema design it is common practice to "denormalize" the schema for a performance improvement: to violate the rules of normalization in exchange for speed. A similar tradeoff can be made here as well. In fact, by reversing the Law of Demeter and tightly coupling several modules, we may realize an important performance gain.
If we know and accept that some modules are coupled, because we are looking for more performance from our system, our design is fine.
In any case, we must see the "Demeter's Law" as a "recommendation" and not as an indispensable dogma.
Now let's see another very interesting topic always related to shy programming: "The Metadata-Driven Programming".
Let's start with the following consideration: Details mess up our original code, especially if they change frequently.
Every time we have to change the code to accommodate some change in business logic, or in the requirements, or in management's personal tastes, we run the risk of breaking the system or introducing a new bug.
So we have to try to take the details out of the code, that way we could make our code highly configurable and easily adaptable to changes.
The advice is “Configure, Don't Integrate”.
We need to use metadata to describe the configuration options for an application. But what exactly is metadata? Strictly speaking, metadata is data about data. The most common example is probably a database schema. Using the term in its broadest sense, metadata is all the data that describes the application: how it should run, what resources it should use, and so on.
Generally, the metadata is accessible and used at run time, not compile time. For example under Windows, an initialization file (using the suffix .ini) or entries in the System Registry are typical. Under Linux, the X Window System provides similar functionality using the application's default files. In all these environments it is enough to specify a key to retrieve a value.
But we want to go beyond using metadata for simple preferences. We want to configure and drive the application via metadata as much as possible. Our goal is to create highly adaptable programs.
We do this by adopting a general rule: program for the general case, and put the specifics somewhere else, outside the compiled code base.
The advice is "Put Abstractions in Code and Details in Metadata".
There are several benefits to this approach:
?-) We are forced to create a more robust design by moving the details completely out of the code.
-) We can customize the application without recompiling it.
-) Metadata can be expressed in a manner that's much closer to the problem domain than a general-purpose programming language might be.
?Now it is time to see a real case of "MetaData Driven Programming".
Let's consider a software system for cardiovascular equipment (like tradmill, bike, etc..). In particular we consider the GUI (Graphic User Interface) related to the feedback of the exercise data during the user's training. Figures 4 and 5 show a couple of typical screenshots of what the treadmill GUI looks like during exercise.
Figure 4 : Treadmill’s screen : "Immersive Mode".
Figure 5 : Treadmill’s screen : "Essential Mode".
领英推荐
The two images in figures 4 and 5 relate to two different feedback modes that the user can choose during the exercise. The data that is shown depends on the type of cardiovascular equipment and the type exercise. Obviously a bike will display different data than a treadmill and a "heart rate-driven" exercise will show different data than a "weight loss" exercise (the two images refer to a treadmill and a quick start exercise).
Considering that the same software must work on a dozen different types of cardiovascular equipment and that into the software there are over seventy different exercises for each equipment, we come to have almost a thousand different configurations.
Obviously, in this situation, thinking about managing these configurations in a hard coded way within the GUI software application would become madness. The "cyclomatic complexity" would explode and the software would become unmanageable. Any request to add a new exercise or add a new equipment would make our legs tremble.
A situation like this must be approached with a "metadata-driven design".
Figure 6 shows a snippet of component diagram relating to this aspect of the software system.
Figure 6 : Cardiovascular Equipment – Component Diagram Snippet.
We briefly describe the components involved in this fragment.
?-) "Property Provider" is the module that contains the physical property managed by the equipment such as "Speed", "Incline", "EffortLevel", "Calories", "ExerciseTime", "DistanceCovered", "DistanceTarget", etc…. It is a "Dictionary" where the key is the name of the property and the value is a structure that contains the actual value, the maximun and minimun allowed (if there are), and some other information.
-) "Training Program" contains the exercise strategy (e.g. constant heart rate strategy, weight loss strategy). The Training Program can read and write physical properties from the "Property Provider". For example, it periodically writes the physical property “Calories” with the value it calculates by applying its formulas, but it can also write some physical properties as “Speed” or as “Incline” or some other, based on the strategy it implements. Then, again as an example, he must read some properties such as the “Distance” covered by the user in order to check if the exercise target has been reached.
-) "Equipment" is an abstraction of the motor (treadmill) or brake (passive equipment like bike). The "Equipment" write on the "Property Provider" the physical properties it reads from the drive such as the distance reached and the instantaneous speed, but it also read or observe the change of some properties such as "TargetSpeed" or "TargetTorque" (which are written on the "Property Provider" from "Training Program" or from the selectors in the "GUI" modules) in order to apply them to the drive.
-) "User Interface" (GUI) handles the dialogue with the user. It is composed of some components, each one has the task of updating the portions of the screen according to the visualization mode of the exercise ("Immersive" or "Essential"). We can distinguish the following modules:
-) the "TrainingBar" module that displays data and commands at the bottom of the screen, is present in both immersive and essential modes but appears differently.
-) the "TrainingWidget" module (Figure 4) where only some essential data of the exercise are shown. It is visible only in Essential Mode.
-) the "TraininhHud" module which shows a complete feedback of data and graphics during the exercise with also the ability to change the Target values (Distance to Run, Duration of Exercise, Calories to Burn, etc).
Transition from “Essential mode” to “Full mode” takes place with a touch on the "Training Widget", transition from “Full mode” to “Essential mode” takes place by touch on the home button of the "TrainingBar".
?-) "Descriptors Provider" is the module that contains the meta description of the different zones of the screen. These descriptors are written by the "TrainingProgram" and are read by the GUI’s components that configure themselves accordingly.
?Figure 7 shows the descriptors for the "TrainingWidget", “TrainingHud” and for “TrainingBar” in immersive and essential mode.
As we can see these descriptors describe the physical property that must be present in the different zones of the GUI.
Figure 7 : Training’s GUI Descriptor.
The descriptor is expressed in XML grammar with a language very close to the domain ("TrainingView" tag for the properties that you just have to see, "TrainingControl" tag for those properties that can be modified, and so on).
If we look at the descriptor and the images in figures 5 and 6 it is immediate to identify the correspondences.
I assure you that this architecture has been successful and has allowed us to easily manage the most variable aspect in our software systems.
?And now let's talk about the first of Robert Martin's principles relating to the association between components, the Acyclic Dependencies Principle (ADP) (.1).
This principle says that "Allow no cycles in the component dependency graph".
Have you ever worked all day, made some things work and then came home, then when you went back to the office the next morning you found that your things no longer work? Why don't they work? Because someone stayed later than you and changed something you depend on! It's called the "morning after syndrome".
?The "morning after syndrome" occurs in development environments where many developers are editing the same source files. In relatively small projects with few developers, that's not too big of a problem. But as the size of the project and development team grows, the next mornings can get pretty nightmare. It is not uncommon for many hours or even days to pass before being able to create a stable version of the project. Meanwhile everyone keeps on changing and changing their code, trying to make it work with the last changes that someone else made.
?The solution to this problem is to partition the development environment into releasable components. Components become units of work that can be the responsibility of a developer or a team of developers. When developers make a component work, they release it for use by other developers. They give it a version number and move it to a directory that other teams can use and keep editing their component in their own private areas. Everyone else uses the released version.
As new versions of a component are made, other teams can decide whether to adopt the new version immediately. If they decide not to, they simply continue to use the old version. Once they decide they are ready, they start using the new version.
Therefore, none of the teams is dependent on the others. Changes made to one component do not need to have an immediate effect on other teams. Each team can decide for itself when to adapt its component to new releases of the components they use. Also, integration occurs in small increments. There is no single point where all developers have to come together and integrate everything they are doing.
This is a very simple and rational process and is widely used. However, to make it work, you need to manage the dependency structure of the components. There can be no cycles. If there are cycles in the dependency structure, the morning-after syndrome cannot be avoided.
Let's see again a real case, the component diagram fragment in Figure 8 shows the components involved in the "predictive maintenance functionality" of a cardiovascular exercise machine.
Figure 8 : Predictive Maintenance Subsystem.
As we can see from the component diagram fragment above, regardless of which component you start with, it is impossible to follow the dependency relationships and return to that component. This structure has no cycles. It is a direct acyclic graph (DAG).
Let's suppose that to send an error on the screen from the package "SensorsCard" we decide to call a GUI object. In this case the dependencies of the packages are transformed like those in figure 9.
Figure 9 : Predictive Maintenance Subsystem after change.
This creates a dependency cycle, as shown in the component diagram above. This cycle creates some immediate problems. For example, the developers working on the "AdaptiveMaintenance" component know that in order to release, they must be compatible with "SensorsCard", "NN-Classificator", but also with “GUI” and “Equipment”. This two last dependencies has been added because “SensorsCard” now depends on “GUI” and “GUI depends on “Equipment”.
But that's only part of the problem. Consider what happens when we want to test the "AdaptiveMaintenance" component. We find that we need to reference every other component in the system, including the "GUI" and "Equipment" components. This means we need to do a full build just to test "AdaptiveMaintenance". This is intolerable.
But fortunately we can always break a cycle of components and restore the dependency graph as a DAG. There are two main mechanisms :
-) Apply the Dependency-Inversion Principle (DIP). See my previous article
?-) Create a new component that both "SensorsCard"and "GUI" depend on. Move the classes that they both depend on into that new component. See Figure 10.
Figure 10 : Predictive Maintenance Subsystem- NewComponent restore DAG compliance.
We conclude our analysis of the ADP Principle with the following consideration.
Component structure cannot be designed from top to bottom without code. Rather, that structure evolves as the system grows and changes.
The component dependency structure is a map of the buildability of the application. This is why component structures cannot be fully designed at the start of the project. This is also why they are not strictly based on functional decomposition. As more and more classes accumulate in the early stages of implementation and design, there is a growing need to manage dependencies so that the project can be developed without the morning after syndrome.
well, that's all for this time, in the next article we will see other principles related to module coupling, in particular we will see the component stability problem.
I remind you my previous article:
thanks for reading my article, and I hope you have found the topic useful,
Feel free to leave any feedback.
Your feedback is very appreciated.
?Thanks again.
Stefano
?References:
1. S.Santilli,?“A systematic approach to managing dependencies between classes” https://www.dhirubhai.net/pulse/systematic-approach-managing-dependencies-between-classes-santilli/
2.?C.Pescio, “C++ Manuale di stile” 1998 Edizioni Infomedia?p 155-160
3.?K.Lieberherr, I.Holland, “Assuring Good Style for Object-Oriented Programs”, ?Northeastern University, September 1989 https://homepages.cwi.nl/~storm/teaching/reader/LieberherrHolland89.pdf
?4.?K.Lieberherr, I.Holland, A.Riel, “Object-Oriented Programming: An Objective Sense of Style”, ?Northeastern University, September 1988 https://www2.ccs.neu.edu/research/demeter/papers/law-of-demeter/oopsla88-law-of-demeter.pdf
?5.?C. Martin, “Clean Architecture - a craftsman's guide to software structure and design” Prentice-Hall (November 2018) p 105-112