C++ Software Design (Sixth Early Release) (Klaus Iglberger) (Z-Library)

Author: Klaus Iglberger

技术

Good software design is essential for the success of your project, but designing software is hard to do. You need to have a deep understanding of the consequences of design decisions and a good overview of available design alternatives. With this book, experienced C++ developers will get a thorough, practical, and unparalleled overview of software design with this modern language. C++ trainer and consultant Klaus Iglberger explains how you can manage dependencies and abstractions, improve changeability and extensibility of software entities, and apply and implement modern design patterns to help you take advantage of today's possibilities. Design is the most essential aspect of a software project because it impacts the software's most important properties: maintainability, changeability, and extensibility. Learn how to evaluate your code with respect to software design Understand what software design is, including design goals such as changeability and extensibility Explore the advantages and disadvantages of each design approach Learn how design patterns help solve problems and express intent Choose the right form of a design pattern to gain most of its advantages

📄 File Format: PDF
💾 File Size: 7.0 MB
76
Views
0
Downloads
0.00
Total Donations

📄 Text Preview (First 20 pages)

ℹ️

Registered users can read the full content for free

Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.

📄 Page 1
(This page has no text content)
📄 Page 2
C++ Software Design Design Principles and Patterns for High-Quality Software With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles. Klaus Iglberger
📄 Page 3
C++ Software Design by Klaus Iglberger Copyright © 2023 Klaus Iglberger. All rights reserved. Printed in the United States of America. Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472. O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com. Editors: Amanda Quinn and Shira Evans Production Editor: Kate Galloway Interior Designer: David Futato Cover Designer: Karen Montgomery Illustrator: Kate Dullea November 2022: First Edition Revision History for the Early Release 2021-10-19: First Release 2021-12-15: Second Release 2022-02-03: Third Release 2022-03-23: Fourth Release 2022-04-21: Fifth Release
📄 Page 4
2022-06-21: Sixth Release See http://oreilly.com/catalog/errata.csp?isbn=9781098113162 for release details. The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. C++ Software Design, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc. The views expressed in this work are those of the author, and do not represent the publisher’s views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights. 978-1-098-11309-4
📄 Page 5
Chapter 1. Software Design and Design Principles A NOTE FOR EARLY RELEASE READERS With Early Release ebooks, you get books in their earliest form—the author’s raw and unedited content as they write—so you can take advantage of these technologies long before the official release of these titles. This will be the 1st chapter of the final book. Please note that the GitHub repo will be made active later on. If you have comments about how we might improve the content and/or examples in this book, or if you notice missing material within this chapter, please reach out to the editor at sevans@oreilly.com. What is software design? And why should you care about it? In this chapter I will set the stage for this book on software design. I will explain software design in general, help you understand why it is vitally important for the success of a project and why it is the one thing that you should get right. But you will also see that software design is hard. Very hard. In fact, it is the hardest part of software development. Therefore I will also explain several software design principles, principles that will help you to stay on the right path. In “Guideline 1: Understand the Importance of Software Design”, I will focus on the big picture and explain to you that software is expected to change. Consequently, software should be designed to cope with change. However, it is much easier said than done, since in reality coupling and dependencies make our developer life so much harder. That problem is addressed by software design. I will introduce software design as the art of
📄 Page 6
managing dependencies and abstractions and thus as an essential part of writing software. In “Guideline 2: Design for Change”, I will explicitly address coupling and dependencies and help you understand how to design for change. For that purpose, I will introduce both the Single-Responsibility Principle (SRP) and the Don’t Repeat Yourself (DRY) principle, which help you to achieve this goal. In “Guideline 3: Separate Interfaces to Avoid Artificial Coupling”, I will expand the discussion about coupling and will specifically address coupling via interfaces. I will also introduce the Interface Segregation Principle (ISP) as a means to reduce artificial coupling induced by interfaces. In “Guideline 4: Design for Testability”, I will focus on testability issues that arise as a result of artificial coupling. In particular, I will raise the question on how to test a private member function and demonstrate that the one true solution is a consequent application of separation of concerns. In “Guideline 5: Design for Extension”, I will address an important kind of change: extensions. Just as code should be easy to change, it should also be easy to extend. I will give you an idea how to achieve that goal and I will demonstrate the value of the Open-Closed Principle (OCP). Guideline 1: Understand the Importance of Software Design If I were to ask you “Which code properties are most important to you?”, you would - after some thinking - probably say things like: “Readability. Testability. Maintainability. Extensibility. Reusability. Scalability.” And I would completely agree. But now, if I were to ask you how to achieve these goals, there is a good chance that you would start to list some C++ features: RAII. Algorithms. Lambdas. Modules… Yes, C++ offers a lot of features. A lot! Approximately half of the almost 2000 pages of the printed C++ standard are devoted to explaining language
📄 Page 7
mechanics and features. And since the release of C++11, there is the explicit promise that there will be more: Every three years, the C++ standardization committee blesses us with a new C++ standard, that ships with additional, brand new features. Knowing that, it does not come as a big surprise that in the C++ community there is a very strong emphasis on features and language mechanics. Most books, talks, and blogs are focused on features, new libraries and language details. It almost feels as if features are the most important thing about programming in C++ and crucial for the success of a C++ project. But sadly, they are not. Neither the knowledge about all the features nor the choice of the C++ standard is responsible for the success of a project. No, you should not expect features to save your project. On the contrary: A project can be very successful even if it uses an older C++ standard, and even if only a subset of the available features are used. Leaving aside the human aspects of software development, much more important than features is the question about the overall structure of a software project. This structure is ultimately responsible for maintainability: How easy is it to change code, to extend code, and to test code? Without the ability to easily change code, add new functionality and guarantee its correctness by means of tests, a project is at the end of its lifecycle. The structure is also responsible for the scalability of a project: How large can the project grow before it collapses under its own weight? How many people can work on realizing the vision of the project before they step on each others’ toes? The overall structure is the architecture and design of a project. Architecture and design play a much more central role in the success of a project than any feature could ever do. Good software is not primarily about the proper use of any feature; rather, it is about solid architecture and design. Good software design can tolerate some bad implementation decisions, but bad software design cannot be saved by features (old or new) and heroic use of features alone. The Problem of Dependencies 1 2
📄 Page 8
Why is software architecture and software design so important for the quality of a project? Well, assuming everything works perfectly right now, as long as nothing changes in your software and as long as nothing needs to be added, you are fine. However, that state will likely not last for long. It is reasonable to expect that something will change. After all, that is the one constant in software development: Change. Change is the driving force behind all our problems. That is why software is called software: Because in comparison to hardware it is soft, it is easy to change, and because it is even expected to change. Yes, software is expected to be easily adapted to the ever changing requirements. But as you may know, in reality this expectation might not always be true. To illustrate this point, let us imagine that you select an issue from your issue tracking system that the team has rated with an expected effort of 2. Whatever a 2 means in your own project(s), it most certainly does not sound like a big task. So you are confident that this will be done quickly. In good faith you first take some time to understand what is expected and then you start off by making a change in some entity A. Because of immediate feedback by your tests (you are lucky to have tests!) you are quickly reminded that you also have to address the issue in entity B. That is surprising! You did not expect that B was involved at all. Still you go ahead and adapt B anyway. However, again unexpectedly, the nightly build reveals that this causes C and D to stop working. Before continuing, you now investigate the issue a little deeper and find that the roots of the issue are spread through a large portion of the code base. The little, initially innocent looking task has evolved into a large, potentially risky code modification. Your confidence in resolving the issue quickly is gone. And your plans for the rest of the week as well. Maybe this story sounds familiar to you. Maybe you can even contribute a few war stories of your own. Indeed, most developers have made similar experiences. And most of these experiences have the same source of trouble. Usually the problem can be reduced to a single word: Dependencies. As Kent Beck has expressed it in his book on test-driven development:3
📄 Page 9
Dependency is the key problem in software development at all scales. —Kent Beck Dependencies: The bane of every software developer’s existence. “But of course there are dependencies”, you argue. “There will always be dependencies. How else should different pieces of code work together?” And of course you are correct. Different pieces of code need to work together, and this interaction will always create some form of coupling. However, while there are necessary, unavoidable dependencies, there are also artificial dependencies. These are the kinds of dependency that we accidentally introduce because we lack an understanding of the underlying problem, because we do not have a clear idea of the bigger picture or because we just do not pay enough attention. Needless to say, these artificial dependencies hurt. They make it harder to understand our software. They make it harder to change software. They make it harder to add new features and they make it harder to write tests. Therefore one of the primary tasks, if not the primary task, of a software developer is to keep artificial dependencies at a minimum. This minimization of dependencies is the goal of software architecture and design. To state it in the words of Robert C. Martin: The goal of software architecture is to minimize the human resources required to build and maintain the required system. —Robert C. Martin Architecture and design are the tools to minimize the work effort in any project. They minimize the effort by dealing with dependencies and the according abstractions: Software design is the art of managing interdependencies between software components. It aims at minimizing (technical) dependencies and introduces the necessary abstractions and compromises. —Klaus Iglberger Yes, software design is an art. It is not a science and it does not come with a set of easy and clear answers. Too often the big picture of design eludes us 4
📄 Page 10
and we are overwhelmed by the complex interdependencies of software entities. But we are trying to deal with this complexity and reduce it by introducing the right kind of abstractions. This way, we keep the level of details at a reasonable level. However, too often individual developers on the team may have a different idea of the architecture and the design. That means that we might not be able to implement our own vision of a design, but we might be forced to make compromises in order to move forward. Note that the words architecture and design are used as synonyms in the quotes above. The similarities become clear if you take a look at the three levels of software development. The Three Levels of Software Development Software Design and Software Architecture are just two of the three levels of software development. They are complemented by the level of Implementation Details. Figure Figure 1-1 gives an overview of these three levels.
📄 Page 11
(This page has no text content)
📄 Page 12
Figure 1-1. The three levels of software development: Software Architecture, Software Design, and Implementation Details. Idioms can be either design or implementation patterns. Implementation Details handle how a solution is implemented. You choose the necessary (and available) C++ standard, the appropriate features, keywords, and language-specifics to use, and would deal with aspects such as memory acquisition, exception safety, performance, etc. This is also the level of implementation patterns, such as std::make_unique() as a factory function, std::enable_if as a recurring solution to apply explicit SFINAE, etc. In Software Design you start to focus on the big picture. Questions about maintainability, changeability, extensibility, testability, and scalability are more pronounced on this level than on the level of Implementation Details. Software Design primarily deals with the interaction of software entities. At this level, you handle the physical and logical dependencies of components (classes, function, …). It is the level of design patterns such as Visitor, Strategy, and Decorator, i.e. patterns that define a dependency structure between software entities as explained in chapter Chapter 3. These patterns help you to break down complex things into digestable pieces. Software Architecture is the fuzziest of the three levels, the hardest to put into words. The reason is, that there is no common, universally accepted definition of Software Architecture. While there may be many different views on what exactly an architecture is, there is one aspect, that everyone seems to agree on: Architecture usually entails the big decisions, the aspects of your software that are amongst the hardest things to change in the future: [Martin Fowler, “Who Needs an Architect?”; IEEE Software, Volume 20, Issue 5, September 2003, pp 11-13, https://doi.org/10.1109/MS.2003.1231144.] Architecture is the decisions that you wish you could get right early in a project, but that you are not necessarily more likely to get them right than any other. —Ralph Johnson 5 6
📄 Page 13
In Software Architecture, you use architectural patterns, such as Client- Server Architecture, Microservices, etc. These patterns are also dealing with the question of how to design systems, where you can change one part without affecting any other parts of your software. In contrast to Software Design patterns, they usually define and address the structure and interdependencies between the key players, the big entities of your software (e.g. modules and components instead of classes and functions). From this perspective, Software Architecture represents the overall strategy of your software approach, whereas Software Design is the tactics to make the strategy work. The problem with this picture is, that there is no definition of big. Especially with the advent of Microservices, it becomes more and more difficult to draw a clear line between small and big entities. Thus architecture is often described as what expert developers in a project perceive as the key decisions. To give a real-world example for the relation between architecture, design, and implementation details, consider yourself to be in the role of an architect. And no, please do not picture yourself in a comfy chair in front of a computer with a hot coffee next to you, but picture yourself outside at a construction site. Yes, I am talking about an architect for buildings. As such an architect you would be in charge of all the important properties of a house: its integration into the neighborhood, its structural integrity, the arrangement of rooms, etc. You would also take care of a pleasing appearance and functional qualities, such as perhaps a large living room, easy access between the kitchen and the dining room, etc. In other words, you would be taking care of the overall architecture, but also deal with smaller design aspects concerning the building. Obviously the boundary between architecture and design is fluent and not clearly separated. These decisions would be the end of your responsibility, however. As an architect, you would not worry about where to place the refrigerator, the TV, or other furniture. You would not deal with all the nifty details about where to place pictures and other pieces of decoration. In other words, you would not handle the details, you would just make sure that the house owner has the necessary foundation to live well. 7 8
📄 Page 14
What makes the separation between architecture, design, and details a little more difficult is the concept of an idiom. An idiom is a commonly used, but language-specific solution for a recurring problem. As such an idiom also represents a pattern, but it could be either an implementation pattern or a design pattern. More loosely speaking, C++ idioms are the best practices of the C++ community for either design or implementation. In C++, most idioms fall into the category of implementation details. For instance, there is the Copy-and-Swap Idiom, that you may know from the implementation of a copy assignment operator, and the RAII Idiom (Resource Acquisition Is Initialization; you should definitely be familiar with this; if not please see your second favorite C++ book ). None of these idioms introduces an abstraction and none of these helps to decouple. Still, they are indispensable to implement good C++ code. On the other hand, there are idioms that fall into the category of Software Design, as for instance the Non-Virtual Interface Idiom (NVI) or the Pimpl Idiom. These two are based on two classic GoF design patterns, the Template Method design pattern and the Bridge design pattern, respectively. As such they introduce an abstraction and help to decouple and to design for change and extensions. The Focus on Features If software architecture and software design is of such importance, then why are we in the C++ community focusing so strongly on features? Why do we create the illusion that C++ standards, language mechanics and features are decisive for a project? I think there are three strong reasons for that. First, because there are so many features, with sometimes complex details, we need to spent a lot of time talking about how to use all of them properly. We need to create a common understanding on which use is good and which use is bad. We as a community need to develop a sense of idiomatic C++. The second reason is that we might put the wrong expectations on features. As an example, let us consider C++20 modules. Without going into details, this feature may indeed be considered as the biggest technical revolution 9 10 11
📄 Page 15
since the very beginning of C++. Modules may at last put the questionable and cumbersome practice of including header files into source files to an end. Due to this potential, the expectations towards that feature are enormous. Some people even expect modules to save their project by fixing their structural issues. Unfortunately, modules will have a hard time to satisfy these expectations: Modules do not improve the structure or design of your code, but can merely represent the current structure and design. Modules do not repair your design issues, at best they make the flaws visible. Thus modules simply cannot save your project. So indeed, we may be putting too many or the wrong expectations on features. And last but not least, the third reason is that despite the huge amount of features and their complexity, in comparison to the complexity of software design, the complexity of C++ features is small. It is much easier to explain a given set of rules for features, regardless on how many special cases they contain, than explaining the best way to decouple software entities. While there is usually a good answer to all feature-related questions, the common answer in software design is “it depends”. That answer might not even be evidence of inexperience, but of the realization that the best way to make code more maintainable, more changeable, extensible, testable and scalable, heavily depends on many project-specific factors. The decoupling of the complex interplay between many entities may indeed be one of the most challenging endeavors that mankind has ever faced: Design and programming are human activities; forget that and all is lost. —Bjarne Stroustrup To me, it is a combination of these three reasons why we focus on features so much. Yes, it is necessary to talk about features and to learn how to use them correctly, but, once again: they alone do not save your project. The Focus on Software Design and Design Principles While features are important and while it is of course good to talk about them, software design is more important. Software design is essential. I would even argue it is the foundation of the success of our projects. 12
📄 Page 16
Therefore, in this book I will make the attempt to truly focus on software design and design principles instead of features. Of course I will still show good and up-to-date C++ code, but I will for instance not use the latest C++ standard. Kudos to John Lakos, who argues similarly and uses C++98 in his book. In this book I will use the more recent standards, but I will not pay attention to noexcept, or use constexpr everywhere. Instead I will try to tackle the difficult aspects of software: I will, for the most part, focus on software design, the rational behind design decisions, design principles, on managing dependencies and on dealing with abstractions. In summary, software design is the critical part of writing software. Software developers are well advised to have a good understanding of software design in order to be able to write good, maintainable software. Because after all, good software is low-cost, bad software is expensive. GUIDELINE 1 Understand the Importance of Software Design. Treat software design as an essential part of writing software; Focus less on C++ language details and more on software design; Make software adaptable to frequent changes; Avoid unnecessary coupling and dependencies to make software more changeable; Understand software design as the art of managing dependencies and abstractions; Consider the boundary between software design and software architecture as fluent. Guideline 2: Design for Change 13 14
📄 Page 17
One of the essential expectations for good software is its ability to change easily. This expectation is even part of the word software. Software, in contrast to hardware, is expected to be able to adapt easily to changing requirements (see also “Guideline 1: Understand the Importance of Software Design”). However, from your own experience you may be able to tell that often it is not easy to change code. On the contrary, sometimes a seemingly simple change turns out to be a weeklong endeavor. Separation of Concerns One of the best and proven solutions to reduce artificial dependencies and to simplify change is to separate concerns. The core of the idea is to split, segregate, or extract pieces of functionality: Systems that are broken up into small, well-named, understandable pieces enable faster work. —Michael Feathers The intent behind separation of concerns is to better understand, design, and manage complexity. This idea is probably as old as software itself and hence has been given many different names. For instance, the same idea is called Orthogonality by the Pragmatic Programmers. They advise to separate orthogonal aspects of software. Tom DeMarco called it Cohesion: Cohesion is a measure of the strength of association of the elements inside a module. A highly cohesive module is a collection of statements and data items that should be treated as a whole because they are so closely related. Any attempt to divide them up would only result in increased coupling and decreased readability. —Tom DeMarco In the SOLID principles the idea is known as the Single-Responsibility Principle (SRP). A class should have only one reason to change. —Robert C. Martin 15 16 17 18
📄 Page 18
Although the concept is old and although it is commonly known under many names, many attempts to explain separation of concerns raise more questions than they give answers. This is particularly true for the SRP. The name of this design principle alone raises questions: What is a responsibility? And what is a single responsibility? A common attempt to clarify the vagueness about SRP is the following: Everything should do just one thing. —Common Wisdom? Unfortunately this explanation is hard to outdo in terms of vagueness. Just as the word responsibility does not carry a lot of meaning, just one thing does not help to shed any more light on it. Irrespective of the name, the idea is always the same: Group only those things that truly belong together, and separate everything that does not strictly belong. Or in other words: Separate the things that change for different reasons. By doing this, you reduce artificial coupling between different aspects of your code and it helps you to make your software more adaptable to change. In the best case, you can change a particular aspect of your software in exactly one place. An Example for Artificial Coupling Let us shed some light on separation of concerns by means of a code example. And I do have a great example indeed: I present to you the abstract Document class: class Document { public: // ... virtual ~Document() = default; virtual void exportToJson( /*...*/ ) const = 0; virtual void serialize( ByteStream&, /*...*/ ) const = 0; // ... };
📄 Page 19
This sounds like a very useful base class for all kinds of documents, doesn’t it? First, there is the exportToJson() function. All deriving classes will have to implement the exportToJson() function in order to produce a Json file from the document. That will prove to be pretty useful: Without having to know about a particular kind of document (and we can imagine that we will eventually have PDF documents, Word documents, and many more) we can always export in Json format. Nice! Second, there is a serialize() function. This function lets you transform a Document into bytes via a ByteStream. You can store these bytes in some persistence system, for instance a file or a database. And of course we can expect that there are many other, useful functions available, that will allow us to pretty much use this document for everything. However, I can see the frown on your face. No, you do not look particularly convinced that this is good software design. It may be because you are just very suspicious about this example (it simply looks too good to be true). Or it may be that you have learned the hard way that this kind of design eventually leads into trouble. You may have experienced that the common object-oriented design principle to bundle the data and the functions that operate on them may easily lead to unfortunate coupling. And I agree: despite the fact that this base class looks like a great all-in-one package, and even looks like it has everything that we might ever need, this design will soon lead to trouble. This is bad design because this design favors the introduction of artificial dependencies, which will make subsequent changes harder. In this case, there are up to three kinds of artificial dependencies. Two of these are introduced by the exportToJson() function. First, exportToJson() needs to be implemented in the derived classes. And yes, there is no choice, because it is a pure virtual function. Since derived classes will very likely not want to carry the burden of implementing Json exports manually, they will rely on an external, third-party Json library: json, flatbuffers, or simdjson. Whatever library you choose for that purpose, because of the exportToJson() member function, deriving documents would suddenly depend on this library. And, very likely, all deriving classes would
📄 Page 20
depend on the same library; for consistency reasons alone. Thus the deriving classes are not really independent, they are artificially coupled to a particular design decision. Also, the dependency on a specific Json library would definitely limit the reusability of the hierarchy, because it would no longer be lightweight. And switching to another library would cause a major change because all deriving classes would have to be adapted. In the worst case, the exportToJson() function might even introduce a second dependency. The arguments expected in the exportToJson() call might accidentally reflect some of the implementation details of the chosen Json library. In that case, eventually switching to another library might result in a change of the signature of the exportToJson() function, which would subsequently cause changes in all callers. Thus the dependency on the chosen Json library might accidentally be far more widespread than intended. Another kind of coupling is introduced by the serialize() function. Due to this function the classes deriving from Document depend on global decisions on how documents are serialized. What format do we use? Do we use little endian or big endian? Do we have to add the information that the bytes represent a PDF file or a Word file? If yes (and I assume that is very likely), how do we represent such a document? By means of an integral value? For instance, we could use an enumeration for this purpose: enum class DocumentType { pdf, word, // ... Potentially many more document types }; This approach is very common for serialization. However, if this low-level representation of documents is used within the implementations of the Document classes, we would accidentally couple all the different kinds of documents. As a result, adding a new kind of document would directly affect all existing document types. That would be a serious design flaw, since, again, it will make change harder. 19 20
The above is a preview of the first 20 pages. Register to read the complete e-book.

💝 Support Author

0.00
Total Amount (¥)
0
Donation Count

Login to support the author

Login Now
Back to List