Full Demo in my github project.
Open-Closed Principle states that software entities must be open for extension but closed for modification. Let use the OP's example:
public class ShapeFactory {
//The purpose of self registering classes is to avoid if's
public Shape getShape(String shapeType){
if(shapeType == null){ //Get rid of this
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("SQUARE")){
return new Square();
}
return null;
}
}
Suppose that I need to add three new shapes: Triangle, Pentagon, and Hexagon. OCP tells me that I should not MODIFY the existing ShapeFactory to support these new shapes, but instead I should EXTEND the existing class to support them. So, how do I do this? I can literally extend ShapeFactory and add the three new shapes to this sublass. That would be a terrible mistake. So instead, I would create an AbstractShapeFactory that ShapeFactory as well as my new factory class will extend. THAT SAID, I prefer using interfaces over abstract classes. So, I would create a shape factory interface, and then my concrete factories will implement said interface.
Without getting too much into Liskov Substitution Principle, you have to make sure you don't end up violating this principle in the process. In short, you will end up with something like this:
interface ShapesFactory {
// Shape should also be an interface
public Shape getShape(String name);
}
public class BasicShapesFactory implements ShapesFactory {
@Override
public Shape getShape(String name){
String errorMsg = name + " is not a legal name for a Shape.";
if(name == null){ //Get rid of this
throw new IllegalShapeException(errorMsg);
}
switch(name.toUpperCase()) {
case "CIRCLE":
return new Circle();
case "RECTANGLE":
return new Rectangle();
case "SQUARE":
return new Square();
default:
throw new IllegalShapeException(errorMsg);
}
}
}
public class AdvancedShapesFactory implements ShapesFactory {
@Override
public Shape getShape(String name){
String errorMsg = name + " is not a legal name for a Shape.";
// This null check could be extracted to a default function in the interface
if(name == null){ //Get rid of this
throw new IllegalShapeException(errorMsg);
}
switch(name.toUpperCase()) {
case "TRIANGLE":
return new Triangle();
case "PENTAGON":
return new Pentagon();
case "HEXAGON":
return new Hexagon();
default:
throw new IllegalShapeException(errorMsg);
}
}
}
Now that you have this structure, you can either create Abstract Factory that will provide a factory for basic shapes or advances shapes.
I think it is misconception that adding if statements is a violation of OCP. The code is going to be modified. Adding classes is a modification of existing code. The real problem is WHERE and WHY the modification occur. You get no argument from me that using something like a Bean Factory is a better solution, but the concept is the same. With OCP, all you are doing is protecting what "ShapeFactory" is by extending it, and not by modifying it.