Over a million developers have joined DZone.

Thoughts On Open/Close Design Principles

The Open/Close design principle states that applications should be open for extension, but closed for modification. What are some ways we can maintain this design pattern and make sure our code is maintainable?

· Java Zone

Microservices! They are everywhere, or at least, the term is. When should you use a microservice architecture? What factors should be considered when making that decision? Do the benefits outweigh the costs? Why is everyone so excited about them, anyway?  Brought to you in partnership with IBM.

According to Bertrand Meyer: “Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

Reading it the first time, one can think the definition is a bit abstract, as design principles are. But the statement is very powerful and can save you from fragile design and wired bugs.

The statement says that design should be done in such a way so it can welcome new changes, but without tampering the old implementation. Because your old implementation is tested and runs fine in production, to welcome new changes it is not good to break old ones. So this principle tells us to welcome new changes on top of old changes.

Let take an example, suppose a requirement is to create a class which determines the area of a Rectangle. This is very easy to develop for an OO Developer. The initial code looks like this:

package com.example.openclose;

public class AreaBuilder {

       public void calculateRectangleArea(int length,int breadth)
       {
              System.out.println("Area of Rectangle is " + length*breadth);
       }

       public static void main(String[] args) {

              AreaBuilder builder = new AreaBuilder();
              builder.calculateRectangleArea(20, 10);


       }

}

The code is tested and deployed. But after a few days, a new feature request states that now this class should able to compute the area of a square also.

Inexperienced developers will incorporate the requirements in following way:

package com.example.openclose;

public class AreaBuilder {

       private void calculateRectangleArea(int length,int breadth)
       {
              System.out.println("Area of Rectangle is " + length*breadth);
       }


       private void calculateSquareArea(int length)
       {
              System.out.println("Area of Square is " + length*length);
       }

       public void getArea(String type,int length,int breadth)
       {
              if("square".equalsIgnoreCase(type))
              {
                     calculateSquareArea(length);
              }
              else if("rectangle".equalsIgnoreCase(type))
              {
                     calculateRectangleArea(length,breadth);
              }
       }
       public void getArea(String type,int length)
       {
              getArea(type,length,length);
       }




       public static void main(String[] args) {

              AreaBuilder builder = new AreaBuilder();
              //builder.calculateRectangleArea(20, 10);
              builder.getArea("square", 10);
              builder.getArea("rectangle", 10,5);


       }

}

Please note that if we call single argument version gerArea() then we do not need to create an another method for square as Square is a kind of Rectangle but as general (for say other shapes like a circle) developer does the above implementation. 

It will certainly meet the requirements but break the Open/Close principle. Also, this code has some shortcomings:

  1. To incorporate new changes, we have to modify the old class so it breaks the statement “closed for modification”.
  2. The new code can break old functionality if the implementation is wrong, so we not only fail to deliver new code but also break the old functionality, which is not intended.
  3. For every new shape added to the system, we need to modify this class and make the class more complex and less cohesive.

To overcome aforesaid problems, we try to follow our principles to refactor the code.

“Open for Extension, But Closed for Modification”

In Java, extensions can be achieved through inheritance. So we can create an interface called “Shape” and a method called area() in it, so every shape can implement the area method and provide its implementation.

So if a new shape needs to be created we just create a new class and implement the shape interface. If anything goes wrong, only that shape class will be impacted not the previous implementations, that is why the Open/Close principle is so important in design.

Refactored code:

package com.example.openclose;

public interface Shape {

       public void area();

}

package com.example.openclose;

public class Rectangle implements Shape{
       private int length;
       private int breadth;
       Rectangle(int length,int breadth)
       {
              this.length=length;
              this.breadth=breadth;
       }

       @Override
       public void area() {
              System.out.println("Area of Rectangle is " + length*breadth);

       }

}

package com.example.openclose;

public class Square implements Shape{

       private int edge;
       public Square(int edge)
       {
              this.edge=edge;
       }
       @Override
       public void area() {
              System.out.println("Area of Square is " + edge*edge);

       }

}

package com.example.openclose;

public class ShapeManager {

       private static ShapeManager manager = new ShapeManager();

       Shape shape;

       private ShapeManager()
       {

       }

       public static ShapeManager getInstance()
       {
              return manager;
       }


       public void getArea() throws IllegalArgumentException
       {
              if(shape ==null)
              {
                     throw new IllegalArgumentException("please provide a Shape");
              }
              shape.area();
       }


       public Shape getShape() {
              return shape;
       }



       public void setShape(Shape shape) {
              this.shape = shape;
       }



       public static void main(String[] args) {

              Shape rect = new Rectangle(10,20);
              Shape square = new Square(10);

              ShapeManager instance = ShapeManager.getInstance();
              instance.setShape(rect);
              instance.getArea();

              instance.setShape(square);
              instance.getArea();


       }

}

Output :

 Area of Rectangle is 200
Area of Square is 100

Hooray, we refactored the code and it is maintaining the principle. We can incorporate any number of shapes by adding new classes and without modifying the old classes.

The client is happy, so now they want to introduce a brand new feature: shapes can be drawn on a piece of paper.

So an inexperienced developer thinks that it will be an easy task to just add a new method called drawShape() in the shape interface, and that it will meet client needs without breaking the Open/Close principle.

Let’s try that:

package com.example.openclose;

public interface Shape {

       public void area();

       public void drawShape();

}

package com.example.openclose;

public class Rectangle implements Shape{
       private int length;
       private int breadth;
       Rectangle(int length,int breadth)
       {
              this.length=length;
              this.breadth=breadth;
       }

       @Override
       public void area() {
              System.out.println("Area of Rectangle is " + length*breadth);

       }

       @Override
       public void drawShape() {

              System.out.println("drawing recangle on paper" );

       }

}
 package com.example.openclose;

public class Square implements Shape{

       private int edge;
       public Square(int edge)
       {
              this.edge=edge;
       }
       @Override
       public void area() {
              System.out.println("Area of Square is " + edge*edge);

       }
       @Override
       public void drawShape() {
              System.out.println("drawing square on paper" );

       }

}

package com.example.openclose;

public class ShapeManager {

       private static ShapeManager manager = new ShapeManager();

       Shape shape;

       private ShapeManager()
       {

       }

       public static ShapeManager getInstance()
       {
              return manager;
       }


       public void getArea() throws IllegalArgumentException
       {
              if(shape ==null)
              {
                     throw new IllegalArgumentException("please provide a Shape");
              }
              shape.area();
       }

       public void draw() throws IllegalArgumentException
       {
              if(shape ==null)
              {
                     throw new IllegalArgumentException("please provide a Shape");
              }
              shape.drawShape();
       }


       public Shape getShape() {
              return shape;
       }



       public void setShape(Shape shape) {
              this.shape = shape;
       }



       public static void main(String[] args) {

              Shape rect = new Rectangle(10,20);
              Shape square = new Square(10);

              ShapeManager instance = ShapeManager.getInstance();
              instance.setShape(rect);
              instance.getArea();
              instance.draw();

              instance.setShape(square);
              instance.getArea();
              instance.draw();


       }

}

Output :
Area of Rectangle is 200
drawing recangle on paper
Area of Square is 100
drawing square on paper

So far, so good, but does it really maintain the Open/Close principle? At first glance, yes but if we put our thinking cap, there is a clue in the client's needs which can break this principle.

Clients want shapes to be drawn on a piece of paper, but it can be drawn on a Computer or a canvas too.

So in the future, if clients want to draw on the computer, we need to prepare for it here. To incorporate this change, the Open/Close principle will be broken. As before, we need to put an if /else check on each drawshape() method.

Now we will ask, "How judiciously can we design Open/Close principle?"

  1. I classified the object's properties in two ways: a) primary properties, and b) composition properties.
  2. If any functionality (method) depends on only primary properties, which are the sole properties of that domain object. You can declare them in the interface or abstract an extension from which this class inherited.
  3. If a functionality depends on an external entity, always use composition rather than inheritance. (Here, draw functionality depends on paper, an external entity, so we should use composition rather than inheritance).

Now, refactor the code again to support the draw function:

package com.example.openclose;

public interface Shape {

       public void area();

       public void drawShape();

}

package com.example.openclose;

public interface DrawAPI {

       public String draw();

}

package com.example.openclose;

public class PaperDraw implements DrawAPI{

       @Override
       public String draw() {
              return "paper";

       }

}

package com.example.openclose;

public class ComputerDraw implements DrawAPI{

       @Override
       public String draw() {
              // TODO Auto-generated method stub
              return "computer";
       }

}

package com.example.openclose;

public class Rectangle implements Shape{
       private int length;
       private int breadth;
       DrawAPI api;
       Rectangle(int length,int breadth,DrawAPI api)
       {
              this.length=length;
              this.breadth=breadth;
              this.api=api;
       }

       @Override
       public void area() {
              System.out.println("Area of Rectangle is " + length*breadth);

       }

       @Override
       public void drawShape() {

              String medium = api.draw();
              System.out.println("drawing recangle on " + medium);

       }



}

package com.example.openclose;

public class Square implements Shape{

       private int edge;
       DrawAPI api;
       public Square(int edge,DrawAPI api)
       {
              this.edge=edge;
              this.api=api;
       }
       @Override
       public void area() {
              System.out.println("Area of Square is " + edge*edge);

       }
       @Override
       public void drawShape() {
              String medium=api.draw();
              System.out.println("drawing square on "+medium );

       }

}

package com.example.openclose;

public class ShapeManager {

       private static ShapeManager manager = new ShapeManager();

       Shape shape;

       private ShapeManager()
       {

       }

       public static ShapeManager getInstance()
       {
              return manager;
       }


       public void getArea() throws IllegalArgumentException
       {
              if(shape ==null)
              {
                     throw new IllegalArgumentException("please provide a Shape");
              }
              shape.area();
       }

       public void draw() throws IllegalArgumentException
       {
              if(shape ==null)
              {
                     throw new IllegalArgumentException("please provide a Shape");
              }
              shape.drawShape();
       }


       public Shape getShape() {
              return shape;
       }



       public void setShape(Shape shape) {
              this.shape = shape;
       }



       public static void main(String[] args) {

              Shape rect = new Rectangle(10,20,new ComputerDraw());
              Shape square = new Square(10,new PaperDraw());

              ShapeManager instance = ShapeManager.getInstance();
              instance.setShape(rect);
              instance.getArea();
              instance.draw();

              instance.setShape(square);
              instance.getArea();
              instance.draw();


       }

}

Output :
Area of Rectangle is 200
drawing rectangle on computer
Area of Square is 100
drawing square on paper

Now the draw() method is in the shape class, as we assume every shape is drawable, and we create a composition between the Shape family and the DrawAPI family.

But to maintain the Open/Close principle, we need to create a lot of classes, which makes code complex.

I am throwing an open question: If a client is reluctant to change, or development time is short, or if you're working with a system where change requests are rare, then should we always maintain the open/close principle, or break that rule and make the code much simpler?

Discover how the Watson team is further developing SDKs in Java, Node.js, Python, iOS, and Android to access these services and make programming easy. Brought to you in partnership with IBM.

Topics:
design by contract ,design pattern ,java

Published at DZone with permission of Shamik Mitra, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}