SOLID Principles: The Shapes Edition
Understanding the SOLID principles is the difference between a codebase that is a house of cards and one that is a fortress. Let’s break these down using a simple Shape-based ecosystem in Java.
1. Single Responsibility Principle (SRP)
“A class should have one, and only one, reason to change.”
The Problem
In this version, the Circle is trying to be both a mathematical model and a UI renderer. If your rendering engine changes, you have to modify your logic class.
public class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
// Responsibility 1: Mathematical Logic
public double getArea() {
return Math.PI * radius * radius;
}
// Responsibility 2: UI/Rendering
public void draw() {
System.out.println("Drawing a circle with radius: " + radius);
}
}
The Solution
Separate the concerns. The Circle holds data, and the Canvas handles the drawing logic.
public class Circle {
private final double radius;
public Circle(double radius) { this.radius = radius; }
public double getRadius() { return radius; }
public double getArea() { return Math.PI * radius * radius; }
}
public class Canvas {
public void drawCircle(Circle circle) {
System.out.println("Drawing circle at Position: 0,0 | Radius: " + circle.getRadius());
}
}
2. Open/Closed Principle (OCP)
“Software entities should be open for extension, but closed for modification.”
The Problem
If we want to add a Square, we have to modify the Canvas class by adding a drawSquare method. This violates OCP.
public class Canvas {
public void drawCircle(Circle circle) { /* ... */ }
public void drawSquare(Square square) { /* ... */ }
// What happens when we add Triangle? Or Pentagon? We keep editing Canvas.
}
The Solution
Use an interface. Now, Canvas can render any Shape without ever needing to be modified again.
public interface Shape {
void draw();
}
public class Circle implements Shape {
@Override
public void draw() { System.out.println("Drawing a Circle."); }
}
public class Canvas {
public void render(Shape shape) {
// This code NEVER needs to change.
shape.draw();
}
}
3. Liskov Substitution Principle (LSP)
“Subtypes must be substitutable for their base types.”
The Problem
A Square is technically a Rectangle, but in code, forcing that relationship often breaks logic. Setting the width of a Square shouldn’t unexpectedly change its height if the user expects a standard Rectangle behavior.
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Unexpected side effect for a Rectangle user!
}
}
The Solution
Avoid the “Is-A” trap if it breaks constraints. Make both implement a common interface instead.
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private int width, height;
public Rectangle(int w, int h) { this.width = w; this.height = h; }
@Override
public int getArea() { return width * height; }
}
public class Square implements Shape {
private int side;
public Square(int side) { this.side = side; }
@Override
public int getArea() { return side * side; }
}
4. Interface Segregation Principle (ISP)
“Clients should not be forced to depend upon interfaces that they do not use.”
The Problem
A 2D Square shouldn’t be forced to implement calculateVolume().
public interface Shape {
double calculateArea();
double calculateVolume(); // 2D shapes hate this
}
The Solution
Split large interfaces into smaller, specific ones.
public interface AreaCalculatable { double calculateArea(); }
public interface VolumeCalculatable { double calculateVolume(); }
public class Square implements AreaCalculatable {
public double calculateArea() { return 10 * 10; }
}
public class Cube implements AreaCalculatable, VolumeCalculatable {
public double calculateArea() { return 600; }
public double calculateVolume() { return 1000; }
}
5. Dependency Inversion Principle (DIP)
“Depend upon abstractions, not concretions.”
The Problem
The Canvas is hard-coded to a Circle. It can’t draw anything else without code changes.
class Canvas {
private Circle circle = new Circle(); // Hard dependency (Bad!)
public void render() {
circle.draw();
}
}
The Solution
Inject the abstraction (Shape) into the Canvas.
public class Canvas {
private Shape shape;
// Dependency is injected, not hard-coded
public Canvas(Shape shape) {
this.shape = shape;
}
public void render() {
shape.draw();
}
}
Summary (The Shapes Edition)
- SRP: Circle does math; Canvas does drawing.
- OCP: Use interfaces so you can add a
Trianglewithout changing theCanvas. - LSP: Don’t force a
Squareto be aRectangleif it breaks the logic. - ISP: Don’t force a
Squareto calculateVolume. - DIP: Make the
Canvasdepend on aShapeinterface, not a specificCircle.
Would you like me to add a section on how to implement these using a specific Framework like Spring Boot?