1. A Cute app deserves a
Clean architecture
Marco Piccolino
developer, consultant & trainer
http://marcopiccolino.eu
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 1
2. Bricks, doors and windows
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 2
3. Bricks, doors and windows
• Qt provides bricks, doors, windows etc.
• Equivalent to a DIY megastore
• What you do with those components is your problem
• That's a good thing, as long as you have a blueprint for your
application
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 3
5. Blueprints
What are the rooms I need? How do they connect to eachother?
Not much literature on application architecture approaches for Qt.
Interesting examples:
A Multilayered Architecture for Qt Quick - David Johnson
http://bit.ly/QtQuickMultilayeredJohnson
QML Application Architecture Guide with Flux – Ben Lau
http://bit.ly/QmlFluxBenlau
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 5
7. Clean architecture (Robert C. Martin)
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 7
8. Clean architecture: advantages
• Independent of Frameworks
• Testable
• Independent of UI
• Independent of Database
• Independent of external agencies
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 8
9. A cute implementation
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 9
10. Application layers
Use cases
Start from here to model application-specific business logic, i.e.
interactions between entities
Entities
Business objects - for me they arise by reasoning about use cases
Repositories
They abstract & encapsulate data I/O
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 10
11. Use cases / 1
Can be modeled via Behaviour-Driven Development's features and
scenarios. E.g.:
Feature: Check available groceries
I want to check groceries available in my fridge
to know when to buy them before I run out of them
Scenario: One or more grocery items available
Given the list of available grocery items is empty
And one or more grocery items are available
When I check available groceries
Then I am given the list of available grocery items
And the grocery items are ordered by type, ascending
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 11
12. Use cases / 2
Preconditions (Given), actions (When) and expected results (Then)
can modeled in QtTest or other testing frameworks.
void Usecases_check_available_groceries::test_one_or_more_grocery_items_available()
{
// Given the list of available grocery items is empty
auto groceryItems = new entities::GroceryItems(this);
QCOMPARE(groceryItems->list().count(), 0);
// And one or more grocery items are available
auto groceryItemsDummy = new repositories::GroceryItemsDummy(groceryItems);
QVERIFY(groceryItemsDummy->count() > 0);
groceryItems->setRepository(groceryItemsDummy);
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 12
13. // When I check available groceries
auto checkAvailableGroceries =
new usecases::CheckAvailableGroceries(groceryItems, this);
QSignalSpy checkAvailableGroceriesSuccess(
checkAvailableGroceries, SIGNAL(success(QString)));
checkAvailableGroceries->run();
QTRY_COMPARE_WITH_TIMEOUT(checkAvailableGroceriesSuccess.count(), 1, 1000);
// Then I am given the list of available grocery items
QCOMPARE(groceryItems->list().count(), groceryItemsDummy->count());
// And the grocery items are ordered by type, ascending
QVERIFY(groceryItems->isSortedBy("type","ASC"));
}
Among other benefits, writing the tests first helps defining a clean
API for usecases, entities and repositories.
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 13
14. Use cases / 3
• Entities can be passed to usecases as arguments:
checkAvailableGroceries->run(groceryItems);
• Alternatively, a global entity register can be created, and use
cases get hold of entities from there
• By creating mock repositories instead of real ones, use case tests
run fast:
auto groceryItemsDummy = new
repositories::GroceryItemsDummy(groceryItems);
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 14
15. Use cases / 4
Running the use case: a sequence of interactions with and between
entities.
void CheckAvailableGroceries::run(entities::GroceryItems *groceryItems)
{
connect(groceryItems, &entities::GroceryItems::allRetrieved,
this, &CheckAvailableGroceries::onGroceryItemsAllRetrieved,
Qt::UniqueConnection);
connect(groceryItems ,&entities::GroceryItems::allNotRetrieved,
this, &CheckAvailableGroceries::onGroceryItemsAllNotRetrieved,
Qt::UniqueConnection);
groceryItems->retrieveAll();
}
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 15
16. Use cases / 5
Once all business logic for the use case is done, we emit a signal:
void CheckAvailableGroceries::onGroceryItemsAllRetrieved() {
emit success("CHECK_AVAILABLE_GROCERIES__SUCCESS");
}
void CheckAvailableGroceries::onGroceryItemsAllNotRetrieved() {
emit failure("CHECK_AVAILABLE_GROCERIES__FAILURE");
}
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 16
17. Entities / 1
• The API of entities arises from use cases or is already given:
groceryItems->retrieveAll();
• Entities only implement business rules, and leave storage and data
retrieval concerns to repositories:
void GroceryItems::retrieveAll()
{
if (m_repository) {
m_repository->retrieveAllRecords();
}
}
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 17
18. Entities / 2
Once the data is retrieved, an entity performs its business logic:
void GroceryItems::onAllRecordsRetrieved(QVariantList records)
{
// sort by type ascending
...
// add records to list
m_list->clear();
m_list->append(records);
emit allRetrieved("ENTITIES_GROCERY_ITEMS_ALL_RETRIEVED");
}
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 18
19. Repositories / 1
Repositories encapsulate data storage, input and output.
Thanks to this, dummy repositories (test doubles) can be employed
while testing use cases and entities.
Benefits:
* Avoids coupling a specific technology (e.g. SQL, REST) to
application logic
* Much faster execution of use cases and entities test suites
* Can switch backends as needed
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 19
20. Repositories / 2
class GroceryItemsDummy : public GroceryItemsRepo
{
Q_OBJECT
public:
explicit GroceryItemsDummy(QObject *parent = nullptr)
:GroceryItemsRepo(parent){}
int count() const { return 3; }
void retrieveAllRecords() {
QVariantList recordsArray;
recordsArray.push_back(QVariantMap{{"type", "bananas"}});
recordsArray.push_back(QVariantMap{{"type", "apples"}});
recordsArray.push_back(QVariantMap{{"type", "cheese"}});
emit allRecordsRetrieved(recordsArray);
}
};
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 20
21. Adding a CLI
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 21
22. Adding a CLI / 1
Once the use case is complete, we can add all kinds of user
interfaces on top of it:
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
auto groceryItems = new entities::GroceryItems(&a);
auto groceryItemsDummy = new repositories::GroceryItemsDummy(&a);
auto checkAvailableGroceries = new usecases::CheckAvailableGroceries(&a);
groceryItems->setRepository(groceryItemsDummy);
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 22
23. Adding a CLI / 2
Text output:
QTextStream cout(stdout);
QObject::connect(checkAvailableGroceries,
&usecases::CheckAvailableGroceries::success,
[&cout, groceryItems](QString message) {
cout << message << endl;
auto list = groceryItems->list();
QVariantList::const_iterator i;
for (i = list.constBegin(); i != list.constEnd(); ++i)
cout << i->toMap().value("type").toString() << endl;
});
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 23
24. Adding a CLI / 3
Text input:
(cout << "Enter action: ").flush();
QTextStream cin(stdin);
QString action(cin.readLine());
if (action == "check available groceries") {
checkAvailableGroceries->run(groceryItems);
a.exit(0);
} else {
cout << "Action not supported" << endl;
a.exit(1);
}
}
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 24
25. Considerations
• For thin clients, one or more layers could be written in QML/JS
• This process seems tedious at first, but gains come later on
(debugging, refactoring, new features, new user interfaces)
• You might want to add more layers (e.g. Presenters).
• I went through a few iterations and am still refining the reasoning.
Would love feedback!
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 25
26. Thank you!
The Clean Architecture (blog post)
http://bit.ly/CleanArchBlog
Clean Architecture (book)
http://bit.ly/CleanArchBook
Code examples
https://github.com/marco-piccolino
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 26