Skip to content

Breaking Dependency Cycles in Qt Applications

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).

Figure 1: Class dependencies without cycles

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.

Figure 2: Class dependencies with cycles

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 that HmiComponentA depends on XxxDatabase. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *