Software Architecture with C++: Design modern systems using effective architecture concepts, design patterns, and techniques with C++20
By Adrian Ostrowski and Piotr Gaczkowski
()
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.
Related to Software Architecture with C++
Related ebooks
Node.js for Beginners: A comprehensive guide to building efficient, full-featured web applications with Node.js Rating: 0 out of 5 stars0 ratingsMastering Linux Administration: Take your sysadmin skills to the next level by configuring and maintaining Linux systems Rating: 0 out of 5 stars0 ratingsC# Interview Guide: Boost your confidence with answers to hundreds of secret interview questions Rating: 0 out of 5 stars0 ratingsGet Your Hands Dirty on Clean Architecture: Build 'clean' applications with code examples in Java Rating: 0 out of 5 stars0 ratingsArchitecting ASP.NET Core Applications: An atypical design patterns guide for .NET 8, C# 12, and beyond Rating: 0 out of 5 stars0 ratingsData Engineering with Python: Work with massive datasets to design data models and automate data pipelines using Python Rating: 0 out of 5 stars0 ratingsSystems Programming with C# and .NET: Building robust system solutions with C# 12 and .NET 8 Rating: 0 out of 5 stars0 ratingsUnlocking the Power of Auto-GPT and Its Plugins: Implement, customize, and optimize Auto-GPT for building robust AI applications Rating: 0 out of 5 stars0 ratingsBuilding Your Own JavaScript Framework: Architect extensible and reusable framework systems Rating: 0 out of 5 stars0 ratingsRAG-Driven Generative AI: Build custom retrieval augmented generation pipelines with LlamaIndex, Deep Lake, and Pinecone Rating: 0 out of 5 stars0 ratingsSoftware Architecture Foundation: CPSA Foundation® Exam Preparation Rating: 0 out of 5 stars0 ratingsBeginning DotNetNuke Skinning and Design Rating: 0 out of 5 stars0 ratings“Exploring Computer Systems: From Fundamentals to Advanced Concepts”: GoodMan, #1 Rating: 0 out of 5 stars0 ratingsUltimate Git and GitHub for Modern Software Development Rating: 0 out of 5 stars0 ratingsHands-On System Programming with C++: Build performant and concurrent Unix and Linux systems with C++17 Rating: 0 out of 5 stars0 ratingsKotlin Quick Start Guide: Core features to get you ready for developing applications Rating: 0 out of 5 stars0 ratingsApps and Services with .NET 8: Build practical projects with Blazor, .NET MAUI, gRPC, GraphQL, and other enterprise technologies Rating: 0 out of 5 stars0 ratingsWAN Survival Guide: Strategies for VPNs and Multiservice Networks Rating: 0 out of 5 stars0 ratingsFull Stack Quarkus and React: Hands-on full stack web development with Java, React, and Kubernetes Rating: 0 out of 5 stars0 ratingsLearn React with TypeScript: A beginner's guide to reactive web development with React 18 and TypeScript Rating: 0 out of 5 stars0 ratingsDeep Learning for Time Series Cookbook: Use PyTorch and Python recipes for forecasting, classification, and anomaly detection Rating: 0 out of 5 stars0 ratings
Computers For You
The Invisible Rainbow: A History of Electricity and Life Rating: 5 out of 5 stars5/5Elon Musk Rating: 4 out of 5 stars4/5CompTIA Security+ Get Certified Get Ahead: SY0-701 Study Guide Rating: 5 out of 5 stars5/5The Innovators: How a Group of Hackers, Geniuses, and Geeks Created the Digital Revolution Rating: 4 out of 5 stars4/5Alan Turing: The Enigma: The Book That Inspired the Film The Imitation Game - Updated Edition Rating: 4 out of 5 stars4/5Slenderman: Online Obsession, Mental Illness, and the Violent Crime of Two Midwestern Girls Rating: 4 out of 5 stars4/5Procreate for Beginners: Introduction to Procreate for Drawing and Illustrating on the iPad Rating: 0 out of 5 stars0 ratingsDeep Search: How to Explore the Internet More Effectively Rating: 5 out of 5 stars5/5The ChatGPT Millionaire Handbook: Make Money Online With the Power of AI Technology Rating: 4 out of 5 stars4/5Standard Deviations: Flawed Assumptions, Tortured Data, and Other Ways to Lie with Statistics Rating: 4 out of 5 stars4/5The Hacker Crackdown: Law and Disorder on the Electronic Frontier Rating: 4 out of 5 stars4/5Excel 101: A Beginner's & Intermediate's Guide for Mastering the Quintessence of Microsoft Excel (2010-2019 & 365) in no time! Rating: 0 out of 5 stars0 ratingsMastering ChatGPT: 21 Prompts Templates for Effortless Writing Rating: 4 out of 5 stars4/5Uncanny Valley: A Memoir Rating: 4 out of 5 stars4/5101 Awesome Builds: Minecraft® Secrets from the World's Greatest Crafters Rating: 4 out of 5 stars4/5How to Create Cpn Numbers the Right way: A Step by Step Guide to Creating cpn Numbers Legally Rating: 4 out of 5 stars4/5SQL QuickStart Guide: The Simplified Beginner's Guide to Managing, Analyzing, and Manipulating Data With SQL Rating: 4 out of 5 stars4/5CompTIA IT Fundamentals (ITF+) Study Guide: Exam FC0-U61 Rating: 0 out of 5 stars0 ratingsManaging Humans: Biting and Humorous Tales of a Software Engineering Manager Rating: 4 out of 5 stars4/5Dark Aeon: Transhumanism and the War Against Humanity Rating: 5 out of 5 stars5/5The Mega Box: The Ultimate Guide to the Best Free Resources on the Internet Rating: 4 out of 5 stars4/5Everybody Lies: Big Data, New Data, and What the Internet Can Tell Us About Who We Really Are Rating: 4 out of 5 stars4/5The Best Hacking Tricks for Beginners Rating: 4 out of 5 stars4/5
Reviews for Software Architecture with C++
0 ratings0 reviews
Book preview
Software Architecture with C++ - Adrian Ostrowski
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