A Cute app deserves a Clean architecture

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
Bricks, doors and windows
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 2
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
Blueprints
Fair use, https://en.wikipedia.org/w/index.php?curid=29612930
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 4
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
Clean architecture
Credits: http://timshapkin.livejournal.com/16693.html
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 6
Clean architecture (Robert C. Martin)
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 7
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
A cute implementation
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 9
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
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
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
// 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
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
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
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
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
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
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
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
Adding a CLI
Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 21
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
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
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
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
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
1 of 26

More Related Content

Similar to A Cute app deserves a Clean architecture(20)

A Cute app deserves a Clean architecture

  • 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
  • 4. Blueprints Fair use, https://en.wikipedia.org/w/index.php?curid=29612930 Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 4
  • 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
  • 6. Clean architecture Credits: http://timshapkin.livejournal.com/16693.html Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 6
  • 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