Monday, July 18, 2011

The Open-Closed Principle

In my previous post I covered the Single Responsibility principle which helps to provide cohesive interfaces to objects. An interface with high cohesion usually delivers low coupling or dependency with other interfaces. Also, these attributes provide maintainability and readability of our design. I would like to continue the series of the S.O.L.I.D. Object Oriented Design Principles; the second of them and my most favorite: the Open-Closed principle.

Every programmer who has used an OOP language knows fundamental concepts that define this paradigm, but the question is, how well these concepts are used. The Open-Closed principle is at the heart of many claims made for Object Oriented Design.

This principle targets the foundations of abstraction and polymorphism. A module should be open for extension but closed for modification.

Open for Extension:
The behavior of the module can be extended. We can make the module behave in NEW and DIFFERENT ways as the requirements of the application change.

Closed for Modification:
The source code of such module is INVIOLATE. No one is allowed to make source code changes to it.

Abstaction is the Key. The following UML class diagrams picture this principle:

The Client class depends directly on the Server class. Extending Server implementations is difficult and further changes may introduce complexity in code maintainability.

The Client class now depends on an abstraction of the Service by the Service Base Class. We can extend further implementation from this abstraction.

Code Sample:
Despite the following code is in C# it can be easily ported to Java or other OOP language.

Wrong implementation of this principle: 

   1:  class Point
   2:  {
   3:      private int x;
   4:      private int y;
   5:   
   6:      public Point(int x, int y)
   7:      {
   8:          this.x = x;
   9:          this.y = y;
  10:      }
  11:   
  12:      public int X
  13:      {
  14:          get { return this.x; }
  15:          set { this.x = value; }
  16:      }
  17:   
  18:      public int Y
  19:      {
  20:          get { return this.y; }
  21:          set { this.y = value; }
  22:      }
  23:   
  24:      public override string ToString()
  25:      {
  26:          return string.Format("X = {0}, Y = {1}", this.X, this.Y);
  27:      }
  28:  }
The class Point contains the coordinates X and Y.

Now we define the Circle and Square classes that we will use to draw instances of these classes in a canvas panel.
   1:  class Circle
   2:  {
   3:      public Circle(double radius, Point center)
   4:      {
   5:          this.Radius = radius;
   6:          this.Center = center;
   7:      }
   8:      public double Radius { get; set; }
   9:      public Point Center { get; set; }
  10:  }
  11:   
  12:  class Square
  13:  {
  14:      public Square(double side, Point topLeft)
  15:      {
  16:          this.Side = side;
  17:          this.TopLeft = topLeft;
  18:      }
  19:      public double Side { get; set; }
  20:      public Point TopLeft { get; set; }
  21:  }

We define the Canvas class where we draw instances of the Circle and Square classes.
   1:  class Canvas
   2:  {
   3:      void DrawSquare(Square square)
   4:      {
   5:          Console.WriteLine(string.Format(
   6:              "Square with side {0} and top left {1}",
   7:              square.Side, square.TopLeft));
   8:      }
   9:   
  10:      void DrawCircle(Circle circle)
  11:      {
  12:          Console.WriteLine(string.Format(
  13:              "Circle with radius {0} and center {1}",
  14:              circle.Radius, circle.Center));
  15:      }
  16:   
  17:      public void DrawAllShapes(object[] shapes)
  18:      {
  19:          for (int i = 0; i < shapes.Length; i++)
  20:          {
  21:              if (shapes[i].GetType() == typeof(Circle))
  22:              {
  23:                  this.DrawCircle((Circle)shapes[i]);
  24:              }
  25:              else if (shapes[i].GetType() == typeof(Square))
  26:              {
  27:                  this.DrawSquare((Square)shapes[i]);
  28:              }
  29:          }
  30:      }
  31:  }

We can see that the Canvas class implements methods to display each one of the figures; DrawSquare and DrawCircle. The public method DrawAllShapes receives an array of objects that will contain Square and Circle instances. The method checks for the type of each object to call the corresponding drawing method.

A major problem would be if we define the Triangle class because the Canvas class will need to be modified as well. We would need to implement a new method to draw Triangle objects and the DrawAllShapes method would need to change accordingly to call such method. This would happen for any new figure we want to add to our application.

This design breaks the Open-Closed principle clearly. Each time we need to extend our application by adding a new class, we need to modify other pre-existing classes (high coupling).

Good implementation of this principle:

We keep the Point class definition as we saw in the wrong implementation, the principle was not affected by this class after all. As specified previously, abstraction is the key, thus abstracting the common behavior of the figures will help to implement this principle properly.

An abstract class Shape defines the method Draw to be implemented by any specialization of this class.
   1:  abstract class Shape
   2:  {
   3:      public abstract void Draw();
   4:  }

Now, the classes Circle and Square inherit from the Shape class and each one is responsible to implement the specifics of their Draw method, hence each class knows how to draw itself, each class is the information expert of how to display itself in the canvas.
   1:  class Circle : Shape
   2:  {
   3:      public Circle(double radius, Point center)
   4:      {
   5:          this.Radius = radius;
   6:          this.Center = center;
   7:      }
   8:      public double Radius { get; set; }
   9:      public Point Center { get; set; }
  10:   
  11:      public override void Draw()
  12:      {
  13:          Console.WriteLine(
  14:              string.Format("Circle with radius {0} and center {1}",
  15:              this.Radius, this.Center));
  16:      }
  17:  }
  18:   
  19:  class Square : Shape
  20:  {
  21:      public Square(double side, Point topLeft)
  22:      {
  23:          this.Side = side;
  24:          this.TopLeft = topLeft;
  25:      }
  26:      public double Side { get; set; }
  27:      public Point TopLeft { get; set; }
  28:   
  29:      public override void Draw()
  30:      {
  31:          Console.WriteLine(
  32:              string.Format("Square with side {0} and top left {1}",
  33:              this.Side, this.TopLeft));
  34:      }
  35:  }

We remove the DrawCircle and DrawSquare methods from the class Canvas and modify the DrawAllShapes method whose parameter now is an array of the Shape type. Internally DrawAllShapes calls the method Draw of each of the Shape instances polymorphically.


   1:  class Canvas
   2:  {
   3:   
   4:      public void DrawAllShapes(Shape[] shapes)
   5:      {
   6:          for (int i = 0; i < shapes.Length; i++)
   7:          {
   8:              shapes[i].Draw();
   9:          }
  10:      }
  11:  }

It is now obvious that by using polymorphism we don't need to modify the class Canvas every single time that we add a new class that extends from the Shape class. We can define a new Triangle class that inherits from Shape without the need to modify Canvas. We have achieved the Open-Closed principle as our design is Open for extension in regards of adding new shapes and Closed for modification in terms of the canvas.

Conclusions:
We have seen how the Open-Closed principle uses inheritance and polymorphism, two core foundations of the OOP paradigm, and how this principle helps considerably with code maintenance and readability. Also, we could see how by abstracting and separating responsibilities our design was improved and ready to be extended harmlessly.

My next post will talk about the Liskov Substitution principle, see you soon!

[RobertMartin96] The Open-Closed Principle, Robert Martin, 1996

5 comments:

  1. this seems to be a basic feature supported in Java - using interface to extract the common features from all concrete implementation class...

    ReplyDelete
  2. The UML class diagrams show the client as a container for the server.

    My question is, shouldn't the Server be the container, and there be multiple clients associated with the Server?

    Les.

    ReplyDelete
  3. Hi JAVIER NAVARRO

    In the frist figure you mentioned "The Client class depends directly on the Server class" but it should be "The Sever class depend on the client Class " according to above diagram.

    In Aggregation,
    Client has a Server, and Server can outlive Client,

    am is right ?

    ReplyDelete