Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Software Architecture with C++
Software Architecture with C++

Software Architecture with C++: Design modern systems using effective architecture concepts, design patterns, and techniques with C++20

Arrow left icon
Profile Icon Adrian Ostrowski Profile Icon Piotr Gaczkowski
Arrow right icon
€22.99 €32.99
Full star icon Full star icon Full star icon Full star icon Half star icon 4.4 (13 Ratings)
eBook Apr 2021 540 pages 1st Edition
eBook
€22.99 €32.99
Paperback
€41.99
Audiobook
€24.99
Subscription
Free Trial
Renews at €18.99p/m
Arrow left icon
Profile Icon Adrian Ostrowski Profile Icon Piotr Gaczkowski
Arrow right icon
€22.99 €32.99
Full star icon Full star icon Full star icon Full star icon Half star icon 4.4 (13 Ratings)
eBook Apr 2021 540 pages 1st Edition
eBook
€22.99 €32.99
Paperback
€41.99
Audiobook
€24.99
Subscription
Free Trial
Renews at €18.99p/m
eBook
€22.99 €32.99
Paperback
€41.99
Audiobook
€24.99
Subscription
Free Trial
Renews at €18.99p/m

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Table of content icon View table of contents Preview book icon Preview Book

Software Architecture with C++

Importance of Software Architecture and Principles of Great Design

The purpose of this introductory chapter is to show what role software architecture plays in software development. It will focus on the key aspects to keep in mind when designing the architecture of a C++ solution. We'll discuss how to design efficient code with convenient and functional interfaces. We'll also introduce a domain-driven approach for both code and architecture.

In this chapter, we'll cover the following topics:

  • Understanding software architecture
  • Learning the importance of proper architecture
  • Exploring the fundamentals of good architecture
  • Developing architecture using Agile principles
  • The philosophy of C++
  • Following the SOLID and DRY principles
  • Domain-driven design
  • Coupling and cohesion

Technical requirements

Understanding software architecture

Let's begin by defining what software architecture actually is. When you create an application, library, or any software component, you need to think about how the elements you write will look and how they will interact with each other. In other words, you're designing them and their relations with their surroundings. Just like with urban architecture, it's important to think about the bigger picture to not end up in a haphazard state. On a small scale, every single building looks okay, but they don't combine into a sensible bigger picture – they just don't fit together well. This is what's called accidental architecture and it is one of the outcomes you want to avoid. However, keep in mind that whether you're putting your thoughts into it or not, when writing software you are creating an architecture.

So, what exactly should you be creating if you want to mindfully define the architecture of your solution? The Software Engineering Institute has this to say:

The software architecture of a system is the set of structures needed to reason about the system, which comprise software elements, relations among them, and properties of both.

This means that in order to define an architecture thoroughly, we should think about it from a few perspectives instead of just hopping into writing code.

Different ways to look at architecture

There are several scopes that can be used to look at architecture:

  • Enterprise architecture deals with the whole company or even a group of companies. It takes a holistic approach and is concerned about the strategy of whole enterprises. When thinking about enterprise architecture, you should be looking at how all the systems in a company behave and cooperate with each other. It's concerned about the alignment between business and IT.
  • Solution architecture is less abstract than its enterprise counterpart. It stands somewhere in the middle between enterprise and software architecture. Usually, solution architecture is concerned with one specific system and the way it interacts with its surroundings. A solution architect needs to come up with a way to fulfill a specific business need, usually by designing a whole software system or modifying existing ones.
  • Software architecture is even more concrete than solution architecture. It concentrates on a specific project, the technologies it uses, and how it interacts with other projects. A software architect is interested in the internals of the project's components.
  • Infrastructure architecture is, as the name suggests, concerned about the infrastructure that the software will use. It defines the deployment environment and strategy, how the application will scale, failover handling, site reliability, and other infrastructure-oriented aspects.

Solution architecture is based on both software and infrastructure architectures to satisfy the business requirements. In the following sections, we will talk about both those aspects to prepare you for both small- and large-scale architecture design. Before we jump into that, let's also answer one fundamental question: why is architecture important?

Learning the importance of proper architecture

Actually, a better question would be: why is caring about your architecture important? As we mentioned earlier, regardless of whether you put conscious effort into building it or not, you will end up with some kind of architecture. If after several months or even years of development you still want your software to retain its qualities, you need to take some steps earlier in the process. If you won't think about your architecture, chances are it won't ever present the required qualities.

So, in order for your product to meet the business requirements and attributes such as performance, maintainability, scalability, or others, you need to take care of its architecture, and it is best if you do it as early as you can in the process. Let's now discuss two things that each good architect wants to protect their projects from.

Software decay

Even after you did the initial work and had a specific architecture in mind, you need to continuously monitor how the system evolves and whether it still aligns with its users' needs, as those may also change during the development and lifetime of your software. Software decay, sometimes also called erosion, occurs when the implementation decisions don't correspond to the planned architecture. All such differences should be treated as technical debt.

Accidental architecture

Failing to track if the development adheres to the chosen architecture or failing to intentionally plan how the architecture should look will often result in a so-called accidental architecture, and it can happen regardless of applying best practices in other areas, such as testing or having any specific development culture.

There are several anti-patterns that suggest your architecture is accidental. Code resembling a big ball of mud is the most obvious one. Having god objects is another important sign of this. Generally speaking, if your software is getting tightly coupled, perhaps with circular dependencies, but wasn't like that in the first place, it's an important signal to put more conscious effort into how the architecture looks.

Let's now describe what an architect must understand to deliver a viable solution.

Exploring the fundamentals of good architecture

It's important to know how to recognize a good architecture from a bad one, but it's not an easy task. Recognizing anti-patterns is an important aspect of it, but for an architecture to be good, primarily it has to support delivering what's expected from the software, whether it's about functional requirements, attributes of the solution, or dealing with the constraints coming from various places. Many of those can be easily derived from the architecture context.

Architecture context

The context is what an architect takes into account when designing a solid solution. It comprises requirements, assumptions, and constraints, which can come from the stakeholders, as well as the business and technical environments. It also influences the stakeholders and the environments, for example, by allowing the company to enter a new market segment.

Stakeholders

Stakeholders are all the people that are somehow involved with the product. Those can be your customers, the users of your system, or the management. Communication is a key skill for every architect and properly managing your stakeholder's needs is key to delivering what they expected and in a way they wanted.

Different things are important to different groups of stakeholders, so try to gather input from all those groups.

Your customers will probably care about the cost of writing and running the software, the functionality it delivers, its lifetime, time to market, and the quality of your solution.

The users of your system can be divided into two groups: end users and administrators. The first ones usually care about things such as the usability, user experience, and performance of the software. For the latter, more important aspects are user management, system configuration, security, backups, and recovery.

Finally, things that could matter for stakeholders working in management are keeping the development costs low, achieving business goals, being on track with the development schedule, and maintaining product quality.

Business and technical environments

Architecture can be influenced by the business side of the company. Important related aspects are the time to market, the rollout schedule, the organizational structure, utilization of the workforce, and investment in existing assets.

By technical environment, we mean the technologies already used in a company or those that are for any reason required to be part of the solution. Other systems that we need to integrate with are also a vital part of the technical environment. The technical expertise of the available software engineers is of importance here, too: the technological decisions an architect makes can impact staffing the project, and the ratio of junior to senior developers can influence how a project should be governed. Good architecture should take all of that into account.

Equipped with all this knowledge, let's now discuss a somewhat controversial topic that you'll most probably encounter as an architect in your daily work.

Developing architecture using Agile principles

Seemingly, architecture and Agile development methodologies are in an adversarial relationship, and there are many myths around this topic. There are a few simple principles that you should follow in order to develop your product in an Agile way while still caring about its architecture.

Agile, by nature, is iterative and incremental. This means preparing a big, upfront design is not an option in an Agile approach to architecture. Instead, a small, but still reasonable upfront design should be proposed. It's best if it comes with a log of decisions with the rationale for each of them. This way, if the product vision changes, the architecture can evolve with it. To support frequent release delivery, the upfront design should then be updated incrementally. Architecture developed this way is called evolutionary architecture.

Managing architecture doesn't need to mean keeping massive documentation. In fact, documentation should cover only what's essential as this way it's easier to keep it up to date. It should be simple and cover only the relevant views of the system.

There's also the myth of the architect as the single source of truth and the ultimate decision-maker. In Agile environments, it's the teams who are making decisions. Having said that, it's crucial that the stakeholders are contributing to the decision-making process – after all, their points of view shape how the solution should look.

An architect should remain part of the development team as often they're bringing strong technical expertise and years of experience to the table. They should also take part in making estimations and plan the architecture changes needed before each iteration.

In order for your team to remain Agile, you should think of ways to work efficiently and only on what's important. A good idea to embrace to achieve those goals is domain-driven design.

Domain-driven design

Domain-driven design, or DDD for short, is a term introduced by Eric Evans in his book of the same title. In essence, it's about improving communication between business and engineering and bringing the developers' attention to the domain model. Basing the implementation of this model often leads to designs that are easier to understand and evolve together with the model changes.

What has DDD got to do with Agile? Let's recall a part of the Agile Manifesto:

Individuals and interactions over processes and tools
Working software over comprehensive documentation
Customer collaboration over contract negotiation
Responding to change over following a plan

— The Agile Manifesto

In order to make the proper design decisions, you must understand the domain first. To do so, you'll need to talk to people a lot and encourage your developer teams to narrow the gap between them and business people. The concepts in the code should be named after entities that are part of ubiquitous language. It's basically the common part of business experts' jargon and technical experts' jargon. Countless misunderstandings can be caused by each of these groups using terms that the other understands differently, leading to flaws in business logic implementations and often subtle bugs. Naming things with care and using terms agreed by both groups can mean bliss for the project. Having a business analyst or other business domain experts as part of the team can help a lot here.

If you're modeling a bigger system, it might be hard to make all the terms mean the same to different teams. This is because each of those teams really operates in a different context. DDD proposes the use of bounded contexts to deal with this. If you're modeling, say, an e-commerce system, you might want to think of the terms just in terms of a shopping context, but upon a closer look, you may discover that the inventory, delivery, and accounting teams actually all have their own models and terms.

Each of those is a different subdomain of your e-commerce domain. Ideally, each can be mapped to its own bounded context – a part of your system with its own vocabulary. It's important to set clear boundaries of such contexts when splitting your solution into smaller modules. Just like its context, each module has clear responsibilities, its own database schema, and its own code base. To help communicate between the teams in larger systems, you might want to introduce a context map, which will show how the terms from different contexts relate to each other:

Figure 1.1 – Two bounding contexts with the matching terms mapped between them (image from one of Martin Fowler's articles on DDD: https://martinfowler.com/bliki/BoundedContext.html)

As you now understand some of the important project-management topics, we can switch to a few more technical ones.

The philosophy of C++

Let's now move closer to the programming language we'll be using the most throughout this book. C++ is a multi-paradigm language that has been around for a few decades now. During the years since its inception, it has changed a lot. When C++11 came out, Bjarne Stroustrup, the creator of the language, said that it felt like a completely new language. The release of C++20 marks another milestone in the evolution of this beast, bringing a similar revolution to how we write code. One thing, however, stayed the same during all those years: the language's philosophy.

In short, it can be summarized by three rules:

  • There should be no language beneath C++ (except assembly).
  • You only pay for what you use.
  • Offer high-level abstractions at low cost (there's a strong aim for zero-cost).

Not paying for what you don't use means that, for example, if you want to have your data member created on the stack, you can. Many languages allocate their objects on the heap, but it's not necessary for C++. Allocating on the heap has some cost to it – probably your allocator will have to lock a mutex for this, which can be a big burden in some types of applications. The good part is you can easily allocate variables without dynamically allocating memory each time pretty easily.

High-level abstractions are what differentiate C++ from lower-level languages such as C or assembly. They allow for expressing ideas and intent directly in the source code, which plays great with the language's type safety. Consider the following code snippet:

struct Duration {
int millis_;
};

void example() {
auto d = Duration{};
d.millis_ = 100;

auto timeout = 1; // second
d.millis_ = timeout; // ouch, we meant 1000 millis but assigned just 1
}

A much better idea would be to leverage the type-safety features offered by the language:

#include <chrono>

using namespace std::literals::chrono_literals;

struct Duration {
std::chrono::milliseconds millis_;
};

void example() {
auto d = Duration{};
// d.millis_ = 100; // compilation error, as 100 could mean anything
d.millis_ = 100ms; // okay

auto timeout = 1s; // or std::chrono::seconds(1);
d.millis_ =
timeout; // okay, converted automatically to milliseconds
}

The preceding abstraction can save us from mistakes and doesn't cost us anything while doing so; the assembly generated would be the same as for the first example. That's why it's called a zero-cost abstraction. Sometimes C++ allows us to use abstractions that actually result in better code than if they were not used. One example of a language feature that, when used, could often result in such benefit is coroutines from C++20.

Another great set of abstractions, offered by the standard library, are algorithms. Which of the following code snippets do you think is easier to read and easier to prove bug-free? Which expresses the intent better?

// Approach #1
int count_dots(const char *str, std::size_t len) {
int count = 0;
for (std::size_t i = 0; i < len; ++i) {
if (str[i] == '.') count++;
}
return count;
}

// Approach #2
int count_dots(std::string_view str) {
return std::count(std::begin(str), std::end(str), '.');
}

Okay, the second function has a different interface, but even it if was to stay the same, we could just create std::string_view from the pointer and the length. Since it's such a lightweight type, it should be optimized away by your compiler.

Using higher-level abstractions leads to simpler, more maintainable code. The C++ language has strived to provide zero-cost abstractions since its inception, so build upon that instead of redesigning the wheel using lower levels of abstraction.

Speaking of simple and maintainable code, the next section introduces some rules and heuristics that are invaluable on the path to writing such code.

Following the SOLID and DRY principles

There are many principles to keep in mind when writing code. When writing object-oriented code, you should be familiar with the quartet of abstraction, encapsulation, inheritance, and polymorphism. Regardless of whether your writing C++ in a mostly object-oriented programming manner or not, you should keep in mind the principles behind the two acronyms: SOLID and DRY.

SOLID is a set of practices that can help you write cleaner and less bug-prone software. It's an acronym made from the first letters of the five concepts behind it:

  • Single responsibility principle
  • Open-closed principle
  • Liskov substitution principle
  • Interface segregation
  • Dependency Inversion

We assume you already have the idea of how those principles relate to object-oriented programming, but since C++ is not always object-oriented, let's look at how they apply to different areas.

Some of the examples use dynamic polymorphism, but the same would apply to static polymorphism. If you're writing performance-oriented code (and you probably are if you chose C++), you should know that using dynamic polymorphism can be a bad idea in terms of performance, especially on the hot path. Further on in the book, you'll learn how to write statically polymorphic classes using the Curiously Recurring Template Pattern (CRTP).

Single responsibility principle

In short, the Single Responsibility Principle (SRP) means each code unit should have exactly one responsibility. This means writing functions that do one thing only, creating types that are responsible for a single thing, and creating higher-level components that are focused on one aspect only.

This means that if your class manages some type of resources, such as file handles, it should do only that, leaving parsing them, for example, to another type.

Often, if you see a function with "And" in its name, it's violating the SRP and should be refactored. Another sign is when a function has comments indicating what each section of the function (sic!) does. Each such section would probably be better off as a distinct function.

A related topic is the principle of least knowledge. In its essence, it says that no object should know no more than necessary about other objects, so it doesn't depend on any of their internals, for example. Applying it leads to more maintainable code with fewer interdependencies between components.

Open-closed principle

The Open-Closed Principle (OCP) means that code should be open for extension but closed for modification. Open for extension means that we could extend the list of types the code supports easily. Closed for modification means existing code shouldn't change, as this can often cause bugs somewhere else in the system. A great feature of C++ demonstrating this principle is operator<< of ostream. To extend it so that it supports your custom class, all you need to do is to write code similar to the following:

std::ostream &operator<<(std::ostream &stream, const MyPair<int, int> 
&mp) {
stream << mp.firstMember() << ", ";
stream << mp.secondMember();
return stream;
}

Note that our implementation of operator<< is a free (non-member) function. You should prefer those to member functions if possible as it actually helps encapsulation. For more details on this, consult the article by Scott Meyers in the Further reading section at the end of this chapter. If you don't want to provide public access to some field that you wish to print to ostream, you can make operator<< a friend function, like so:

class MyPair {
// ...
friend std::ostream &operator<<(std::ostream &stream,
const MyPair &mp);
};
std::ostream &operator<<(std::ostream &stream, const MyPair &mp) {
stream << mp.first_ << ", ";
stream << mp.second_ << ", ";
stream << mp.secretThirdMember_;
return stream;
}

Note that this definition of OCP is slightly different from the more common one related to polymorphism. The latter is about creating base classes that can't be modified themselves, but are open for others to inherit from them.

Speaking of polymorphism, let's move on to the next principle as it is all about using it correctly.

Liskov substitution principle

In essence, the Liskov Substitution Principle (LSP) states that if a function works with a pointer or reference to a base object, it must also work with a pointer or reference to any of its derived objects. This rule is sometimes broken because the techniques we apply in source code do not always work in real-world abstractions.

A famous example is a square and a rectangle. Mathematically speaking, the former is a specialization of the latter, so there's an "is a" relationship from one to the other. This tempts us to create a Square class that inherits from the Rectangle class. So, we could end up with code like the following:

class Rectangle {
public:
virtual ~Rectangle() = default;
virtual double area() { return width_ * height_; }
virtual void setWidth(double width) { width_ = width; }
virtual void setHeight(double height) { height_ = height; }
private:
double width_;
double height_;
};

class Square : public Rectangle {
public:
double area() override;
void setWidth(double width) override;
void setHeight(double height) override;
};

How should we implement the members of the Square class? If we want to follow the LSP and save the users of such classes from surprises, we can't: our square would stop being a square if we called setWidth. We can either stop having a square (not expressible using the preceding code) or modify the height as well, thus making the square look different than a rectangle.

If your code violates the LSP, it's likely that you're using an incorrect abstraction. In our case, Square shouldn't inherit from Rectangle after all. A better approach could be making the two implement a GeometricFigure interface.

Since we are on the topic of interfaces, let's move on to the next item, which is also related to them.

Interface segregation principle

The interface segregation principle is just about what its name suggests. It is formulated as follows:

No client should be forced to depend on methods that it does not use.

That sounds pretty obvious, but it has some connotations that aren't that obvious. Firstly, you should prefer more but smaller interfaces to a single big one. Secondly, when you're adding a derived class or are extending the functionality of an existing one, you should think before you extend the interface the class implements.

Let's show this on an example that violates this principle, starting with the following interface:

class IFoodProcessor {
public:
virtual ~IFoodProcessor() = default;
virtual void blend() = 0;
};

We could have a simple class that implements it:

class Blender : public IFoodProcessor {
public:
void blend() override;
};

So far so good. Now say we want to model another, more advanced food processor and we recklessly tried to add more methods to our interface:

class IFoodProcessor {
public:
virtual ~IFoodProcessor() = default;
virtual void blend() = 0;
virtual void slice() = 0;
virtual void dice() = 0;
};

class AnotherFoodProcessor : public IFoodProcessor {
public:
void blend() override;
void slice() override;
void dice() override;
};

Now we have an issue with the Blender class as it doesn't support this new interface – there's no proper way to implement it. We could try to hack a workaround or throw std::logic_error, but a much better solution would be to just split the interface into two, each with a separate responsibility:

class IBlender {
public:
virtual ~IBlender() = default;
virtual void blend() = 0;
};

class ICutter {
public:
virtual ~ICutter() = default;
virtual void slice() = 0;
virtual void dice() = 0;
};

Now our AnotherFoodProcessor can just implement both interfaces, and we don't need to change the implementation of our existing food processor.

We have one last SOLID principle left, so let's learn about it now.

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.

The DRY rule

DRY is short for "don't repeat yourself." It means you should avoid code duplication and reuse when it's possible. This means you should extract a function or a function template when your code repeats similar operations a few times. Also, instead of creating several similar types, you should consider writing a template.

It's also important not to reinvent the wheel when it's not necessary, that is, not to repeat others' work. Nowadays there are dozens of well-written and mature libraries that can help you with writing high-quality software faster. We'd like to specifically mention a few of them:

Sometimes duplicating code can have its benefits, however. One such scenario is developing microservices. Of course, it's always a good idea to follow DRY inside a single microservice, but violating the DRY rule for code used in multiple services can actually be worth it. Whether we're talking about model entities or logic, it's easier to maintain multiple services when code duplication is allowed.

Imagine having multiple microservices reusing the same code for an entity. Suddenly one of them needs to modify one field. All the other services now have to be modified as well. The same goes for dependencies of any common code. With dozens or more microservices that have to be modified because of changes unrelated to them, it's often easier for maintenance to just duplicate the code.

Since we're talking about dependencies and maintenance, let's proceed to the next section, which discusses a closely related topic.

Coupling and cohesion

Coupling and cohesion are two terms that go hand in hand in software. Let's see what each of them means and how they relate to each other.

Coupling

Coupling is a measure of how strongly one software unit depends on other units. A unit with high coupling relies on many other units. The lower the coupling, the better.

For example, if a class depends on private members of another class, it means they're tightly coupled. A change in the second class would probably mean that the first one needs to be changed as well, which is why it's not a desirable situation.

To weaken the coupling in the preceding scenario, we could think about adding parameters for the member functions instead of directly accessing other classes' private members.

Another example of tightly coupled classes is the first implementation of the Project and developer classes in the dependency inversion section. Let's see what would happen if we were to add yet another developer type:

class MiddlewareDeveloper {
public:
void developMiddleware() {}
};

class Project {
public:
void deliver() {
fed_.developFrontEnd();
med_.developMiddleware();
bed_.developBackEnd();
}

private:
FrontEndDeveloper fed_;
MiddlewareDeveloper med_;
BackEndDeveloper bed_;
};

It looks like instead of just adding the MiddlewareDeveloper class, we had to modify the public interface of the Project class. This means they're tightly coupled and that this implementation of the Project class actually breaks the OCP. For comparison, let's now see how the same modification would be applied to the implementation using dependency inversion:

class MiddlewareDeveloper {
public:
void develop() { developMiddleware(); }

private:
void developMiddleware();
};

No changes to the Project class were required, so now the classes are loosely coupled. All we needed to do was to add the MiddlewareDeveloper class. Structuring our code this way allows for smaller rebuilds, faster development, and easier testing, all with less code that's easier to maintain. To use our new class, we only need to modify the calling code:

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

This shows coupling on a class level. On a larger scale, for instance, between two services, the low coupling can be achieved by introducing techniques such as message queueing. The services wouldn't then depend on each other directly, but just on the message format. If you're having a microservice architecture, a common mistake is to have multiple services use the same database. This causes coupling between those services as you cannot freely modify the database schema without affecting all the microservices that use it.

Let's now move on to cohesion.

Cohesion

Cohesion is a measure of how strongly a software unit's elements are related. In a highly cohesive system, the functionality offered by components in the same module is strongly related. It feels like such components just belong together.

On a class level, the more fields a method manipulates, the more cohesive it is to the class. This means that the most commonly spotted low-cohesion data types are those big monolithic ones. When there's too much going on in a class, it most probably is not cohesive and breaks the SRP, too. This makes such classes hard to maintain and bug-prone.

Smaller classes can be incohesive as well. Consider the following example. It may seem trivial, but posting real-life scenarios, often hundreds if not thousands of lines long, would be impractical:

class CachingProcessor {
public:
Result process(WorkItem work);
Results processBatch(WorkBatch batch);
void addListener(const Listener &listener);
void removeListener(const Listener &listener);

private:
void addToCache(const WorkItem &work, const Result &result);
void findInCache(const WorkItem &work);
void limitCacheSize(std::size_t size);
void notifyListeners(const Result &result);
// ...
};

We can see that our processor actually does three types of work: the actual work, the caching of the results, and managing listeners. A common way to increase cohesion in such scenarios is to extract a class or even multiple ones:

class WorkResultsCache {
public:
void addToCache(const WorkItem &work, const Result &result);
void findInCache(const WorkItem &work);
void limitCacheSize(std::size_t size);
private:
// ...
};

class ResultNotifier {
public:
void addListener(const Listener &listener);
void removeListener(const Listener &listener);
void notify(const Result &result);
private:
// ...
};

class CachingProcessor {
public:
explicit CachingProcessor(ResultNotifier &notifier);
Result process(WorkItem work);
Results processBatch(WorkBatch batch);
private:
WorkResultsCache cache_;
ResultNotifier notifier_;
// ...
};

Now each part is done by a separate, cohesive entity. Reusing them is now possible without much hassle. Even making them a template class should require little work. Last but not least, testing such classes should be easier as well.

Putting this on a component or system level is straightforward – each component, service, and system you design should be concise and focus on doing one thing and doing it right:

Figure 1.2 – Coupling versus cohesion

Low cohesion and high coupling are usually associated with software that's difficult to test, reuse, maintain, or even understand, so it lacks many of the quality attributes usually desired to have in software.

Those terms often go together because often one trait influences the other, regardless of whether the unit we talk about is a function, class, library, service, or even a whole system. To give an example, usually, monoliths are highly coupled and low cohesive, while distributed services tend to be at the other end of the spectrum.

This concludes our introductory chapter. Let's now summarize what we've learned.

Summary

In this chapter, we discussed what software architecture is and why it's worth caring about it. We've shown what happens when architecture is not updated along with the changing requirements and implementation and how to treat architecture in an Agile environment. Then we moved on to some core principles of the C++ language.

We learned that many terms from software development can be perceived differently in C++ because C++ allows more than writing object-oriented code. Finally, we discussed terms such as coupling and cohesion.

You should now be able to point out many design flaws in code reviews and refactor your solutions for greater maintainability, as well as being less bug-prone as a developer. You can now design class interfaces that are robust, self-explanatory, and complete.

In the next chapter, we will learn about the different architectural approaches or styles. We will also learn about how and when we can use them to gain better results. 

Questions

  1. Why care about software architecture?
  2. Should the architect be the ultimate decision-maker in an Agile team?
  3. How is the SRP related to cohesion?
  4. In what phases of a project's lifetime can it benefit from having an architect?
  5. What's the benefit of following the SRP?

Further reading

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Design scalable large-scale applications with the C++ programming language
  • Architect software solutions in a cloud-based environment with continuous integration and continuous delivery (CI/CD)
  • Achieve architectural goals by leveraging design patterns, language features, and useful tools

Description

Software architecture refers to the high-level design of complex applications. It is evolving just like the languages we use, but there are architectural concepts and patterns that you can learn to write high-performance apps in a high-level language without sacrificing readability and maintainability. If you're working with modern C++, this practical guide will help you put your knowledge to work and design distributed, large-scale apps. You'll start by getting up to speed with architectural concepts, including established patterns and rising trends, then move on to understanding what software architecture actually is and start exploring its components. Next, you'll discover the design concepts involved in application architecture and the patterns in software development, before going on to learn how to build, package, integrate, and deploy your components. In the concluding chapters, you'll explore different architectural qualities, such as maintainability, reusability, testability, performance, scalability, and security. Finally, you will get an overview of distributed systems, such as service-oriented architecture, microservices, and cloud-native, and understand how to apply them in application development. By the end of this book, you'll be able to build distributed services using modern C++ and associated tools to deliver solutions as per your clients' requirements.

Who is this book for?

This software architecture C++ programming book is for experienced C++ developers looking to become software architects or develop enterprise-grade applications.

What you will learn

  • Understand how to apply the principles of software architecture
  • Apply design patterns and best practices to meet your architectural goals
  • Write elegant, safe, and performant code using the latest C++ features
  • Build applications that are easy to maintain and deploy
  • Explore the different architectural approaches and learn to apply them as per your requirement
  • Simplify development and operations using application containers
  • Discover various techniques to solve common problems in software design and development

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Apr 23, 2021
Length: 540 pages
Edition : 1st
Language : English
ISBN-13 : 9781789612462
Category :
Languages :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning

Product Details

Publication date : Apr 23, 2021
Length: 540 pages
Edition : 1st
Language : English
ISBN-13 : 9781789612462
Category :
Languages :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total 158.97
Modern C++ Programming Cookbook
€71.99
C++ High Performance
€44.99
Software Architecture with C++
€41.99
Total 158.97 Stars icon

Table of Contents

22 Chapters
Section 1: Concepts and Components of Software Architecture Chevron down icon Chevron up icon
Importance of Software Architecture and Principles of Great Design Chevron down icon Chevron up icon
Architectural Styles Chevron down icon Chevron up icon
Functional and Nonfunctional Requirements Chevron down icon Chevron up icon
Section 2: The Design and Development of C++ Software Chevron down icon Chevron up icon
Architectural and System Design Chevron down icon Chevron up icon
Architectural and System Design
Technical requirements
Understanding the peculiarities of distributed systems
Different service models and when to use them
On-premises model
Infrastructure as a Service (IaaS) model
Platform as a Service (PaaS) model
Software as a Service (SaaS) model
Function as a Service (FaaS) model and serverless architecture
Avoiding the fallacies of distributed computing
The network is reliable
Latency is zero
Bandwidth is infinite
The network is secure
Topology doesn't change
There is one administrator
Transport cost is zero
The network is homogeneous
CAP theorem and eventual consistency
Sagas and compensating transactions
Choreography-based sagas
Orchestration-based sagas
Making your system fault tolerant and available
Calculating your system's availability
Building fault-tolerant systems
Redundancy
Leader election
Consensus
Replication
Master-slave replication
Multi-master replication
Queue-based load leveling
Back pressure
Detecting faults
Sidecar design pattern
Heartbeat mechanism
Leaky bucket counter
Minimizing the impact of faults
Retrying the call
Avoiding cascading failures
Circuit breaker
Bulkhead
Geodes
Integrating your system
Pipes and filters pattern
Competing consumers
Transitioning from legacy systems
Anti-corruption layer
Strangler pattern
Achieving performance at scale
CQRS and event sourcing
Command-query responsibility segregation
Command-query separation
Event sourcing
Caching
Updating caches
Write-through approach
Write-behind approach
Cache-aside
Deploying your system
The sidecar pattern
Deploying a service with tracing and a reverse proxy using Envoy
Zero-downtime deployments
Blue-green deployments
Canary releases
External configuration store
Managing your APIs
API gateways
Summary
Questions
Further reading
Leveraging C++ Language Features Chevron down icon Chevron up icon
Design Patterns and C++ Chevron down icon Chevron up icon
Building and Packaging Chevron down icon Chevron up icon
Section 3: Architectural Quality Attributes Chevron down icon Chevron up icon
Writing Testable Code Chevron down icon Chevron up icon
Continuous Integration and Continuous Deployment Chevron down icon Chevron up icon
Security in Code and Deployment Chevron down icon Chevron up icon
Performance Chevron down icon Chevron up icon
Section 4: Cloud-Native Design Principles Chevron down icon Chevron up icon
Service-Oriented Architecture Chevron down icon Chevron up icon
Designing Microservices Chevron down icon Chevron up icon
Containers Chevron down icon Chevron up icon
Cloud-Native Design Chevron down icon Chevron up icon
Assessments Chevron down icon Chevron up icon
About Packt Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Most Recent
Rating distribution
Full star icon Full star icon Full star icon Full star icon Half star icon 4.4
(13 Ratings)
5 star 76.9%
4 star 7.7%
3 star 0%
2 star 7.7%
1 star 7.7%
Filter icon Filter
Most Recent

Filter reviews by




sri vasala Aug 17, 2022
Full star icon Full star icon Full star icon Full star icon Full star icon 5
love the content. Love the simplicity and specialization in explanation. Its easily digestible for me. Serves as a good reference as well
Amazon Verified review Amazon
bluemarlin Jul 11, 2022
Full star icon Empty star icon Empty star icon Empty star icon Empty star icon 1
Page 144 talks about adjacent_find doing the same as a double loop where the outer loop at the ith element is being compared to the inner loop elements going from i+1 to the end iter -- these are two different things. The book promises to cover topics and then just glosses over in a cursory manner - rubbish book
Amazon Verified review Amazon
Mag. Herwig Huber Dec 08, 2021
Full star icon Full star icon Empty star icon Empty star icon Empty star icon 2
The book was written so erratically that I put it aside after 50 pages. In terms of content, an attempt is made to convey a lot of opinion and poorly founded knowledge. Too bad.The authors may be good developers, but writing books is a different story.
Amazon Verified review Amazon
J. W. Semmelink Aug 22, 2021
Full star icon Full star icon Full star icon Full star icon Full star icon 5
If you purchase this book hoping for a 'how to' guide with lots of c++ code, you may be disappointed. There is actually not much code examples in the book.Therefore my headline: it's about the architecture. This book summarises the best known patterns, idioms, and practices in system design. The first chapters give a thorough revision of basic principles like SOLID, coupling, cohesion, monolith or service based architecture, etc. Many third party products and external c++ libraries are mentioned to design modern c++ systems in line with current practices. If you fell behind with system design and wish to catch up, or are new to it and need a great overview of the field, this book will be of great value.The authors cover a lot of material. The book does not go in depth on each of the many topics. The authors make the reader aware that a design principle, product, library or technique exists, and provide links to more information in a further reading section at the end of each chapter.Highly recommended reading as a reference guide or introduction to c++ system design.
Amazon Verified review Amazon
Cale Dunlap Jul 15, 2021
Full star icon Full star icon Full star icon Full star icon Full star icon 5
This book is full of useful modern practices when it comes to distributed systems development, continuous integration/deployment, testing, and some of the modern language features that come with C++20. The organization I work within doesn't use a lot of the STL, which is where quite a bit of C++20 gets its power, and my background and experience hover around the C++11 era. However the other parts about cloud-native design, CI/CD, microservices, etc. are very useful to anybody who is building a large system in C++ and wants to take advantage of some of the paradigms not typically associated with C++ development.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.