A fairly common anti-pattern found in Qt applications is to derive a class MyApplication
from QApplication
and to have it hand out pointers to a dozen or more major components of the application. Similar to Qt’s qApp
macro, these applications introduce a macro myApp
pointing to the single MyApplication
object. The global variable myApp
provides an easy way to access the major components stored in MyApplication
from everywhere. This is a recipe for disaster: for spaghetti code with dependency cycles galore. I’ll show you several methods how to eliminate these dependency cycles.
Class-Level Dependency Cycles Galore
The class MyApplication
inherits QApplication
. Similar to Qt’s qApp
macro, the single instance of MyApplication
is globally available through the macro definition
#define myApp (static_cast<MyApplication *>(QCoreApplication::instance());
We follow the siren call of the global variable myApp
and make MyApplication hand out pointers to globally used objects like settings, databases, communication gateways and the main HMI components. The declaration of MyApplication
looks like this:
class MyApplication : public QApplication
{
public:
MySettings *settings() const;
LanguageDatabase *languageDatabase() const;
XxxDatabase *xxxDatabase() const;
...
ZzzDatabase *zzzDatabase() const;
MachineGateway *machineGateway() const;
CloudGateway *cloudGateway() const;
HmiComponentA *hmiComponentA() const;
...
HmiComponentF *hmiComponentF() const;
MySettings
is a home-grown implementation of a settings class based on XML files. LanguageDatabase
is a huge map for translations into different languages. It is backed by an SQL database.
The classes XxxDatabase
, …, ZzzDatabase
store information about the work pieces designed on the operator terminal and produced by the attached machine. These classes are implemented with SQL databases. MachineGateway
handles the communication between terminal and machine. CloudGateway
enables the communication between terminal and cloud for OTA updates, remote support or remote diagnosis.
The home screen of the application shows a grid with six buttons. When users click the buttons, the application shows the corresponding components A to F – accessed through the functions hmiComponentA()
to hmiComponentF()
.
The interface of MyApplication
provides global access to objects on very different layers of the application. The functions hmiComponentA()
to hmiComponentF()
are used by by the HomeScreen
class to switch between the main HMI components. The classes HmiComponentA
to HmiComponentF
use the gateways, work-piece databases, the language database and the settings directly or indirectly. So, they belong in the top layer of your application. Except for the HomeScreen class, no other class should depend on the main HMI components.
The communication gateways definitely use MySettings
. The CloudGateway
may additionaly use the language and work-piece databases directly or indirectly. The CloudGateway
may use the MachineGateway
. The gateways must not use the main HMI components. So, the gateways are on a middle layer below the HMI components but above the databases and the settings.
The work-piece databases use MySettings
. They must not use any other classes provided by MyApplication
. The work-piece databases are one layer below the gateways.
As LanguageDatabase
uses MySettings
for finding out the current language, MySettings
is on the lowest layer and LanguageDatabase
one layer above. Note that the settings and the language database can be used by any other class.
The next figure summarises the class dependencies and the application layers. An arrow from class A to class Z says that class A uses class B directly or indirectly (with one or more classes in between).
So far, I have described the desired structure: multiple layers, no dependency cycles. MyApplication
spoils this rosy picture. Every arrow in the diagram above means a call through the global variable myApp
. Let us look at the dependency path
HomeScreen -> HmiComponentE -> CloudGateway ->
ZzzDatabase -> LanguageDatabase -> MySettings
HomeScreen
uses myApp->hmiComponentE()
somewhere in its code or in code it uses, HmiComponentE
uses myApp->cloudGateway()
somewhere, CloudGateway
uses myApp->zzzDatabase
somewhere, and so on. This turns the acyclic dependency graph of Figure 1 into a cyclic graph.
Thanks to the global variable myApp
, every class in the application can call functions from every class in the circle around MyApplication
. The result is spaghetti code with many dependency cycles.
Dependency cycles prevent us from dividing the application code into self-dependent libraries. If we created a library from the work-piece databases XxxDatabase
, YyyDatabase
, ZzzDatabase
and its auxiliary classes, the libary wouldn’t even compile. It lacks the definition of myApp and the declarations of the getter functions in MyApplication.
Adding the header file my_application.h
to the library gets rid of the compilation errors. However, the linker will pipe up with undefined symbols. We must add the source file my_application.cpp
to pacify the linker. We must then add the header and source files of all the classes returned by MyApplication
‘s getter functions. These files pull in more and more dependencies. We end up with adding most of the source and header files to the libary. That’s quite the opposite of a self-dependent library.
In the rest of this post, I’ll show you how to get rid of the dependency cycles. Simply put, I’ll transform the cyclic dependency graph of Figure 2 into the acyclic graph of Figure 1.
Replacing MySettings by QSettings
Every class may need access to the settings. Every class should see the same values for the settings. Hence, the application must store the settings only once – typically in a key-value mapping. The old implementation provides global access through the global variable myApp
.
#include "my_application.h"
#include "my_settings.h"
auto s = myApp->settings();
auto theme = s->value("theme");
s->setValue("theme", "dark");
QSettings hides the complexity of accessing the global key-value mapping behind a simple interface. It is a singleton – without burdening its clients with the downsides of singletons. We create a local QSettings
variable and can read and write the global value of any given key.
#include <QSettings>
QSettings s;
auto theme = s.value("theme");
s.setValue("theme", "dark");
The class depends only on QSettings
from the Qt5Core
library, which is on a lower layer than any class from the application. The dependency on MyApplication
is gone. And so is the dependency cycle. As a nice side effect, we get rid of the DIY implementation of MySettings
, which couldn’t be accessed safely from other threads, let alone from other processes.
Using Qt’s i18n Functionality
The current code translates texts as follows:
#include "my_application.h"
#include "language_database.h"
auto langDb = myApp->languageDatabase();
m_label->setText(langDb->getTextResources("TXT_LENGTH"));
As with the settings, we replace the DIY implementation for internationalisation (i18n) by Qt’s implementation.
#include <QObject>
m_label->setText(QObject::tr("Length"));
As a static function, any class can use the function QObject::tr()
by including QObject
. The dependencies on MyApplication
and LanguageDatabases
are gone.
Passing Used Classes to Users’ Constructors
The work-piece databases XxxDatabase
, YyyDatabase
and ZzzDatabase
exist once in the application. They are effectively singletons, which are accessed through the singleton myApp
. We could build a QSettings
-like singleton, which looks like an ordinary value class. However, I want to introduce another solution, which demands less effort.
In the old implementation, HmiComponentA
calls functions of the XxxDatabase
by calling myApp->xxxDatabase()->someFunc(...)
. Hence, HmiComponentA
depends both on MyApplication
and on XxxDatabase
. We eliminate the dependeny on MyApplication
by passing a pointer to XxxDatabase
to HmiComponentA
‘s constructor. This happens best in the member initialisation list of MyApplication
‘s constructor.
// In my_application.h
XxxDatabase m_xxxDatabase;
HmiComponentA m_hmiComponentA;
// In my_application.cpp
MyApplication::MyApplication(int &argc, char **argv)
: QApplication{argc, argv}
, m_xxxDatabase{"myXxxDb"}
, m_hmiComponentA{&m_xxxDatabase}
...
The constructor of HmiComponentA
stores the XxxDatabase
pointer in a member variable so that member functions can use this pointer.
// In hmi_component_a.h
class XxxDatabase;
XxxDatabase *m_xxxDatabase;
// In hmi_component_a.cpp
#include "xxx_database.h"
HmiComponentA::HmiComponentA(XxxDatabase *xxxDb, QWidget *parent)
: QWidget{parent}
, m_xxxDatabase{xxxDb}
...
In the old implementation, a member function would query the database as follows:
// In hmi_component_a.cpp
#include "my_application.h"
#include "xxx_database.h"
void HmiComponentA::someFunc(const QString &name)
{
auto xxx = myApp->xxxDatabase()->findXxxThingByName(name);
...
The new implementation gets rid of the dependency on MyApplication
and uses the stored m_xxxDatabase
pointer.
// In hmi_component_a.cpp
#include "xxx_database.h"
void HmiComponentA::someFunc(const QString &name)
{
auto xxx = m_xxxDatabase->findXxxThingByName(name);
...
If the XxxDatabase
pointer is not used by HmiComponentA
itself but by a class Other
used by HmiComponentA
, then HmiComponentA
passes the XxxDatabase
pointer to Other
‘s constructor. We may have to pass down the XxxDatabase
pointer through a couple of constructors. A forward declaration of XxxDatabase
is enough. We should strive to move the construction of XxxDatabase
as close to its use as possible.
Passing the used objects to the constructors of the using objects has two very useful side effects:
- The dependency relationship between using class and used class is made obvious. For example, the declaration
HmiComponentA(XxxDatabase *xxxDb, ...)
clearly states thatHmiComponentA
depends onXxxDatabase
. This dependency is hidden when we use global variables or singletons. - This solution makes unit testing of the using class much easier. We can pass a double (e.g., dummy, fake, mock) of the used class to the constructor in our unit tests. For example, we could pass an in-memory database into the constructor as a double for the SQL-backed database. The unit test doesn’t have to pull in all the dependencies of
XxxDatabase
.
If we pass the same classes to constructors over and over again, we can group them together in a class and pass the group class. For example, the three work-piece databases refer to each other, are used together and are on the same abstraction layer. This is a clear sign that we can group them together.
class WorkPieceGroup
{
private:
XxxDatabase m_xxxDb{"myXxxDb"};
YyyDatabase m_yyyDb{"myYyyDb"};
ZzzDatabase m_zzzDb{"myZzzDb"};
public:
XxxDatabase *xxxDatabase() const { return &m_xxxDb; };
YyyDatabase *yyyDatabase() const { return &m_yyyDb; };
ZzzDatabase *zzzDatabase() const { return &m_zzzDb; };
};
With this grouping, we can simplify the constructor of HmiComponentA
from
HmiComponentA(XxxDatabase *xxxDb, YyyDatabase *yyyDb,
ZzzDatabase *zzzDb, QWidget parent = nullptr);
to
HmiComponentA(WorkPieceGroup *wpg, QWidget parent = nullptr);
The solution for the work-piece databases also works for the gateways. The same MachineGateway
object is used by, say, the HMI components B and C to tell the CNC machine how to process a work piece and to display the processing status of the machine and work piece. Component B shows the status in a table, whereas component C uses 2D graphics. As with the work-piece database, MyApplication
creates the MachineGateway
object and passes it to the constructors of HmiComponentB
and HmiComponentC
.
CloudGateway
forwards data from the MachineGateway
to the cloud and data from the cloud to the MachineGateway
. CloudGateway
doesn’t display any information in the application’s HMI. Its information is displayed in applications on remote PC, phones or tablets. Hence, we only pass a MachineGateway
pointer to CloudGateway
‘s constructor.
Connecting Components with Signals and Slots
By now we have removed from MyApplication
the getters for the settings, language database, work-piece databases and gateways – and the cyclic dependencies caused by them. This leaves us with the getters for the six main HMI components.
We can think of the main HMI components as independent applications, which users can start from the HomeScreen
. Application A cannot simply call a function in application B, because A and B run in different processes. Instead, A sends a message using inter-process communication (IPC) like DBUS or Qt remote objects. B receives the message, processes it and possibly sends a response message back to A.
Qt has adapters for DBUS and remote objects that make sending a message look like emitting a signal and receiving a message like calling a slot. IPC messages are mimicked by signals and slots in Qt applications. If we can use enhanced versions of signals and slots for inter-process communication, we can use the standard version of signals and slots for intra-process communication, that is, for communication between the main HMI components. Let us look at an example.
In HMI component E, users configure which tools are installed on the CNC machine. When users change a tool, the application must notify HMI components B and C about it. Both B and C control the processing of the work piece and show the machine status. The old implementation solves this with direct function calls.
// In hmi_component_e.cpp
#include "my_application.h"
#include "hmi_component_b.h"
#include "hmi_component_c.h"
void changeTool(const ToolK &tool)
{
myApp->hmiComponentB->setTool(tool);
myApp->hmiComponentC->setTool(tool);
}
The new implementation emits or publishes one signal, to which interested components like B and C subscribe. Qt signals and slots implement the Publish-Subscribe pattern.
// In hmi_component_e.cpp
void changeTool(const ToolK &tool)
{
emit toolChanged(tool);
}
Components B and C subscribe to the signal in the MyApplication
constructor with signal-slot connections. We connect the toolChanged
signal from HmiComponentE
with the setTool
slots from HmiComponentB
and HmiComponentC
.
// In my_application.h
HmiComponentB m_hmiComponentB;
HmiComponentC m_hmiComponentC;
HmiComponentE m_hmiComponentE;
// In my_application.cpp
MyApplication::MyApplication(int &argc, char **argv)
...
{
connect(&m_hmiComponentE, &HmiComponentE::toolChanged,
&m_hmiComponentB, &HmiComponentB::setTool);
connect(&m_hmiComponentE, &HmiComponentE::toolChanged,
&m_hmiComponentC, &HmiComponentC::setTool);
...
}
Note that HmiComponentE
does not depend on MyApplication
, HmiComponentB
and HmiComponentC
any more. Using signals and slots breaks the remaining dependency cycles and eliminates the tight coupling between the HMI components B, C and E. In addition to the Publish-Subscribe pattern, signal-slot connections follow the Mediator pattern.
The Mediator pattern has two goals. First, it eliminates tight coupling between components. In our case, it removes the need for an HMI component to include any other HMI component. Second, it enables us to change the communication between objects without changing the objects themselves. We add or remove signal-slot connections between objects.
In all classes of our application, we must replace the calls to functions of any of the main HMI component by signal-slot connections. The offending call may not happen in the main HMI component itselft, but in a child, grandchild or any descendant of the main HMI component. In this case, we pass the signal from the descendant up to the top-level component. The top-level component sends the signal to its peer (another main HMI component). The peer may pass the slot down to the right descendant.
Introducing signal-slot connections between the main HMI components is the last piece in breaking all the dependency cycles caused by the global variable myApp
. We are ready to apply the final touches.
Applying the Final Touches
We cannot remove the getter functions for HMI components from MyApplication
yet. They are needed by HomeScreen
to switch between the HMI components. We get rid of these getter functions by moving the creation of all the “global” objects from the MyApplication
constructor to the HomeScreen
constructor. HomeScreen
seems to be the natural place, because it allows users to switch between the main HMI components.
Here is a partial implementation of the HomeScreen
constructor.
// In home_screen.h
#include "work_piece_group.h"
#include "machine_gateway.h"
#include "cloud_gateway.h"
#include "hmi_component_b.h"
#include "hmi_component_c.h"
#include "hmi_component_e.h"
...
WorkPieceGroup m_workPieceGroup; // Holds Xxx, Yyy and Zzz database
MachineGateway m_machineGateway;
CloudGateway m_cloudGateway;
HmiComponentB m_hmiComponentB;
HmiComponentC m_hmiComponentC;
HmiComponentE m_hmiComponentE;
...
// In home_screen.cpp
HomeScreen::HomeScreen(QWidget *parent)
: QWidget(parent)
, m_cloudGateway(&m_machineGateway)
, m_hmiComponentB(&m_workPieceGroup, &m_machineGateway)
, m_hmiComponentC(&m_workPieceGroup, &m_machineGateway)
...
{
// Connections for signals published by HmiComponentA
...
// Connections for signals published by HmiComponentE
connect(&m_hmiComponentE, &HmiComponentE::toolChanged,
&m_hmiComponentB, &HmiComponentB::setTool);
connect(&m_hmiComponentE, &HmiComponentE::toolChanged,
&m_hmiComponentC, &HmiComponentC::setTool);
...
}
main.cpp
is the only file including home_screen.h
. The main() function creates an instance of HomeScreen
and shows it.
We are done! We have removed all the dependency cycles from the application. Now we are free to split up the application code into libraries. The work-piece databases could go into a library, the gateways into another library and each main HMI component into a library of its own.
Great article! As every post you have published!! Thank you very much!