Use Frameworks But Don’t Marry Them

Last Year I finished Clean Architecture by Robert C. Martin, where he describes what a good software architecture is and how to achieve it. In this article I'll show an example about using frameworks according to:

Don’t marry the framework!
— Robert C. Martin

Consider this: You started a software project for a book store API. You'll need a function to add books and for the first release it was defined to provide them in a xml file. You decided early on to use the boost xml parser. Everything worked as expected and the release was successful. So far so good...

Take a look at the following code snippet and see the highlighted boost dependencies.

// book_store_api.hpp:
// ...
void add_books(const std::string& xml_file) {
    book_reader books(xml_file);
    books.add_books();
}
// ...

// book_reader.hpp:
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>

// ...
class book_reader {
private:
    boost::property_tree::ptree pt;
public:
    book_reader(const std::string& xml_file) {
        boost::property_tree::read_xml(xml_file, pt);
    }

    void add_books() {
        BOOST_FOREACH(boost::property_tree::ptree::value_type &v, pt.get_child("books")) {
            internal::add_book(
                v.second.get_child("id").data(),
                v.second.get_child("author").data(), 
                v.second.get_child("title").data(), 
                v.second.get_child("published").data()
            );
        }
    }
};
// ...

With this implementation we literally married the boost framework. The book_reader depends on it. And worse: A poor name was chosen with book_reader. Only the constructor's argument indicates which file type is valid.

Imagine there is a new feature request where also adding books should work from a json and a lua file.

Of course this can be implemented but problems are arising here.

  • The changes need to be done on the released code, where the logic is implemented
  • You can't change the name nor the signature from the API or the book_reader (pretend clients can use this class too)
  • Everything could be worse if there aren't any or poor unittests

We'd have avoided these problems by just using an interface. For instance you could combine this with a factory to create a book_reader.

Consider following code as an alternative first release and note the highlighted changes.

 // book_store_api.hpp:
// ...
void add_books(const std::string& file) {
    auto book_reader = book_reader_factory::create(file);
    book_reader->add_books();
}

// book_reader.hpp:
class book_reader {
public: 
    virtual ~book_reader() {}
    virtual void add_books() = 0; 
};

// book_reader_factory.hpp:
class book_reader_factory {
public:
    static std::unique_ptr<book_reader> create(const std::string& configuration_file);
};

// xml_book_reader.hpp:
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
// ...
class xml_book_reader : public book_reader {
private:
    boost::property_tree::ptree pt;
public: 
    // implementation from the code snippet above (book_reader)
    xml_book_reader(const std::string& file);
    void add_books() override; 
};

Let's go through this new approach step by step:

  1. We changed book_reader to an interface, specific implementations can be done in derived classes
  2. We can create any type which inherits the interface book_reader
  3. The book_reader_factory could create specific book_readers depending on the file type, which is passed to the create function
  4. The xml_book_reader implements book_reader and therefore the xml and boost specific code

Now we gained more flexibility. If there were a feature request for reading books from json or lua files, we'd for instance create a json_book_reader and a lua_book_reader. The factory then would create these new types and public methods would be the same. The API function and the existing xml implementation wouldn't be affected from that (which is exactly what we want).

 

Conclusion

Don't marry a framework, keep dependencies encapsulated. It's useful to take advantage from them, because you don't want to write in this particular case your own xml parser. But keep in mind changes and new requirements are coming.

This is one scenario why a specific implementations needs to be changed or extended. Furthermore it illustrates why hardcoded details are bad. There are other reasons too. Prefer interfaces to make changes or replacements without affecting your released code.

I'll continue on this example with an article about parameterized unittests with the google test framework. This is a great way to write interfacetests.

But that's it for now.

Best Thomas

Previous
Previous

[C++] Typed Tests For Interfaces With Googletest

Next
Next

[C++] Protobuf: Project Setup With Conan and CMake