Discover millions of ebooks, audiobooks, and so much more with a free trial

From $11.99/month after trial. Cancel anytime.

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

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

Rating: 0 out of 5 stars

()

Read preview

About this ebook

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.

LanguageEnglish
Release dateApr 23, 2021
ISBN9781789612462
Software Architecture with C++: Design modern systems using effective architecture concepts, design patterns, and techniques with C++20

Related to Software Architecture with C++

Related ebooks

Computers For You

View More

Related articles

Reviews for Software Architecture with C++

Rating: 0 out of 5 stars
0 ratings

0 ratings0 reviews

What did you think?

Tap to rate

Review must be at least 10 words

    Book preview

    Software Architecture with C++ - Adrian Ostrowski

    Software Architecture with C++

    Software Architecture

    with C++

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

    Adrian Ostrowski

    Piotr Gaczkowski

    BIRMINGHAM - MUMBAI

    Software Architecture with C++

    Copyright © 2021 Packt Publishing

    All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews.

    Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the authors, nor Packt Publishing or its dealers and distributors, will be held liable for any damages caused or alleged to have been caused directly or indirectly by this book.

    Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.

    Group Product Manager: Aaron Lazar

    Publishing Product Manager: Richa Tripathi

    Senior Editor: Rohit Singh

    Content Development Editor: Kinnari Chohan

    Technical Editor: Gaurav Gala

    Copy Editor: Safis Editing

    Project Coordinator: Deeksha Thakkar

    Proofreader: Safis Editing

    Indexer: Priyanka Dhadke

    Production Designer: Aparna Bhagat

    First published: April 2021

    Production reference: 2210421

    Published by Packt Publishing Ltd.

    Livery Place

    35 Livery Street

    Birmingham

    B3 2PB, UK.

    ISBN 978-1-83855-459-0

    www.packt.com

    To Agnieszka, for all her love and support

    To Mateusz, for being a great mentor

    To my parents, for sparking the curiosity in me

    To my friends, for not being hidden and for all they do

    – Adrian Ostrowski

    To Emilia, who tolerated me when I was writing this book; my parents, who encouraged me to learn coding; the mastermind group members who cheered me on in this journey; Hackerspace Trójmiasto, for positive vibes; IOD, for reminding me that I love writing; 255, for the workouts; and all the friends who shared the journey with me. Love you!

    – Piotr Gaczkowski

    Contributors

    About the authors

    Adrian Ostrowski is a modern C++ enthusiast interested in the development of both the C++ language itself and the high-quality code written in it. A lifelong learner with over a decade of experience in the IT industry and more than 8 years of experience with C++ specifically, he's always eager to share his knowledge. His past projects range from parallel computing, through fiber networking, to working on a commodity exchange's trading system. Currently, he's one of the architects of Intel and Habana's integration with machine learning frameworks.

    In his spare time, Adrian used to promote music bands together with Piotr and has learned how to fly a glider. Currently, he likes riding his bicycle, going to music events, and browsing memes.

    Piotr Gaczkowski has more than 10 years of experience in programming and practicing DevOps and uses his skills to improve people's lives. He likes building simple solutions to human problems, organizing cultural events, and teaching fellow professionals. Piotr is keen on automating boring activities and using his experience to share knowledge by conducting courses and writing articles about personal growth and remote work. 

    He has worked in the IT industry both in full-time positions and as a freelancer, but his true passion is music. When not making his skills useful at work, you can find him building communities.

    About the reviewer

    Andrey Gavrilin is a senior software engineer working for an international company that provides treasury management cloud solutions. He has an MSc degree in engineering (industrial automation) and has worked in different areas such as accounting and staffing, road data bank, web and Linux distribution development, and fintech. His interests include mathematics, electronics, embedded systems, full-stack web development, retro gaming, and retro programming.

    Table of Contents

    Title Page

    Copyrights

    Software Architecture with C++

    Dedication

    Contributors

    About the authors

    About the reviewer

    Preface

    Who this book is for

    What this book covers

    To get the most out of this book

    Download the example code files

    Download the color images

    Conventions used

    Get in touch

    Reviews

    Section 1: Concepts and Components of Software Architecture

    Importance of Software Architecture and Principles of Great Design

    Technical requirements

    Understanding software architecture

    Different ways to look at architecture

    Learning the importance of proper architecture

    Software decay

    Accidental architecture

    Exploring the fundamentals of good architecture

    Architecture context

    Stakeholders

    Business and technical environments

    Developing architecture using Agile principles

    Domain-driven design

    The philosophy of C++

    Following the SOLID and DRY principles

    Single responsibility principle

    Open-closed principle

    Liskov substitution principle

    Interface segregation principle

    Dependency inversion principle

    The DRY rule

    Coupling and cohesion

    Coupling

    Cohesion

    Summary

    Questions

    Further reading

    Architectural Styles

    Technical requirements

    Deciding between stateful and stateless approaches

    Stateless and stateful services

    Understanding monoliths—why they should be avoided, and recognizing exceptions

    Understanding services and microservices

    Microservices

    Benefits and disadvantages of microservices

    Characteristics of microservices

    Microservices and other architectural styles

    Scaling microservices

    Transitioning to microservices

    Exploring event-based architecture

    Common event-based topologies

    Event sourcing

    Understanding layered architecture

    Backends for Frontends

    Learning module-based architecture

    Summary

    Questions

    Further reading

    Functional and Nonfunctional Requirements

    Technical requirements documentation from sources, you must have

    Understanding the types of requirements

    Functional requirements

    Nonfunctional requirements

    Quality attributes

    Constraints

    Recognizing architecturally significant requirements

    Indicators of architectural significance

    Hindrances in recognizing ASRs and how to deal with them

    Gathering requirements from various sources

    Knowing the context

    Knowing existing documentation

    Knowing your stakeholders

    Gathering requirements from stakeholders

    Documenting requirements

    Documenting the context

    Documenting the scope

    Documenting functional requirements

    Documenting nonfunctional requirements

    Managing the version history of your documentation

    Documenting requirements in Agile projects 

    Other sections

    Documenting architecture

    Understanding the 4+1 model

    Understanding the C4 model

    Documenting architecture in Agile projects

    Choosing the right views to document

    Functional view

    Information view

    Concurrency view

    Development view

    Deployment and operational views

    Generating documentation

    Generating requirements documentation

    Generating diagrams from code

    Generating (API) documentation from code

    Summary

    Questions

    Further reading

    Section 2: The Design and Development of C++ Software

    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

    Technical requirements

    Designing great APIs

    Leveraging RAII

    Specifying the interfaces of containers in C++

    Using pointers in interfaces

    Specifying preconditions and postconditions

    Leveraging inline namespaces

    Leveraging std::optional

    Optional function parameters

    Optional function return values

    Optional class members

    Writing declarative code

    Showcasing a featured items gallery

    Introducing standard ranges

    Reducing memory overhead and increasing performance using ranges

    Moving computations at compile time

    Helping the compiler help you by using const

    Leveraging the power of safe types

    Constraining template parameters

    Writing modular C++

    Summary

    Questions

    Further reading

    Design Patterns and C++

    Technical requirements

    Writing idiomatic C++

    Automating scope exit actions using RAII guards

    Managing copyability and movability

    Implementing non-copyable types

    Adhering to the rules of three and five

    Adhering to the rule of zero

    Using hidden friends

    Providing exception safety using the copy-and-swap idiom

    Writing niebloids

    Policy-based design idiom

    Curiously recurring template pattern

    Knowing when to use dynamic versus static polymorphism

    Implementing static polymorphism

    Interlude – using type erasure

    Creating objects

    Using factories

    Using factory methods

    Using factory functions

    Choosing the return type of a factory

    Using factory classes

    Using builders

    Building with composites and prototypes

    Tracking state and visiting objects in C++

    Dealing with memory efficiently

    Reducing dynamic allocations using SSO/SOO

    Saving memory by herding COWs

    Leveraging polymorphic allocators

    Using memory arenas

    Using the monotonic memory resource

    Using pool resources

    Writing your own memory resource

    Ensuring there are no unexpected allocations

    Winking out memory

    Summary

    Questions

    Further reading

    Building and Packaging

    Technical requirements

    Getting the most out of compilers

    Using multiple compilers

    Reducing build times

    Using a fast compiler

    Rethinking templates

    Leveraging tools

    Finding potential code issues

    Using compiler-centric tools

    Abstracting the build process

    Introducing CMake

    Creating CMake projects

    Distinguishing between CMake directory variables

    Specifying CMake targets

    Specifying the output directories

    Using generator expressions

    Using external modules

    Fetching dependencies

    Using find scripts

    Writing find scripts

    Using the Conan package manager

    Preparing Conan profiles

    Specifying Conan dependencies

    Installing Conan dependencies

    Using Conan targets from CMake

    Adding tests

    Reusing quality code

    Installing

    Exporting

    Using CPack

    Packaging using Conan

    Creating the conanfile.py script

    Testing our Conan package

    Adding Conan packaging code to our CMakeLists

    Summary

    Questions

    Further reading

    Section 3: Architectural Quality Attributes

    Writing Testable Code

    Technical requirements

    Why do you test code?

    The testing pyramid

    Non-functional testing

    Regression testing

    Root cause analysis

    The groundwork for further improvement

    Introducing testing frameworks

    GTest examples

    Catch2 examples

    CppUnit examples

    Doctest examples

    Testing compile-time code

    Understanding mocks and fakes

    Different test doubles

    Other uses for test doubles

    Writing test doubles

    GoogleMock example

    Trompeloeil example

    Test-driven class design

    When tests and class design clash

    Defensive programming

    The boring refrain – write your tests first

    Automating tests for continuous integration/continuous deployment

    Testing the infrastructure

    Testing with Serverspec

    Testing with Testinfra

    Testing with Goss

    Summary

    Questions

    Further reading

    Continuous Integration and Continuous Deployment

    Technical requirements

    Understanding CI

    Release early, release often

    Merits of CI

    Gating mechanism

    Implementing the pipeline with GitLab

    Reviewing code changes

    Automated gating mechanisms

    Code review – the manual gating mechanism

    Different approaches to a code review

    Using pull requests (merge requests) for a code review

    Exploring test-driven automation

    Behavior-driven development

    Writing tests for CI

    Continuous testing

    Managing deployment as code

    Using Ansible

    How Ansible fits with the CI/CD pipeline

    Using components to create deployment code

    Building deployment code

    Building a CD pipeline

    Continuous deployment and continuous delivery

    Building an example CD pipeline

    Using immutable infrastructure

    What is immutable infrastructure?

    The benefits of immutable infrastructure

    Building instance images with Packer

    Orchestrating the infrastructure with Terraform

    Summary

    Questions

    Further reading

    Security in Code and Deployment

    Technical requirements

    Checking the code security

    Security-conscious design

    Making interfaces easy to use and hard to misuse

    Enabling automatic resource management

    Drawbacks of concurrency and how to deal with it

    Secure coding, the guidelines, and GSL

    Defensive coding, validating everything

    The most common vulnerabilities

    Checking whether the dependencies are secure

    Common Vulnerabilities and Exposures

    Automated scanners

    Automated dependency upgrade management

    Hardening your code

    Security-oriented memory allocator

    Automated checks

    Compiler warnings

    Static analysis

    Dynamic analysis

    Valgrind and Application Verifier

    Sanitizers

    Fuzz-testing

    Process isolation and sandboxing

    Hardening your environment

    Static versus dynamic linking

    Address space layout randomization

    DevSecOps

    Summary

    Questions

    Further reading

    Performance

    Technical requirements

    Measuring performance

    Performing accurate and meaningful measurements

    Leveraging different types of measuring tools

    Using microbenchmarks

    Setting up Google Benchmark

    Writing your first microbenchmark

    Passing arbitrary arguments to a microbenchmark

    Passing numeric arguments to a microbenchmark

    Generating the passed arguments programmatically

    Choosing what to microbenchmark and optimize

    Creating performance tests using benchmarks

    Profiling

    Choosing the type of profiler to use

    Preparing the profiler and processing the results

    Analyzing the results

    Tracing

    Correlation IDs

    Helping the compiler generate performant code

    Optimizing whole programs

    Optimizing based on real-world usage patterns

    Writing cache-friendly code

    Designing your code with data in mind

    Parallelizing computations

    Understanding the differences between threads and processes

    Using the standard parallel algorithms

    Parallelizing computations using OpenMP and MPI

    Using coroutines

    Distinguishing between cppcoro utilities

    Looking under the hood of awaitables and coroutines

    Summary

    Questions

    Further reading

    Section 4: Cloud-Native Design Principles

    Service-Oriented Architecture

    Technical requirements

    Understanding Service-Oriented Arcitecture

    Implementation approaches

    Enterprise Service Bus

    Web services

    Benefits and disadvantages of web services

    Messaging and streaming

    Message queues

    Message brokers

    Cloud computing

    Microservices

    Benefits of Service-Oriented Architecture

    Challenges with SOA

    Adopting messaging principles

    Low-overhead messaging systems

    MQTT

    ZeroMQ

    Brokered messaging systems

    Using web services

    Tools for debugging web services

    XML-based web services

    XML-RPC

    Relationship to SOAP

    SOAP

    WSDL

    UDDI

    SOAP libraries

    JSON-based web services

    JSON-RPC

    REpresentational State Transfer (REST)

    Description languages

    OpenAPI

    RAML

    API Blueprint

    RSDL

    Hypermedia as the Engine of Application State

    REST in C++

    GraphQL

    Leveraging managed services and cloud providers

    Cloud computing as an extension of SOA

    Using API calls directly

    Using API calls through a CLI tool

    Using third-party tools that interact with the Cloud API

    Accessing the cloud API

    Using the cloud CLI

    Using tools that interact with the Cloud API

    Cloud-native architecture

    Summary

    Questions

    Further reading

    Designing Microservices

    Technical requirements

    Diving into microservices

    The benefits of microservices

    Modularity

    Scalability

    Flexibility

    Integration with legacy systems

    Distributed development

    Disadvantages of microservices

    Reliance on a mature DevOps approach

    Harder to debug

    Additional overhead

    Design patterns for microservices

    Decomposition patterns

    Decomposition by business capability

    Decomposition by subdomain

    Database per service pattern

    Deployment strategies

    Single service per host

    Multiple services per host

    Observability patterns

    Log aggregation

    Application metrics

    Distributed tracing

    Health check APIs

    Building microservices

    Outsourcing memory management

    Memcached

    Redis

    Which in-memory cache is better?

    Outsourcing storage

    Outsourcing computing

    Observing microservices

    Logging

    Logging with microservices

    Logging in C++ with spdlog

    Unified logging layer

    Logstash

    Filebeat

    Fluentd

    Fluent Bit

    Vector

    Log aggregation

    Elasticsearch

    Loki

    Log visualization

    Kibana

    Grafana

    Monitoring

    Tracing

    OpenTracing

    Jaeger

    OpenZipkin

    Integrated observability solutions

    Connecting microservices

    Application programming interfaces (APIs)

    Remote procedure calls

    Apache Thrift

    gRPC

    Scaling microservices

    Scaling a single service per host deployment

    Scaling multiple services per host deployment

    Summary

    Questions

    Further reading

    Containers

    Technical requirements

    Reintroducing containers

    Exploring the container types

    The rise of microservices

    Choosing when to use containers

    The benefits of containers

    The disadvantages of containers

    Building containers

    Container images explained

    Using Dockerfiles to build an application

    Naming and distributing images

    Compiled applications and containers

    Targeting multiple architectures with manifests

    Alternative ways to build application containers

    Buildah

    Ansible-bender

    Others

    Integrating containers with CMake

    Configuring the Dockerfile with CMake

    Integrating containers with CMake

    Testing and integrating containers

    Runtime libraries inside containers

    Alternative container runtimes

    Understanding container orchestration

    Self-hosted solutions

    Kubernetes

    Docker Swarm

    Nomad

    OpenShift

    Managed services

    AWS ECS

    AWS Fargate

    Azure Service Fabric

    Summary

    Questions

    Further reading

    Cloud-Native Design

    Technical requirements

    Understanding cloud-native

    Cloud-Native Computing Foundation

    Cloud as an operating system

    Load balancing and service discovery

    Reverse proxies

    Service Discovery

    Using Kubernetes to orchestrate cloud-native workloads

    Kubernetes structure

    Control plane

    Worker nodes

    Possible approaches to deploying Kubernetes

    Understanding the Kubernetes concepts

    Declarative approach

    Kubernetes networking

    Container-to-container communication

    Pod-to-pod communication

    Pod-to-service communication

    External-to-internal communication

    When is using Kubernetes a good idea?

    Observability in distributed systems

    How tracing differs from logging

    Choosing a tracing solution

    Jaeger and OpenTracing

    Zipkin

    Instrumenting an application with OpenTracing

    Connecting services with a service mesh

    Introducing a service mesh

    Service mesh solutions

    Istio

    Envoy

    Linkerd

    Consul service mesh

    Going GitOps

    The principles of GitOps

    Declarative description

    The system's state versioned in Git

    Auditable

    Integrated with established components

    Configuration drift prevention

    The benefits of GitOps

    Increased productivity

    Better developer experience

    Higher stability and reliability

    Improved security

    GitOps tools

    FluxCD

    ArgoCD

    Jenkins X

    Summary

    Questions

    Further reading

    Appendix A

    Designing data storage

    Which NoSQL technology should I use?

    Serverless architecture

    Communication and culture

    DevOps

    Assessments

    Chapter 1

    Chapter 2

    Chapter 3

    Chapter 4

    Chapter 5

    Chapter 6

    Chapter 7

    Chapter 8

    Chapter 9

    Chapter 10

    Chapter 11

    Chapter 12

    Chapter 13

    Chapter 14

    Chapter 15

    About Packt

    Why subscribe?

    Other Books You May Enjoy

    Packt is searching for authors like you

    Leave a review - let other readers know what you think

    Preface

    Modern C++ allows you to write high-performing applications in a high-level language without sacrificing readability and maintainability. There's more to software architecture than just language, though. We're going to show you how to design and build applications that are robust and scalable and that perform well.

    Complete with step-by-step explanations of essential concepts, practical examples, and self-assessment questions, you will begin by understanding the importance of architecture, looking at a case study of an actual application.

    You'll learn how to use established design patterns at the level of a single application, exploring how to make your applications robust, secure, performant, and maintainable. You'll then build higher-level services that connect multiple single applications using patterns such as service-oriented architecture, microservices, containers, and serverless technology.

    By the end of this book, you will be able to build distributed services using modern C++ and associated tools to deliver solutions that your clients will recommend.

    Are you interested in becoming a software architect or looking to learn more about modern trends in architecture? If so, this book should help you!

    Who this book is for

    Developers working with modern C++ will be able to put their knowledge to work with this practical guide to software architecture. The book takes a hands-on approach to implementation and associated methodologies that will have you up and running and productive in no time.

    What this book covers

    Chapter 1, Importance of Software Architecture and Principles of Great Design, looks at why we design software in the first place.

    Chapter 2, Architectural Styles, covers the different approaches you can take in terms of architecture.

    Chapter 3, Functional and Nonfunctional Requirements, explores understanding the needs of clients.

    Chapter 4, Architectural and System Design, is all about creating effective software solutions.

    Chapter 5, Leveraging C++ Language Features, gets you speaking native C++.

    Chapter 6, Design Patterns and C++, focuses on modern C++ idioms and useful code constructs.

    Chapter 7, Building and Packaging, is about getting code to production.

    Chapter 8, Writing Testable Code, teaches you how to find bugs before the clients do.

    Chapter 9, Continuous Integration and Continuous Deployment, introduces the modern way of automating software releases.

    Chapter 10, Security in Code and Deployment, is where you will learn how to make sure it's hard to break your systems.

    Chapter 11, Performance, looks at performance (of course!). C++ should be fast – can it be even faster?

    Chapter 12, Service-Oriented Architecture, sees you building systems based on services.

    Chapter 13, Designing Microservices, focuses on doing one thing only – designing microservices.

    Chapter 14, Containers, gives you a unified interface to build, package, and run applications.

    Chapter 15, Cloud-Native Design, goes beyond traditional infrastructure to explore cloud-native design.

    To get the most out of this book

    The code examples in this book are mostly written for GCC 10. They should work with Clang or Microsoft Visual C++ as well, though certain features from C++20 may be missing in older versions of the compilers. To get a development environment as close to the authors' as possible, we advise you to use Nix (https://nixos.org/download.html) and direnv (https://direnv.net/) in a Linux-like environment. These two tools should configure the compilers and supporting packages for you if you run direnv allow in a directory containing examples.

    Without Nix and direnv, we can't guarantee that the examples will work correctly. If you're on macOS, Nix should work just fine. If you're on Windows, the Windows Subsystem for Linux 2 is a great way to have a Linux development environment with Nix.

    To install both tools, you have to run the following:

    # Install Nix

    curl -L https://nixos.org/nix/install | sh

    # Configure Nix in the current shell

    . $HOME/.nix-profile/etc/profile.d/nix.sh

    # Install direnv

    nix-env -i direnv

    # Download the code examples

    git clone https://github.com/PacktPublishing/Hands-On-Software-Architecture-with-Cpp.git

    # Change directory to the one with examples

    cd Hands-On-Software-Architecture-with-Cpp

    # Allow direnv and Nix to manage your development environment

    direnv allow

    After executing the preceding command, Nix should download and install all the necessary dependencies. This might take a while but it helps to ensure we're using exactly the same tools.

    If you are using the digital version of this book, we advise you to type the code yourself or access the code via the GitHub repository (link available in the next section). Doing so will help you avoid any potential errors related to the copying and pasting of code.

    Download the example code files

    You can download the example code files for this book from GitHub at https://github.com/PacktPublishing/Software-Architecture-with-Cpp. In case there's an update to the code, it will be updated on the existing GitHub repository.

    We also have other code bundles from our rich catalog of books and videos available at https://github.com/PacktPublishing/. Check them out!

    Download the color images

    We also provide a PDF file that has color images of the screenshots/diagrams used in this book. You can download it here: https://static.packt-cdn.com/downloads/9781838554590_ColorImages.pdf.

    Conventions used

    There are a number of text conventions used throughout this book.

    CodeInText: Indicates code words in text, database table names, folder names, filenames, file extensions, pathnames, dummy URLs, user input, and Twitter handles. Here is an example: The first two fields (openapi and info) are metadata describing the document.

    A block of code is set as follows:

    using namespace CppUnit;

    using namespace std;

    Bold: Indicates a new term, an important word, or words that you see onscreen. For example, words in menus or dialog boxes appear in the text like this. Here is an example: Select System info from the Administration panel.

    Warnings or important notes appear like this.

    Tips and tricks appear like this.

    Get in touch

    Feedback from our readers is always welcome.

    General feedback: If you have questions about any aspect of this book, mention the book title in the subject of your message and email us at [email protected].

    Errata: Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you have found a mistake in this book, we would be grateful if you would report this to us. Please visit www.packtpub.com/support/errata, selecting your book, clicking on the Errata Submission Form link, and entering the details.

    Piracy: If you come across any illegal copies of our works in any form on the Internet, we would be grateful if you would provide us with the location address or website name. Please contact us at [email protected] with a link to the material.

    If you are interested in becoming an author: If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, please visit authors.packtpub.com.

    Reviews

    Please leave a review. Once you have read and used this book, why not leave a review on the site that you purchased it from? Potential readers can then see and use your unbiased opinion to make purchase decisions, we at Packt can understand what you think about our products, and our authors can see your feedback on their book. Thank you!

    For more information about Packt, please visit packt.com.

    Section 1: Concepts and Components of Software Architecture

    This section introduces you to the basics of software architecture, demonstrating effective approaches to its design and documentation.

    This section contains the following chapters:

    Chapter 1, Importance of Software Architecture and Principles of Great Design

    Chapter 2, Architectural Styles 

    Chapter 3, Functional and Nonfunctional Requirements

    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

    To play with the code from this chapter, you'll need the following:

    A Git client for checking out the repositories given shortly.

    A C++20-compliant compiler to compile all the snippets. Most of them are written in C++11/14/17, but concept support is required to experiment with the few that touch the subject.

    GitHub link for code snippets: https://github.com/PacktPublishing/Software-Architecture-with-Cpp/tree/master/Chapter01.

    GitHub link for GSL: https://github.com/Microsoft/GSL

    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

    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

    Enjoying the preview?
    Page 1 of 1