Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

A Cute app deserves a Clean architecture

283 views

Published on

An implementation of Clean Architecture principles for Qt and C++ apps, touching upon BDD and automated testing

Published in: Software
  • Be the first to comment

A Cute app deserves a Clean architecture

  1. 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. 2. Bricks, doors and windows Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 2
  3. 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. 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. 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. 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. 7. Clean architecture (Robert C. Martin) Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 7
  8. 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. 9. A cute implementation Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 9
  10. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 21. Adding a CLI Marco Piccolino - A Cute app deserves a Clean architecture - http://marcopiccolino.eu - hello@marcopiccolino.eu 21
  22. 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. 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. 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. 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. 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

×