Raymii.org
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 years 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 $200 credit for 60 days. Spend $25 after your credit expires and I'll get $25!
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 usingRAII
.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