Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Software Architecture with C++

You're reading from   Software Architecture with C++ Design modern systems using effective architecture concepts, design patterns, and techniques with C++20

Arrow left icon
Product type Paperback
Published in Apr 2021
Publisher Packt
ISBN-13 9781838554590
Length 540 pages
Edition 1st Edition
Languages
Arrow right icon
Authors (2):
Arrow left icon
Adrian Ostrowski Adrian Ostrowski
Author Profile Icon Adrian Ostrowski
Adrian Ostrowski
Piotr Gaczkowski Piotr Gaczkowski
Author Profile Icon Piotr Gaczkowski
Piotr Gaczkowski
Arrow right icon
View More author details
Toc

Table of Contents (24) Chapters Close

Preface 1. Section 1: Concepts and Components of Software Architecture
2. Importance of Software Architecture and Principles of Great Design FREE CHAPTER 3. Architectural Styles 4. Functional and Nonfunctional Requirements 5. Section 2: The Design and Development of C++ Software
6. Architectural and System Design 7. Leveraging C++ Language Features 8. Design Patterns and C++ 9. Building and Packaging 10. Section 3: Architectural Quality Attributes
11. Writing Testable Code 12. Continuous Integration and Continuous Deployment 13. Security in Code and Deployment 14. Performance 15. Section 4: Cloud-Native Design Principles
16. Service-Oriented Architecture 17. Designing Microservices 18. Containers 19. Cloud-Native Design 20. Assessments 21. About Packt 22. Other Books You May Enjoy Appendix A

Dependency inversion principle

Dependency inversion is a principle useful for decoupling. In essence, it means that high-level modules should not depend on lower-level ones. Instead, both should depend on abstractions.

C++ allows two ways to inverse the dependencies between your classes. The first one is the regular, polymorphic approach and the second uses templates. Let's see how to apply both of them in practice.

Assume you're modeling a software development project that is supposed to have frontend and backend developers. A simple approach would be to write it like so:

class FrontEndDeveloper {
public:
void developFrontEnd();
};

class BackEndDeveloper {
public:
void developBackEnd();
};

class Project {
public:
void deliver() {
fed_.developFrontEnd();
bed_.developBackEnd();
}
private:
FrontEndDeveloper fed_;
BackEndDeveloper bed_;
};

Each developer is constructed by the Project class. This approach is not ideal, though, since now the higher-level concept, Project, depends on lower-level ones – modules for individual developers. Let's see how applying dependency inversion using polymorphism changes this. We can define our developers to depend on an interface as follows:

class Developer {
public:
virtual ~Developer() = default;
virtual void develop() = 0;
};

class FrontEndDeveloper : public Developer {
public:
void develop() override { developFrontEnd(); }
private:
void developFrontEnd();
};

class BackEndDeveloper : public Developer {
public:
void develop() override { developBackEnd(); }
private:
void developBackEnd();
};

Now, the Project class no longer has to know the implementations of the developers. Because of this, it has to accept them as constructor arguments:

class Project {
public:
using Developers = std::vector<std::unique_ptr<Developer>>;
explicit Project(Developers developers)
: developers_{std::move(developers)} {}

void deliver() {
for (auto &developer : developers_) {
developer->develop();
}
}

private:
Developers developers_;
};

In this approach, Project is decoupled from the concrete implementations and instead depends only on the polymorphic interface named Developer. The "lower-level" concrete classes also depend on this interface. This can help you shorten your build time and allows for much easier unit testing – now you can easily pass mocks as arguments in your test code.

Using dependency inversion with virtual dispatch comes at a cost, however, as now we're dealing with memory allocations and the dynamic dispatch has overhead on its own. Sometimes C++ compilers can detect that only one implementation is being used for a given interface and will remove the overhead by performing devirtualization (often you need to mark the function as final for this to work). Here, however, two implementations are used, so the cost of dynamic dispatch (commonly implemented as jumping through virtual method tables, or vtables for short) must be paid.

There is another way of inverting dependencies that doesn't have those drawbacks. Let's see how this can be done using a variadic template, a generic lambda from C++14, and variant, either from C++17 or a third-party library such as Abseil or Boost. First are the developer classes:

class FrontEndDeveloper {
public:
void develop() { developFrontEnd(); }
private:
void developFrontEnd();
};

class BackEndDeveloper {
public:
void develop() { developBackEnd(); }
private:
void developBackEnd();
};

Now we don't rely on an interface anymore, so no virtual dispatch will be done. The Project class will still accept a vector of Developers:

template <typename... Devs>
class Project {
public:
using Developers = std::vector<std::variant<Devs...>>;

explicit Project(Developers developers)
: developers_{std::move(developers)} {}

void deliver() {
for (auto &developer : developers_) {
std::visit([](auto &dev) { dev.develop(); }, developer);
}
}

private:
Developers developers_;
};

If you're not familiar with variant, it's just a class that can hold any of the types passed as template parameters. Because we're using a variadic template, we can pass however many types we like. To call a function on the object stored in the variant, we can either extract it using std::get or use std::visit and a callable object – in our case, the generic lambda. It shows how duck-typing looks in practice. Since all our developer classes implement the develop function, the code will compile and run. If your developer classes would have different methods, you could, for instance, create a function object that has overloads of operator() for different types.

Because Project is now a template, we have to either specify the list of types each time we create it or provide a type alias. You can use the final class like so:

using MyProject = Project<FrontEndDeveloper, BackEndDeveloper>;
auto alice = FrontEndDeveloper{};
auto bob = BackEndDeveloper{};
auto new_project = MyProject{{alice, bob}};
new_project.deliver();

This approach is guaranteed to not allocate separate memory for each developer or use a virtual table. However, in some cases, this approach results in less extensibility, since once the variant is declared, you cannot add another type to it.

As the last thing to mention about dependency inversion, we'd like to note that there is a similarly named idea called dependency injection, which we even used in our examples. It's about injecting the dependencies through constructors or setters, which can be beneficial to code testability (think about injecting mock objects, for example). There are even whole frameworks for injecting dependencies throughout whole applications, such as Boost.DI. Those two concepts are related and often used together.

You have been reading a chapter from
Software Architecture with C++
Published in: Apr 2021
Publisher: Packt
ISBN-13: 9781838554590
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime