The Open Closed Principle states that
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
The objective here is to write your code in such a manner that extending its functionality does not require you to change the existing code. The greater motive is to avoid unexpected bugs which can occur from changing the behavior of your class or method.
Why not just make a few changes?
Lets look into a small example. Below is a class which gets me the images for a product. One of its methods gets product images based on product id.
class ProductImageProvider {
public List<Image> getProductImagesByProductId(String productId) {
//get images where productId = productId
}
...
}
When we wrote this code, it was expected to behave in just one way and get me all the images. But your system faced a small bug - on the product listing, some of the products have a weird landscape image next to them. Developer 1(a newbie) goes ahead and explores how ListingPage gets it product images. He finds this class and make a small change in our method. Now, it returns only images suitable for a product listing.
class ProductImageProvider {
public List<Image> getProductImagesByProductId(String productId) {
//get images where "productId" = productId AND "view" = "listing"
}
...
}
Introduced a bigger bug - A more experienced developer would know that this method is called from several places and different types of images may be required in those places. Anyways, in this case, the bug gets corrected but now on the product display page, there is only one image displayed and customers are potentially not sure whether to buy the product or not.
Now Developer 2(a more experienced one) takes matters into his own hands. He finds out which classes are calling this method and forces the callers to send a flag which tells the getProductImages which type of images are expected.
class ProductImageProvider {
public List<Image> getProductImagesByProductId(String productId, String view) {
//get images where "productId" = productId AND "view" = view
}
...
}
Works fine but what about complexity - To correct a bug which was on one page, the developer modified the code for all pagess. Moreover, it took him time to make sure each class sent the flag correctly and then he unit tested each functionality again to make sure no new bug got introduced.
How could we solve it?
The solution is simple and it works on the fundamental principle of polymorphism. You expect your code to work differently in different situations. So why not make it more polymorphic.
Inheritance
The most basic way to resolve this is to introduce inheritance. We make a new provider that does something special for this new scenario.
class ProductImageProvider {
public List<Image> getProductImagesByProductId(String productId) {
//get images where "productId" = productId
}
...
}
class ProductListingImageProvider extends ProductImageProvider {
public List<Image> getProductImagesByProductId(String productId) {
//get images where "productId" = productId AND "view" = "Listing"
}
...
}
This is fine for one special scenario and this is what the principle was made for. But this isn't perfect either. Maybe in future you will add a similar subclass for another page. Soon, some of your pages will be calling the superclass directly and some will depend on specialized classes. Eventually it will be hard to understand these dependencies and there will be no uniformity in your code.
Interfaces
A better way to implement it would be to hide every special implementation behind an interface.
public interface ProductImageProvider {
public List<Image> getProductImagesByProductId(String productId);
}
class ProductListingImageProvider implements ProductImageProvider {
public List<Image> getProductImagesByProductId(String productId) {
//get images where "productId" = productId AND "view" = "Listing"
}
...
}
class ProductDisplayImageProvider implements ProductImageProvider {
public List<Image> getProductImagesByProductId(String productId) {
//get images where "productId" = productId AND "view" = "Product display"
}
...
}
public class ListingPage {
private ProductImageProvider productImageProvider;
private String productId;
public ListingPage(String productId, ProductImageProvider productImageProvider) {
this.productId = productId;
this.productImageProvider = productImageProvider;
}
public Product getProduct() {
List<Image> images = productImageProvider.getProductImagesByProductId(productId);
}
}
Now each page carries a reference to a ProductImageProvider. It gets its images from some implementation of a ProductImageProvider. It is not concerned with the initialization logic of the image provider. The code is fairly simple to understand and consistent for every page. We will talk more about this type of interface behavior when we discuss Dependency Inversion Principle.
Benefits
- Separation of concerns - Rather than one piece of code solving all variations of a problem, we now have separate code for each fundamental problem. This makes my code more maintainable and well encapsulated.
- Curb collateral damage - As it is often said, "If it works don't touch it". When you rarely touch code that works, you will not be breaking working features.
- Plan for flexibility - When you write your code with extensibility in mind, it offers you maximum flexibility in the long run. Often in tough situations it would be faster to switch to a different implementation than changing a piece of code which is called from several places.
For you to explore
Open closed principle and polymorphism are the building block of many design patterns. Some of these famous patterns are Decorator, Strategy, Factory, Composite and Template Method pattern. You may want to explore them as they are all easy to understand and will help you in understanding OCP better.
Top comments (0)