(JOURNAL): Began working on the SOLID article!

This commit is contained in:
Hayden Hargreaves 2025-11-06 22:49:16 -07:00
parent bc849ab6a5
commit a1dfee0f4c
5 changed files with 316 additions and 14 deletions

4
.gitignore vendored
View File

@ -1,5 +1,7 @@
node_modules node_modules
.idea .idea
flake.lock
bun.lockb
# Output # Output
.output .output
@ -24,5 +26,3 @@ vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
# Jet brains
.idea

41
flake.nix Normal file
View File

@ -0,0 +1,41 @@
{
description = "Bun development flake. Adjust as needed";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in
{
# Define the development shell.
# When you run `nix develop` (or direnv activates), you'll enter this shell.
devShells.default = pkgs.mkShell {
# List all the development tools you need available in this shell's PATH.
packages = with pkgs; [
# Add packages here...
bun
];
# Define the shell that will be executed.
# Here, we explicitly use zsh.
# Note: pkgs.zsh needs to be included in `packages` or `nativeBuildInputs`
# for it to be found in the shell's environment. `inherit pkgs.zsh;` is concise.
inherit (pkgs) zsh;
# Environment variables and commands to run when the shell starts.
shellHook = ''
# Add any exports, hooks, aliases, or anything else here
# Exec zsh to replace the current shell process with zsh.
# This ensures your prompt and zsh configurations load correctly.
exec zsh
'';
};
}
);
}

View File

@ -0,0 +1,270 @@
Date: 2025-11-??
Desc: SOLID principles are very well known, but are they really that important?
# SOLID: Do They Matter
<img src="/journal/SOLID.png" alt="Solid principles guide" style="background-color: white; border-radius: 15px; margin-inline: 10px; margin-top: 2.5%; margin-bottom: 1.5%; width: 95%; max-width: 500px;">
###### Image source: [Geeks for Geeks](https://www.geeksforgeeks.org/system-design/solid-principle-in-programming-understand-with-real-life-examples/)
<br>
###### 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 <cmath>
#include <iostream>
// 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<const Rectangle *>(&shape)) {
std::cout << "Type: Rectangle\n";
std::cout << "Dimensions: " << rect->getWidth() << "x"
<< rect->getHeight() << "\n";
} else if (const auto *circ = dynamic_cast<const Circle *>(&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

View File

@ -9,7 +9,7 @@ Desc: I have decided to begin the journey of learning functional programming. He
###### Author: Hayden Hargreaves ###### Author: Hayden Hargreaves
###### Published: 05/??/2025 ###### Published: 05/21/2025
## Background ## Background
@ -287,17 +287,8 @@ a full stack web application using the Phoenix Framework. This app will allow us
a love for cooking and having to store all my recipes in my notes app is cumbersome. Plus, my parents are **amazing** cooks and I would to be able to "borrow" a love for cooking and having to store all my recipes in my notes app is cumbersome. Plus, my parents are **amazing** cooks and I would to be able to "borrow"
their recipes and save them for myself, without having to copy them manually. their recipes and save them for myself, without having to copy them manually.
##### More about the app, and what I liked and hated.
This article is under construction. Come back later for more!
## Elixir Review ## Elixir Review
After writing a *quality* full stack application I have learned enough about the language to develop an opinion. The learning process has been put on pause to focus on work, but when I make it back to
Elixir, I will finish the conclusion!
This is what I found...

BIN
static/journal/SOLID.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB