Skip to main content

Raymii.org Raymii.org Logo

Quis custodiet ipsos custodes?
Home | About | All pages | Cluster Status | RSS Feed

Johnnie 'QObject' Walker, replace a service locator pattern while you're at it

Published: 14-01-2023 04:30 | Author: Remy van Elst | Text only version of this article


❗ This post is over one year old. It may no longer be up to date. Opinions may have changed.

Table of Contents


I've seen many C++ code bases where there was the concept of a service locator. An global static object that anyone can query to get a class. This is handy with old legacy spiderweb intertwined code that gets everything from everywhere, but not so useful when you're trying to unit test code, it is not visible from the header what dependencies you need. My preference goes to dependency injection, give all the dependencies to the class' constructor and use them that way. Makes it easy to mock and if you have many dependencies, it serves as a starting point to refactor in to a more clearly separated architecture. This article shows a piece of code that uses QObject, the Qt object base class, to replace a servicelocator. All QObjects can have a parent QObject, thus a tree is formed, which you can walk back up and search. This effectively replaces the servicelocator, since you can just request a certain type of QObject.

Recently I removed all Google Ads from this site due to their invasive tracking, as well as Google Analytics. Please, if you found this content useful, consider a small donation using any of the options below:

I'm developing an open source monitoring app called Leaf Node Monitoring, for windows, linux & android. Go check it out!

Consider sponsoring me on Github. It means the world to me if you show your appreciation and you'll help pay the server costs.

You can also sponsor me by getting a Digital Ocean VPS. With this referral link you'll get $100 credit for 60 days.

Johnnie 'QObject Walker is a satirical reference to the Whisky blend named Johnnie Walker. I can't confirm that I've actually named the class that way, but I might have.

I've used this code in a Qt5 application to replace a servicelocator. The application is a Qt Widgets application, making heavy use of QObject. But it had a legacy hardware communication component in it, which was using a servicelocator class to find runtime dependencies for logging, serial communication and other things which you'd normally not put inside that class. When looking at the code from a C4 architecture level, all of those things should be either in a presentation layer (logging/notifications) or in an infrastructure layer (serial communication) or elsewhere, not part of the core domain code.

The servicelocator code it replaced was a generic templated piece of code which was constructed early on. Over time, all objects were either replaced with QObject inherited code or no longer needed. In the end all objects on 'the service locator' were QObjects, which already were in the QObject tree.

QObject is a core part of the Qt C++ framework. Quoting the Qt documentation:

QObject is the heart of the Qt Object Model. The central feature in this model is a very powerful mechanism for seamless object communication called signals and slots. You can connect a signal to a slot with connect() and destroy the connection with disconnect(). To avoid never ending notification loops you can temporarily block signals with blockSignals (). The protected functions connectNotify() and disconnectNotify() make it possible to track connections.

QObjects organize themselves in object trees. When you create a QObject with another object as parent, the object will automatically add itself to the parent's children() list. The parent takes ownership of the object; i.e., it will automatically delete its children in its destructor. You can look for an object by name and optionally type using findChild() or findChildren (). Every object has an objectName() and its class name can be found via the corresponding metaObject() (see QMetaObject::className()). You can determine whether the object's class inherits another class in the QObject inheritance hierarchy by using the inherits() function.

When you're using QThreads the parent tree might be a bit different, but in this application the QObject tree, where applicable (when service locator was used) was linear. So, why not remove that (untested as I might say) code and replace it by framework-provided and tested code?

Note that this code is not applicable if you don't use Qt or do not have a linear parent tree, that is to say, every object has a parent leading up to your topmost QApplication (or QML variant)

FindQObject C++ code

This is the code I wrote to search the QObject tree. It first walks back up until there is no parent anymore, because in my case it is likely that the any one of the grand-parents is the class we want. (Don't ask me why, this is a 20 year old legacy C with objects compiled with a C++ compiler codebase).

template <typename T>
static T *findQObjectRecursive(const QObject* objectToSearch)
{
    if(objectToSearch == nullptr)
        return nullptr;

    if(objectToSearch->parent() == nullptr) {
        // arrived at the topmost QObject.
        // as last resort, search children recursively
        T* objectToFind = qobject_cast<T*>(objectToSearch->findChild<T*>());
        if(objectToFind)
            return objectToFind;
        else
            return nullptr;
    }


    T* objectToFind =  qobject_cast<T*>(objectToSearch->parent());
    if(objectToFind)
        return objectToFind;
    else
        return findQObjectRecursive<T>(objectToSearch->parent());
}

Usage example, finding the one and only class NotificationsModel:

NotificationsModel* notificationsModel = findQObjectRecursive<NotificationsModel>(this);
    if(notificationsModel)
        notificationsModel->sendNotification(notification);
}

This code is tailored to the specific codebase. It first searches the parents, and only if that didn't result in anything it uses the framework-provided findChild method. That method recursively walks it's own children and if not found, all their children. Not that efficient, but as said, in the codebase I'm currently working on, most objects have the utility classes in the parent tree (like notifications or serial communication). If you have multiple instances of a class, it is easy to adapt this code to search for multiple instances, returning a list, or maybe even search by QObject::objectName.

Benchmarking showed no noticeable slowdown, in most cases even a slight speed increase of a few milliseconds.

Unit Tests

There are of course a few unit tests for this code, a few of them generic enough to share. I like to use the pattern:

  • Arrange: set up the required test dependencies and mocks
  • Act: the one and only call that we're testing
  • Assert: check that the result matches what we're expecting
  • (cleanup): delete any pointers or cleanup other things. Optional step which I try to prevent by using RAII.

    This helps in keeping the unit tests small, testing one thing only. With test fixtures many different variations are possible, however most of the code I write is unit testable without mocks or fixtures, almost functional-programming like code.

The following unit tests are available:

TEST(HelpersTests, findQobjectTreeWorks)
{
    //arrange
    auto* t1 = new testObject1(nullptr);
    t1->setObjectName("rootTest");
    auto* t2 = new testObject2(t1);
    auto* t3 = new testObject3(t2);

    //act
    testObject1* findResult = findQObjectRecursive<testObject1>(t3);

    //assert
    ASSERT_EQ(findResult->objectName(), t1->objectName());

    //cleanup
    t1->deleteLater();
}

TEST(HelpersTests, findQobjectWithoutParentsDoesNotReturnResult)
{
    //arrange
    auto* t1 = new testObject1(nullptr);
    t1->setObjectName("rootTest");
    auto* t2 = new testObject2(nullptr);
    t2->setObjectName("t2");
    auto* t3 = new testObject3(nullptr);
    t3->setObjectName("t3");

    //act
    testObject1* findResult = findQObjectRecursive<testObject1>(t3);

    //assert
    ASSERT_EQ(findResult, nullptr);

    //cleanup
    t1->deleteLater();
    t2->deleteLater();
    t3->deleteLater();

}


TEST(HelpersTests, findQobjectSiblingOneLevelWorks)
{
    //arrange
    auto* t1 = new testObject1(nullptr);
    t1->setObjectName("t1");
    auto* t2 = new testObject2(t1);
    t2->setObjectName("t2");
    auto* t3 = new testObject3(t1);
    t3->setObjectName("t3");

    //act
    testObject2* findResult = findQObjectRecursive<testObject2>(t3);

    //assert
    ASSERT_NE(findResult, nullptr);
    ASSERT_EQ(findResult->objectName(), t2->objectName());

    //cleanup
    t1->deleteLater();
}

TEST(HelpersTests, findQobjectSiblingTwoLevelsWorks)
{
    //arrange
    auto* t1 = new testObject1(nullptr);
    auto* t2 = new testObject1(t1);
    auto* t3 = new testObject1(t1);
    auto* t2_1 = new testObject1(t2);
    auto* t3_1 = new testObject1(t3);
    auto* t2_2 = new testObject2(t2_1);
    auto* t3_2 = new testObject3(t3_1);
    t3_2->setObjectName("t3_2");

    //act
    testObject3* findResult = findQObjectRecursive<testObject3>(t2_2);

    //assert
    ASSERT_NE(findResult, nullptr);
    ASSERT_EQ(findResult->objectName(), t3_2->objectName());

    //cleanup
    t1->deleteLater();
}
Tags: c++ , cpp , gof , qmake , qt , qt5 , servicelocator , software