From fb590f93246e7369a5c29df6a4f52cecc83b7705 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Fri, 7 Nov 2025 13:34:46 -0700 Subject: [PATCH] (ARTICLE): Wrote the OCP section. Furthermore, also used Grammarly on the entire article! --- .../SOLID Principles: Does They Matter.md | 422 ++++++++++++++++++ src/journal/SOLID: Does They Matter.md | 270 ----------- 2 files changed, 422 insertions(+), 270 deletions(-) create mode 100644 src/journal/SOLID Principles: Does They Matter.md delete mode 100644 src/journal/SOLID: Does They Matter.md diff --git a/src/journal/SOLID Principles: Does They Matter.md b/src/journal/SOLID Principles: Does They Matter.md new file mode 100644 index 0000000..e08480d --- /dev/null +++ b/src/journal/SOLID Principles: Does They Matter.md @@ -0,0 +1,422 @@ +Date: 2025-11-?? +Desc: SOLID principles are very well known, but are they really that important? +# SOLID Principles: Do They Matter + +Solid principles guide + +###### Image source: [Geeks for Geeks](https://www.geeksforgeeks.org/system-design/solid-principle-in-programming-understand-with-real-life-examples/) +
+ +###### Author: Hayden Hargreaves + +###### Published: 11/??/2025 + +## Background + +If you have not heard of the SOLID principles, you are in the right place! SOLID is an acronym for +the first five **object-oriented design** (OOD) principles, invented by Robert C. Martin, commonly +known as [Uncle Bob](https://en.wikipedia.org/wiki/Robert_C._Martin). The goal of the SOLID principles is to establish best practices for developing +maintainable and extensible software. Adapting these principles into your own code can help you avoid +[code smells](https://en.wikipedia.org/wiki/Code_smell), refactor code and develop Agile software. + +> +> "If you think good architecture is expensive, try bad architecture." ~Uncle Bob +> + +The five principles are as follows: +- **S** - Single-responsibility Principle +- **O** - Open-closed Principle +- **L** - Liskov Substitution Principle +- **I** - Interface Segregation Principle +- **D** - Dependency Inversion Principle + +This article serves as an *introduction*, not a comprehensive guide. However, a simple understanding +of the principles can help you level up as a developer! + + +## Object-Oriented Programming Refresher + +A basic understanding of object-oriented programming (OOP) is expected for optimal success when reading +this article. Regardless, a simple refresher can't hurt! Object-oriented programming is precisely +what it sounds like, **object-based programming**. Code written in OOP languages is organized into +"objects", which are self-contained units that combine data (attributes) and functions that operate +on the data (methods). The OOP approach can simplify complex systems, promote code reusability and +modularity, which makes OOP code easier to maintain and scale. There are four main principles of +object-oriented programming: **encapsulation**, **inheritance**, **abstraction** and **polymorphism**. I will write +a dedicated article about these four principles soon, which will also be found here in my journal. + +To understand the SOLID principles, it is essential to remember **what a class is**: a class is a blueprint +or template for creating objects. An object is a unique instance of a class. + + +There are many object-oriented languages, and the concepts taught in this article are not unique to +a specific language; they can be applied to any language that implements OOP structure (even Python!). +Some languages include: **[C++](https://en.wikipedia.org/wiki/C%2B%2B)**, **[C#](https://en.wikipedia.org/wiki/C_Sharp_(programming_language))**, **[Java](https://en.wikipedia.org/wiki/Java_(programming_language))**, **[Ruby](https://en.wikipedia.org/wiki/Ruby_(programming_language))**, and others. The examples provided in this article +will be in C++, but as mentioned previously, they apply to any OOP language! + +## Single-Responsibility Principle + +The **[Single-Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle)** states: + +> +> "A class should have one and only one reason to change, meaning that a class should have only one +> job." ~Robert "Uncle Bob" Martin +> + +### The Misunderstood Principle + +The SRP is the simplest, yet most commonly misunderstood principle. The goal of the SRP is to **prevent +unexpected side effects* by keeping each unit (class) simple and with only a single purpose. A class +with many responsibilities will often need to be modified as requirements change, which can lead to +more bugs. When a class is changed, it can impact classes that depend on it, which can result in +unexpected bugs in code that did not seem to change. However, a class with a **single responsibility** +will be changed much less, reducing the number of sneaky bugs that result from code refactors. + + +### Easier to Understand + +Another benefit of implementing the single-responsibility principle is that the resulting code becomes +much easier to understand. A class with a single purpose is much easier to explain to a coworker or +intern. However, this is another area of shared misunderstanding. Some developers take the SRP a bit +too far and **oversimplify** their code, for example, by writing a new class for each function. When they +later want to write some real code, they need to inject dozens of dependencies to achieve a single task! + +A healthy balance of responsibility and simplicity exists, which can be challenging to understand at +first. The best thing you can do is keep the SRP in mind, but do not follow it **too strictly**. Do not +use it as your "programming bible." Use common sense; there is no point in classes that only contain +a single function! + +### Code Example + +To display this concept, we will examine a `Shape` class that needs to be drawn to an output. Below is +an implementation that does not adhere to the single-responsibility principle. + +```cpp +class Shape { +public: + Shape(double w, double h) : width(w), height(h) {}; + + // Responsibility 1: Core Business Logic (Math) + double calculateArea() const { + return this->width * this->height; + } + + // Responsibility 2: Presentation/Output (Drawing) + void draw() const { + // Imagine complex rendering code here... + std::cout << "Drawing a rectangle of size " << this->width << "x" + << this->height << "\n"; + } + +private: + double width; + double height; +}; +``` + +However, as the comments note, this class has more than one responsibility. The class is responsible +for storing shape data, computing the area, and rendering it to the output. Imagine we have hundreds +of shapes, we don't want to write hundreds of different ways to render each shape! This example is a +tad simple, but it helps us understand why we need to split responsibilities as code scales. + +To fix this, we can create a `ShapeRenderer` class and simplify our `Shape` class. + +```cpp +// 1. ShapeSRP: Responsibility = Core Business Logic ONLY (Data and Math) +class ShapeSRP { +public: + ShapeSRP(double w, double h) : width(w), height(h) {} + + // Methods for data access and core calculation + double getWidth() const { return width; } + double getHeight() const { return height; } + + // Stays here as it's the core purpose of the data + double calculateArea() const { + return width * height; + } + +private: + double width; + double height; +}; + +// 2. ShapeRenderer: Responsibility = Presentation/Output ONLY +class ShapeRenderer { +public: + // This class's sole job is to handle how the Shape is visualized. + void draw(const ShapeSRP& shape) const { + // The rendering logic is isolated here. + std::cout << "--- Graphics Renderer Output ---\n"; + std::cout << "Drawing a shape with area: " << shape.calculateArea() << "\n"; + std::cout << "Using dimensions: " << shape.getWidth() << "x" << shape.getHeight() << "\n"; + } +}; +``` + +We have successfully implemented a scalable and modular class that can be used with various shapes. +Using polymorphism, we can achieve an even better solution, which is not the focus of this article, +but further encourages the idea. + +```cpp +#include +#include + +// 1. Abstract Base Class: Defines the contract for all shapes +class Shape { +public: + // Core Business Logic: Must be implemented by derived classes + virtual double calculateArea() const = 0; + + // Virtual destructor is crucial for proper cleanup with polymorphism + virtual ~Shape() = default; +}; + +// Concrete Shape 1: Rectangle +class Rectangle : public Shape { +public: + Rectangle(double w, double h) : width(w), height(h) {} + + // Implements the specific area calculation for a rectangle + double calculateArea() const override { return width * height; } + + // Getters needed for the renderer + double getWidth() const { return width; } + double getHeight() const { return height; } + +private: + double width; + double height; +}; + +// Concrete Shape 2: Circle +class Circle : public Shape { +public: + Circle(double r) : radius(r) {} + + // Implements the specific area calculation for a circle + double calculateArea() const override { return M_PI * radius * radius; } + + // Getters needed for the renderer + double getRadius() const { return radius; } + +private: + double radius; +}; + +// Renderer Interface (Contract for drawing) +class Renderer { +public: + // The renderer must be able to handle any kind of Shape + virtual void render(const Shape &shape) const = 0; + virtual ~Renderer() = default; +}; + +// Console Renderer Implementation +class ConsoleRenderer : public Renderer { +public: + void render(const Shape &shape) const override { + std::cout << "\n--- Console Output (Simple) ---\n"; + + // This dynamic_cast is often necessary when a Renderer needs specific data, + // but it's important to keep the logic here, separate from the Shape class! + if (const auto *rect = dynamic_cast(&shape)) { + std::cout << "Type: Rectangle\n"; + std::cout << "Dimensions: " << rect->getWidth() << "x" + << rect->getHeight() << "\n"; + } else if (const auto *circ = dynamic_cast(&shape)) { + std::cout << "Type: Circle\n"; + std::cout << "Radius: " << circ->getRadius() << "\n"; + } else { + std::cout << "Type: Unknown Shape\n"; + } + + // **Polymorphic call:** This works for all shapes! + std::cout << "Calculated Area: " << shape.calculateArea() << "\n"; + } +}; +``` + +This has taken our shape renderer example to new heights! But by now, you should be able to understand +the pros and cons of the **S**ingle-responsibility principle. + + +## Open/Closed Principle + +The **[Open/Closed Principle](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle)** states: + +> +> "Objects or entities should be open for extension but closed for modification." ~Bertrand Meyer +> + +This means a class should be extensible without requiring modifications to the class itself. + +The general idea of the Open/Closed Principle (OCP) is excellent! It requires a developer to write +code that can be upgraded or extended without requiring modifications to existing code. This effect +prevents extensions from requiring the developer to adapt all classes that depend on the target +class. But Bertrand Meyer suggests that [inheritance](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)) is used to achieve this: + +> +> “A class is closed, since it may be compiled, stored in a library, baselined, and used by client +> classes. But it is also open, since any new class may use it as parent, adding new features. When +> a descendant class is defined, there is no need to change the original or to disturb its clients.” +> ~Bertrand Meyer +> + +This is problem because it frequently introduces tight coupling if subclasses depend on the +implementation of a parent class. For that reason, "Uncle Bob" revised the principle to the **[Polymorphic](https://en.wikipedia.org/wiki/Polymorphism_(computer_science)) +Open/Closed Principle**. Using interfaces instead of **superclasses** (a class which has subclasses) allows +different implementations which can be swapped and changed with relative ease. Furthermore, the calling +(using) code does not need to be changed, as long as the interface's requirements are met. + +This is a problem because it frequently introduces tight coupling if subclasses depend on the +implementation of a parent class. For that reason, "Uncle Bob" revised the principle to the **[Polymorphic](https://en.wikipedia.org/wiki/Polymorphism_(computer_science)) +Open/Closed Principle**. Using interfaces instead of **superclasses** (a class that has subclasses) allows +different implementations, which can be swapped and changed with relative ease. Furthermore, the +calling (using) code does not need to be changed, as long as the interface's requirements are met. + +Another significant benefit of using this polymorphic guideline is that it introduces an additional +level of **[abstraction](https://en.wikipedia.org/wiki/Abstraction_(computer_science))**, which enables loose coupling. Interface implementations are distinct and have +no relation to one another, and share no code. But if the interfaces should share some code, then  +**composition** and **inheritance** can be used. + + +### Code Example + +To reinforce this point, a simple employer bonus calculation program will be written in two different +ways: one that violates the OCP, and one that does not. It will be up to you to judge the effectiveness +of each solution, and therefore the effectiveness of the rule! + +In the example below, we violate the Open/Closed principle because any time a new employee type is +added, the `BonusCalculator` class must be updated to match. + +```cpp +enum EmployeeType { MANAGER, DEVELOPER, SALES }; + +class Employee { +public: + Employee(EmployeeType type, double salary) : type(type), salary(salary) {} + EmployeeType getType() const { return type; } + double getSalary() const { return salary; } + +private: + EmployeeType type; + double salary; +}; + +// VIOLATION: This class must be MODIFIED every time a new EmployeeType is added. +class BonusCalculator { +public: + double calculateBonus(const Employee &emp) const { + double salary = emp.getSalary(); + + if (emp.getType() == MANAGER) { + // Manager gets 10% bonus + return salary * 0.10; + } else if (emp.getType() == DEVELOPER) { + // Developer gets 5% bonus + return salary * 0.05; + } else if (emp.getType() == SALES) { + // Sales gets a fixed bonus + return 500.0; + } + // If we add 'HR', we MUST modify and recompile this function. + return 0.0; + } +}; +``` + +To fix this example, we can take three steps: + +First, we can use abstraction via an **Abstract Base Class** (a class that cannot be instantiated, ABC) +and polymorphism. This allows us to extend the base Employee class and let the final `BonusProcessor ` +class grow via polymorphism. + +The `EmployeeBase` class is open for extension since many more types can implement it without +modifying the class itself. + +```cpp +// 1. Base Class: Defines the contract +class EmployeeBase { +public: + EmployeeBase(double salary) : salary(salary) {} + + // Virtual function (the key to polymorphism) + virtual double calculateBonus() const = 0; + virtual ~EmployeeBase() = default; + +protected: + double getSalary() const { return salary; } + +private: + double salary; +}; +``` + +Now, we can create some different types of employees that inherit (implement) the `EmployeeBase` ABC. + +```cpp +// 2. Concrete Implementations +class Manager : public EmployeeBase { +public: + Manager(double salary) : EmployeeBase(salary) {} + // Implements its unique bonus logic + double calculateBonus() const override { return getSalary() * 0.10; } +}; + +class Developer : public EmployeeBase { +public: + Developer(double salary) : EmployeeBase(salary) {} + // Implements its unique bonus logic + double calculateBonus() const override { return getSalary() * 0.05; } +}; + +// Adding a brand new type (e.g., HR) requires NO modification to existing code! +class HRSpecialist : public EmployeeBase { +public: + HRSpecialist(double salary) : EmployeeBase(salary) {} + double calculateBonus() const override { return getSalary() * 0.03; } +}; +``` + +Finally, we can create the `BonusProcessor` class, which interacts only with the `EmployeeBase` ABC, +making it **closed for modification**. + +```cpp +// 3. ADHERENCE: This class is CLOSED for modification. +class BonusProcessor { +public: + // This function doesn't need to know the specific type (Manager, Developer, + // etc.) It relies only on the contract (virtual function) defined in + // EmployeeBase. + double processBonus(const EmployeeBase &emp) const { + return emp.calculateBonus(); + } +}; +``` + +Hopefully, you can see how powerful this principle can be when implemented correctly. Code that +adheres to the open/closed principle is easy to scale, expand and upgrade. + + +## Liskov Substitution Principle +define the rule +why it exists +what is attempts to achieve + + +## Interface Segregation Principle +define the rule +why it exists +what is attempts to achieve + + +## Dependency Inversion Principle +define the rule +why it exists +what is attempts to achieve + +## SOLID Only For OOP? + + +## "Don't Repeat Yourself" From Uncle Bob diff --git a/src/journal/SOLID: Does They Matter.md b/src/journal/SOLID: Does They Matter.md deleted file mode 100644 index 9de47f1..0000000 --- a/src/journal/SOLID: Does They Matter.md +++ /dev/null @@ -1,270 +0,0 @@ -Date: 2025-11-?? -Desc: SOLID principles are very well known, but are they really that important? -# SOLID: Do They Matter - -Solid principles guide - -###### Image source: [Geeks for Geeks](https://www.geeksforgeeks.org/system-design/solid-principle-in-programming-understand-with-real-life-examples/) -
- -###### Author: Hayden Hargreaves - -###### Published: 11/??/2025 - -## Background - -If you have not heard of the SOLID principles, you are in the right place! SOLID is an acronym for -the first five **object-oriented design** (OOD) principles, invented by Robert C. Martin who is commonly -known as [Uncle Bob](https://en.wikipedia.org/wiki/Robert_C._Martin). The goal of the SOLID principles is to establish best practices for developing -maintainable and extensible software. Adapting these principles into your own code can help you avoid -[code smells](https://en.wikipedia.org/wiki/Code_smell), refactor code and develop Agile software. - -> -> "If you think good architecture is expensive, try bad architecture." ~Uncle Bob -> - -The five principles are as follows: -- **S** - Single-responsibility Principle -- **O** - Open-closed Principle -- **L** - Liskov Substitution Principle -- **I** - Interface Segregation Principle -- **D** - Dependency Inversion Principle - -This article will serve as an *introduction*, not a complete guide. However, a simple understanding -of the principles can help you level up as a developer! - - -## Object-Oriented Programming Refresher - -Some basic knowledge of object-oriented programming (OOP) is expected for best success when reading this -article. Regardless, a simple refresher can't hurt! Object-oriented programming is exactly as it sounds, -**object based programming.** Code written in OOP languages is organized into "objects", which are self-contained -units that combine data (attributes) and functions that operate on the data (methods). The OOP approach -*can* simplify complex systems, promote code reusability and modularity which makes OOP code easier to -maintain and scale. There are four main principles of object-oriented principles: **encapsulation**, -**inheritance**, **abstraction** and **polymorphism**. I will write a dedicated article about these four -principles soon, which will also be found here in my journal. - -To understand the SOLID principles, the most important thing to remember is **what a class is;** a -class is a blueprint or template for creating objects. An object is a unique instance of a class. - - -There are many object-oriented languages and the concepts taught in this article are not unique to a -specific language, they can be applied to any language which implements OOP structure (even Python!). -Some languages include: **[C++](https://en.wikipedia.org/wiki/C%2B%2B)**, **[C#](https://en.wikipedia.org/wiki/C_Sharp_(programming_language))**, **[Java](https://en.wikipedia.org/wiki/Java_(programming_language))**, **[Ruby](https://en.wikipedia.org/wiki/Ruby_(programming_language))**, and more. The examples provided in this article -will be in C++, but as mentioned previously, they apply to any OOP language! - -## Single-Responsibility Principle - -The **[Single-Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle)** states: - -> -> "A class should have one and only one reason to change, meaning that a class should have only one -> job." ~Robert "Uncle Bob" Martin -> - -### The Misunderstood Principle - -The SRP is the simplest, yet most commonly misunderstood principle. The goal of the SRP is to **prevent -unexpected side effects** by keeping each *unit* (class) simple and with only a single purpose. A class -that has many responsibilities will frequently need to be changed as requirements change, which can -lead to more bugs. When a class is changed, it can impact classes that depend on it, which can result -in unexpected bugs in code that did not *seem* to change. However, a class with a **single responsibility** -will be changed much less, reducing the number of sneaky bugs that result from code refactors. - - -### Easier to Understand - -Another benefit of implementing the single-responsibility principle is that resulting code becomes much -easier to understand. A class that has a single purpose is much easier to explain to a co-worker or intern. -However, this is another common place of misunderstanding. Some developers take the SRP a bit too far -and **over-simplify** their code, for example: writing a new class for each function!? When they later -want to write some real code, they need to inject dozens of dependencies just to achieve a single task! - -There exists a healthy balance of responsibility and simplicity, which can be hard to understand at first. -The best thing you can do is keep the SRP in mind, but not follow it **too strictly**. Do not use it as -your "programming bible." Use common sense, there is no point in classes that only contain a single function! - -### Code Example - -To display this concept we will look at a **Shape** class which needs to be draw to an output. Below -is an implementation which does not adhere to the single-responsibility principle. - -```cpp -class Shape { -public: - Shape(double w, double h) : width(w), height(h) {}; - - // Responsibility 1: Core Business Logic (Math) - double calculateArea() const { - return this->width * this->height; - } - - // Responsibility 2: Presentation/Output (Drawing) - void draw() const { - // Imagine complex rendering code here... - std::cout << "Drawing a rectangle of size " << this->width << "x" - << this->height << "\n"; - } - -private: - double width; - double height; -}; -``` - -However, as the comments note, this class has more than one responsibility. The class is responsible -for storing shape data and computing the area as well as rendering it to the output. Imagine we have -hundreds of shapes, we don't want to write hundreds of different ways to render each shape! This -example is a tad simple, but it helps us understand why we need to split responsibilities as code scales. - -To fix this, we can create a **ShapeRenderer** class and simplify our **Shape** class. - -```cpp -// 1. ShapeSRP: Responsibility = Core Business Logic ONLY (Data and Math) -class ShapeSRP { -public: - ShapeSRP(double w, double h) : width(w), height(h) {} - - // Methods for data access and core calculation - double getWidth() const { return width; } - double getHeight() const { return height; } - - // Stays here as it's the core purpose of the data - double calculateArea() const { - return width * height; - } - -private: - double width; - double height; -}; - -// 2. ShapeRenderer: Responsibility = Presentation/Output ONLY -class ShapeRenderer { -public: - // This class's sole job is to handle how the Shape is visualized. - void draw(const ShapeSRP& shape) const { - // The rendering logic is isolated here. - std::cout << "--- Graphics Renderer Output ---\n"; - std::cout << "Drawing a shape with area: " << shape.calculateArea() << "\n"; - std::cout << "Using dimensions: " << shape.getWidth() << "x" << shape.getHeight() << "\n"; - } -}; -``` - -Now we have successfully implemented a scalable and modular class which can be used by many shapes! -Using polymorphism we can achieve an even better solution, which is not the focus of this article, but -further encourages the idea. - -```cpp -#include -#include - -// 1. Abstract Base Class: Defines the contract for all shapes -class Shape { -public: - // Core Business Logic: Must be implemented by derived classes - virtual double calculateArea() const = 0; - - // Virtual destructor is crucial for proper cleanup with polymorphism - virtual ~Shape() = default; -}; - -// Concrete Shape 1: Rectangle -class Rectangle : public Shape { -public: - Rectangle(double w, double h) : width(w), height(h) {} - - // Implements the specific area calculation for a rectangle - double calculateArea() const override { return width * height; } - - // Getters needed for the renderer - double getWidth() const { return width; } - double getHeight() const { return height; } - -private: - double width; - double height; -}; - -// Concrete Shape 2: Circle -class Circle : public Shape { -public: - Circle(double r) : radius(r) {} - - // Implements the specific area calculation for a circle - double calculateArea() const override { return M_PI * radius * radius; } - - // Getters needed for the renderer - double getRadius() const { return radius; } - -private: - double radius; -}; - -// Renderer Interface (Contract for drawing) -class Renderer { -public: - // The renderer must be able to handle any kind of Shape - virtual void render(const Shape &shape) const = 0; - virtual ~Renderer() = default; -}; - -// Console Renderer Implementation -class ConsoleRenderer : public Renderer { -public: - void render(const Shape &shape) const override { - std::cout << "\n--- Console Output (Simple) ---\n"; - - // This dynamic_cast is often necessary when a Renderer needs specific data, - // but it's important to keep the logic here, separate from the Shape class! - if (const auto *rect = dynamic_cast(&shape)) { - std::cout << "Type: Rectangle\n"; - std::cout << "Dimensions: " << rect->getWidth() << "x" - << rect->getHeight() << "\n"; - } else if (const auto *circ = dynamic_cast(&shape)) { - std::cout << "Type: Circle\n"; - std::cout << "Radius: " << circ->getRadius() << "\n"; - } else { - std::cout << "Type: Unknown Shape\n"; - } - - // **Polymorphic call:** This works for all shapes! - std::cout << "Calculated Area: " << shape.calculateArea() << "\n"; - } -}; -``` - -This has taken our shape renderer example to new heights! But by now, you should be able to understand -the pros and cons of the **S**ingle-responsibility principle. - - -## Open/Closed Principle - -define the rule -why it exists -what is attempts to achieve - - -## Liskov Substitution Principle -define the rule -why it exists -what is attempts to achieve - - -## Interface Segregation Principle -define the rule -why it exists -what is attempts to achieve - - -## Dependency Inversion Principle -define the rule -why it exists -what is attempts to achieve - -## SOLID Only For OOP? - - -## "Don't Repeat Yourself" From Uncle Bob