Solid principles series
- Java 21
- JUnit 5
tl;dr
Do you want to end up like this? Imagine this is not a story anymore, but your reality. How could you resolve the issues on the portion of the code without breaking the logic? And even not breaking the unit tests? Apply SOLID principles. Please keep in mind that you should already know about the basics about the OOP (encapsulation, inheritance, polymorphism, and abstraction)
What is SOLID anyway?
SOLID is : Single Resposibility Open/Closed Principle Liskov Substitution Principle Interface Segregation Principle Dependency Inversion Principle
On this first part, we will discuss about Single Responsibility and Open/Closed Principle.
Single Responsibility
As the name implies, the responsibility of a class should have only one responsibility. Say you have a class, Shape. What would you expect from this class?
- Calculate the are of the shape?
- Print the area?
- Define the logic behind the area calculation?
Exactly. None of the above. It just has to do exactly one of these, or maybe even take part of the polymorphism role.
Open / Closed Principle
Text book definition says: “An interface should be open for extension but closed for modification” So do whatever you can to comply with it, as it has some disadvantages (see below), for example:
- Use interfaces in order to move the methods to one place
- Apply static and dynamic polymorphism so you don't need to create separate objects for every different class
Disadvantages of OCP
In principle:
- You might still need some toggles according to your business case (switch, if, etc)
- You can't always achieve OCP since there will always be modifications for that "specific" interface
- What makes it hardest principle is, the dependencies we will create might take time to determine and resolve, so make sure to spend a "resonable" amount of time on it
Let’s look at the following example:
public class Shape {
public enum ShapeType {
CIRCLE, SQUARE
}
private ShapeType type;
private double radius;
private double side;
public Shape(ShapeType type, double value) {
this.type = type;
if (type == ShapeType.CIRCLE) {
this.radius = value;
} else if (type == ShapeType.SQUARE) {
this.side = value;
}
}
public double calculateArea() {
if (type == ShapeType.CIRCLE) {
return Math.PI * Math.pow(radius, 2);
} else if (type == ShapeType.SQUARE) {
return Math.pow(side, 2);
}
return 0;
}
public static void main(String[] args) {
Shape circle = new Shape(ShapeType.CIRCLE, 5);
Shape square = new Shape(ShapeType.SQUARE, 4);
System.out.println("Circle Area: " + circle.calculateArea());
System.out.println("Square Area: " + square.calculateArea());
}
}
The above class has everything opposite of SOLID. Namely, Single responsibility and Open Closed Principle.
- The class Shape makes area calculation
- The class Shape makes printing of results
- The class Shape checks different types of shapes accordingly
- The class Shape holds informations such as: radius or side, so it knows a lot about the shapes
- When you want to add a new type, say pentagon, you have to update the whole code and update it everywhere
How did I approach this?
- Separated the class Shape from the full logic by modifying it as an interface
- Created the classes Circle, and Square implemented Shape interface, and moved the logic related to the class to it
- Created the Main class, so that for calling the methods and used polymorphism in order to comply with not implementing types everywhere (like enum etc)
My solution
public class Main {
public static void main(String[] args) {
Shape circle = new Circle(5);
Shape square = new Square(4);
System.out.println("Circle Area: " + circle.calculateArea());
System.out.println("Square Area: " + square.calculateArea());
}
}
public interface Shape {
double calculateArea();
}
public class Square implements Shape {
private final double side;
public Square(double side) {
this.side = side;
}
@Override
public double calculateArea() {
return Math.pow(side, 2);
}
}
public class Circle implements Shape {
private final double radius;
@Override
public double calculateArea() {
return Math.PI * Math.pow(radius, 2);
}
public Circle(double radius) {
this.radius = radius;
}
}
This still can be improved, but might create other dependencies, for example,
- Could create interface for creating area, like AreaCalculator, then implement it in Circle and Square
- Could create implementation for printing the results
But as stated in the above disadvantages, these would still create dependencies.
How did I test this?
I have created an output stream captor to read from the log statement, and checked if the results are equal as before.
class ShapeTest {
private final PrintStream standardOut = System.out;
private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
@BeforeEach
public void setUp() {
System.setOut(new PrintStream(outputStreamCaptor));
}
@Test
void shouldCalculateAreaOfCircleCorrectly() {
// given - when
// previously Shape.main(new String[]{});
Main.main(new String[]{});
// then
assertTrue(outputStreamCaptor.toString().contains("Circle Area: 78.53981633974483"));
}
@AfterEach
public void tearDown() {
System.setOut(standardOut);
}
}
Refs:
Accessed: 26.12.2023 The Open-Closed Principle Explained https://reflectoring.io/open-closed-principle-explained/ Accessed: 26.12.2023 SOLID part 2: The Open Closed Principle https://giannisakritidis.com/blog/The-Open-Closed-Principle/