Rethinking C++ Design: From Gang of Four to Modern Approaches
0:00 Introduction: The speaker, Claus, introduces the topic of "silver bullets" in software design, focusing on C++, and uses a shape-drawing example to explore different solutions.
0:00 Go-to Problem: The example involves drawing shapes (circles, squares, rectangles, ellipses, etc.), with a need for extensibility and scalability.
0:00 Requirements: The system should be extensible with new shapes, and should be designed for a large team (100+ developers) and codebase (10 million lines of code).
0:00 Initial Solution (Gang of Four Style): The speaker introduces a base Shape class with a virtual draw function and concrete shape classes (e.g., Circle, Square) inheriting from it. A strategy pattern is used to decouple the shapes from the graphics library.
0:02 Draw Strategy: A DrawStrategy base class is introduced, templated on the shape type. Concrete draw strategies implement specific drawing logic. The Circle and Square classes are updated to accept and use this new strategy, further decoupling them from graphics libraries.
0:03 Shape Factory: A ShapeFactory interface is introduced to extract shapes from a file, and a drawShapes function is created to traverse and draw a vector of shapes. A createAndDraw shapes function is used to create and draw shapes by using the factory pattern.
0:05 Implementation: The presenter implements openGL versions of the Draw Strategy for Circle and Square, and a Shape Factory that will extract these shapes from a file.
0:06 Architectural Boundary: A key architectural boundary is defined: above, abstract base classes; below, concrete implementations (e.g., OpenGL, file format). This is called "my code" and "your code", respectively.
0:06 Extending: The presenter then showcases how easy it is to implement the new Rectangle class based on the previous design with an appropriate draw strategy.
0:07 Modern C++ Features: The initial solution uses modern C++ features such as unique pointers, move semantics, override, default, range-based for loops, and string_view.
0:07 Skepticism: The audience is skeptical, recognizing the old-school nature of the design, referred to as the "Gang of Four" style.
0:08 Gang of Four Style: The speaker acknowledges the design is rooted in 1990s patterns and is commonly used but not truly "modern."
0:09 Industry Practices: The speaker claims that 90% of projects use this "Gang of Four" style of programming, so it's not something of the past.
0:10 Alternative Perspectives: The presenter mentions that there are people who are not happy with the object oriented programming paradigm. He mentions multiple different people talking about how this might not be the way to approach problems today, referring to functional programming.
0:12 Variant-Based Solution: The speaker introduces a variant-based approach as an alternative to inheritance and virtual functions.
0:13 New Shape Primitives:Circle and Square classes are simplified to only store their geometric properties, removing inheritance and coupling to graphics libraries.
0:14 Shape Variant: A Shape variant is defined to hold different shape types (Circle, Square, etc.), and shapes are stored as values, not as pointers, and that they are very natural and intuitive. A vector of shape objects replaces the previous vector of shape pointers.
0:15 Factory Update: The shape factory is updated to handle values instead of pointers to shapes, using variants.
0:15 Abstraction of Factories: The presenter introduces a way to abstract different kinds of factories using a variant, so that they can easily be managed.
0:15 Open Gl Drawer: The openGL drawer gets an update as well, to remove inheritance, but it still nicely encapsulates all details for drawing with openGL.
0:16 Drawer Variant: A drawer variant is introduced, abstracting from the different drawers, and then the drawShapes function is updated to take a drawer and shape variant, and using std::visit, doing a multi dispatch call.
0:17 Main Function Updated: The createAndDraw function is updated to use std::visit, and all of this is packed in the updated main function.
0:17 Modern C++ Solution Summary: This new solution has no inheritance, no smart pointers, no dynamic allocations, and uses values rather than pointers.
0:18 Simpler Code: The new code is simpler and uses values, not pointers or smart pointers. It encapsulates graphics code well. The speaker emphasizes less boiler plate and more focus on relevant code.
0:18 Performance Comparison: The speaker states this new approach might even be faster, but it depends on the specific implementation details and hardware, and presents results showing the variant solution is faster but when including an Impact Variant the performance numbers of both models are closer to one another.
0:20 Performance Caveats: The presenter highlights that with templated abstractions, the performance increase might be due to compiler optimization and that it is important to benchmark for the exact use-case of the code base.
0:20 Type Erasure Implementation: The speaker mentions that "the Ancients" have sung a song about using variant, highlighting how the functional programming approach and the variant pattern are very powerful and useful.
0:21 Functional Approach: The speaker shows that the variant and functional approach is aligned with functional programming principles, and quotes a number of other C++ experts that encourage this approach.
0:22 Refactoring: The speaker introduces that after showcasing the new approach, people might consider it too easy and want to refactor the whole code base to use that as a silver bullet.
0:22 Architectural Boundary Issue: The architectural boundary is revisited to highlight that the current implementation of both the type-erasure and variant model, makes it harder for other developers to work on specific implementations, coupling code bases.
0:24 Solution Failure: The speaker reveals that the initial solution and the variant solution, both fail from an architectural point of view because everything is coupled, and a simple change will require changes everywhere.
0:24 "Silver Bullet" Trap: The presenter refers to a song about variant, highlighting how its approach might not be the solution to all problems.
0:25 Templates as a Solution: The speaker introduces the idea of templating the drawShapes and createAndDrawShapes functions to achieve an inversion of dependencies. The new shape abstraction is given to the implementation and not the other way around, and allows developers to introduce new shapes without requiring changes to the main structure.
0:26 Inversion of Dependencies: By using templates, the speaker highlights the ability to invert dependencies, not depending on specific implementations and shape types, allowing the system to be more flexible.
0:26 Templates Issue: The presenter does mention that the code might become too dependent on templates, making it hard to compile.
0:27 Value-Based Approach: The presenter explores how to achieve a value-based version of object oriented programming, introducing a new design pattern.
0:28 Shape Concept: The speaker renames the shape base class to Shape Concept, and introduces a new Shape Model which inherits from Shape Concept.
0:28 Shape Model Implementation: The Shape Model is a class template that takes a shape and a draw strategy as a parameter and stores a specific instance of that shape, wrapping the type erasure.
0:28 Polymorphic Behavior: The ShapeModel encapsulates the polymorphic drawing logic by delegating to a draw strategy, decoupling from concrete drawing implementations.
0:30 Circle Simplification: The Circle class no longer has to inherit from ShapeConcept, making it a simple geometric primitive, further decoupling from drawing logic.
0:30 Shape Abstraction: The presenter also introduces a wrapper class, Shape to encapsulate the previous logic, by storing a unique pointer of ShapeConcept, which hides its implementation.
0:31 Shape Wrapper: The shape class is templated, receives the shape and draw strategy as a parameter, stores an instance of the ShapeModel by using a unique pointer, and is also responsible for implementing the draw function, delegating the method to the Shape Model implementation.
0:31 Value Semantics: The Shape class is designed to act as a value, including implementing the rule of five, focusing on copy/move semantics and lifetime management. A note is added that if the implementation is using a unique pointer to a virtual base class, then one has to also consider implementing virtual clones, that the base class does not provide.
0:31 Type Erasure: All of the complexity has been hidden inside this new abstraction layer, referred to as "Type Erasure".
0:32 Modern Code Summary: A modern code summary is presented: no base class, no virtual functions, and it is a value. The code that uses this new abstraction is now easier to read and write.
0:32 Surrounding Code: The presenter shows an updated implementation of how the Shape, Shapes, ShapeFactory and OpenGlDrawer will behave with the new design.
0:33 Draw Shapes Impl: The drawShapes function now calls a . operator, because the shapes are values and not pointers.
0:34 Value-Based Design: The presenter re-emphasizes that the new implementation works with values rather than pointers, which makes the code simpler to read and implement.
0:34 Separation of Concerns: The new abstraction allows developers to separate work and be in control of their code.
0:35 Architectural Boundary: The architectural boundary is reintroduced so that the developer can see where his code will be added with a clean separation of responsibilities, making it easy to add new shapes and functionalities.
0:35 Code Properties Summary: The code is simpler, has fewer pointers, fewer virtual functions (encapsulated in type erasure), less inheritance, and less manual memory management. The speaker notes that all of this is done while potentially being faster.
0:36 Performance Results: Results are shown to not be dramatically faster, because the inheritance hierarchy is just encapsulated inside the class. The speaker adds that because the implementation is a value type, there is room to apply a small buffer optimization (SBO) to further optimize the performance.
0:37 Recommendation: The speaker recommends this value based approach for object oriented programming, but adds that you should not forget about the functional programming solution.
0:38 Solution Selection: You should look at where functional programming with variants works best and where the object oriented approach using type erasure is more suitable.
0:38 Variant Usage: Variants are better for lower-level implementations, algorithms, and places with a closed set of types but an open set of operations.
0:38 Type Erasure Usage: Type Erasure is better for higher level implementation, an open set of types with a closed set of operations where new types can be added in the future without the need to refactor the rest of the code base.
0:40 Mental Model: The presenter introduces a mental model that this is like "two sides of the same coin", and it is up to the developer to select what side works best by carefully considering the architectural properties.
0:40 Software Design: The first rule of software design is to think about the architectural properties of the solution first, focusing on flexibility, extensibility, and future needs, rather than focusing on a single "silver bullet" approach.
0:40 The Next Silver Bullet: The speaker introduces the C++26 standard, which might introduce reflection and how that might be seen as the next "silver bullet" that should be used everywhere.
0:41 C++ Reflection: The speaker suggests that reflection may automate type erasure and potentially even replace variants.
0:42 Architectural Tradeoffs: A second rule is introduced that developers must always consider both the advantages and disadvantages of each approach.
0:42 No Silver Bullet: The speaker reiterates that there are no silver bullet solutions in C++. Developers must be knowledgeable about multiple different solutions, their architectural properties, pros and cons, and choose the right one for the job.
0:43 Q&A: Libraries: A member of the audience asks about a library for boiler plate code.
0:43 Libraries: The presenter recommends three specific libraries that implement a form of type erasure: LibLon, the Zoo library, and the boost library.
0:43 Custom Implementation: The speaker notes that most of these libraries are hard to use and suggests that it is easier to implement the type erasure code yourself and perhaps that the reflection capabilities of C++ 26 might help eventually in automating this boiler plate code generation.
0:44 Q&A: C++ Concepts: Another member of the audience suggests using C++ concepts to relieve tie coupling, but the presenter notes that this only pushes the problem into templates, rather than solving it.
0:44 Q&A: CRTP: Another question is introduced in regard to using CRTP.
0:46 CRTP Usage: The speaker notes that CRTP is not a drop-in replacement for inheritance hierarchies and can quickly push the developer to creating complex template logic, limiting the flexibility that is provided by inheritance.
0:47 Conclusion: The presenter concludes that it is best to focus on architectural properties rather than chasing after a silver bullet solution and choose between different solutions depending on the problem at hand, and the needs of the software system.