lunes, 21 de octubre de 2013

The Open/Close Principle (Principio Abierto/Cerrado)

The Open/Close Principle (OCP)

"Un módulo debe estar abierto para la extensión pero cerrado para la modificación."

Este post pertenece a la serie Principios SOLID.

De todos los principios de diseño este es quizá el más importante. Quiere decir que debemos diseñar nuestros módulos de forma que puedan extenderse sin tener que modificarlos. En otras palabras, tenemos que poder cambiar lo que el módulo hace sin cambiar el código fuente del módulo.

Quizá esto suene contradictorio, pero hay diferentes técnicas para conseguir aplicar este principio. Todas estas técnicas se basan en la abstracción, de hecho la abstracción es el concepto clave del OCP.
La abstracción es el concepto clave del OCP
Supongamos que nuestro cliente "Cálculos de Áreas ACME" nos solicita una aplicación para calcular la suma de las áreas de una colección de rectángulos. Esto no supone ningún problema de entrada para nosotros.
Primero escribimos nuestra clase para el rectángulo.
public class Rectangle
{
  public double Width { get; set; }
  public double Height { get; set; }
}

Después escribimos una clase para calcular el área de la colección de rectángulos.
public class AreaCalculator
{
  public double Area(Rectangle[] shapes)
  {
    double area = 0;
    foreach (var shape in shapes)
    {
       area += shape.Width * shape.Height;
    }

   return area;
  }
}

Todo perfecto, nuestra aplicación funciona. Pero antes de entregarla, el responsable de marketing de "Cálculos de Áreas ACME" nos llama para decirnos que van a hacer una gran campaña de publicidad ofreciendo el servicio de "Cálculo de áreas de círculos" y necesita que nuestra aplicación soporte también el calculo del área de círculos.

Bien. Llegamos a la conclusión que no nos costaría mucho satisfacer al cliente si hacemos algunas modificaciones al código del método Area. Escribimos primero una clase para representar al área de tipo círculo.
public class Circle
{
   public double Radius { get; set; }        
}
Y hacemos unos cambios en nuestro método Area()
public double Area(object[] shapes)
{
   double area = 0;
   foreach (var shape in shapes)
   {
      if (shape is Rectangle)
      {
          Rectangle rectangle = (Rectangle)shape;
          area += rectangle.Width * rectangle.Height;
       }
       else
       {
          Circle circle = (Circle)shape;
          area += circle.Radius * circle.Radius * Math.PI;
       }
    }

    return area;
}

¿Cual es el problema con este código?
Evidentemente la estructura de if/else en el método Area. Desde la perspectiva de OCP esta estructura nos obligará a modificar esta clase cada vez que agregamos un tipo nuevo de área a calcular.
Aunque en un ejemplo sencillo como este esto no parezca un problema muy grave. Lo habitual es que el software que se diseña de esta forma está plagado de este tipo de estructuras if/else o switch por todo el código.

Una solución que aplique OCP a nuestro problema.
Una forma de solucionar este problema es hacer que todas las áreas implementen una interfaz común de la que dependa nuestro método Area()
Escribimos la interfaz y reescribimos nuestras clases Rectangle y Circle para que implementen la interfaz.

public interface IShape
{
  double GetArea();
}

public class Rectangle : IShape
{
   public double Width { get; set; }
   public double Height { get; set; }

   public double GetArea()
   {
      return Width * Height;
   }
}

public class Circle : IShape
{
   public double Radius { get; set; }
   public double GetArea()
   {
      return Radius * Radius * Math.PI;
   }
}

Ahora reescribimos nuestra clase AreaCalculator
public class AreaCalculator
{
    public double Area(IShape[] shapes)
    {
       double area = 0;
       foreach (var shape in shapes)
       {
           area += shape.GetArea();
       }

       return area;
    }    
}

Hemos movido la responsabilidad de calcular el área de nuestra clase AreaCalculator a cada uno de los tipos de áreas (circulo y rectángulo) consiguiendo un código más robusto y haciendo que no sea necesario modificar el código de AreaCalculator si aparecen nuevos tipos de áreas a calcular.

Aplicar el OCP es la base para crear código mantenible y reutilizable

Aplicando OCP a nuestro código podemos conseguir módulos que podemos extender sin tener que cambiarlos. Es decir, podemos extender su funcionalidad sin tocar el código que ya está escrito. De esta manera es menos probable cometer errores.
Incluso si no es posible aplicar el OCP completamente, una aplicación parcial ya será un gran avance en cuanto a calidad de nuestro código.

6 comentarios:

  1. Así a primera vista parece muy sencillo, ya veremos luego en el día a día. Muy bien contado

    ResponderEliminar
    Respuestas
    1. Como para todo requiere cierta práctica y sobre todo constancia. Pero si pones estos principios en práctica quizá especialmente este de Open/Close y eres metódico verás como el diseño será una ayuda que facilite los cambios en lugar de ser una traba. Por supuesto no hay que olvidar que esto no son más que guías. Cómo los apliques y hasta que punto debes ser "purista" dependerá de cada contexto concreto y eso sin duda eres tu mismo quien mejor lo sabe.

      De cualquier forma te animo a seguir leyendo los post que pretendo seguir poniendo por que vamos a profundizar mucho más en cómo identificar y solucionar problemas de diseño en nuestras aplicaciones.

      Muchas gracias por tu comentario.

      Eliminar
  2. Entonces podría decirse que las interfaces sirven para agrupar clases

    ResponderEliminar
    Respuestas
    1. Hola Pedro.
      Aunque entiendo tu reflexión, no creo que la palabra agrupar se la más correcta para definir la finalidad de las interfaces.

      Las interfaces definen el contrato público de las clases. Es decir, definen lo que una clase hace.

      En nuestro ejemplo la interfaz IShape define un contrato público que "dice" que las clases que implementan esta interfaz pueden calcular su área.

      De alguna forma IShape es una abstracción de las clases Rectangle y Circle y en nuestra solución trabajamos con esa abstracción en lugar de trabajar con las clases reales.

      Por tanto, aunque de alguna forma mediante la interfaz IShape "agrupamos" las clases Circle y Rectangle, no es un agrupamiento en el sentido estricto sino más bien una abstracción de la funcionalidad de calcular el área.

      Muchas gracias por tu comentario.
      Te invito a que sigas leyendo más artículos del blog.
      Un saludo.

      Eliminar
  3. Excelente, muy buena explicación.
    Gracias :)

    ResponderEliminar
    Respuestas
    1. Gracias por tu comentario.
      Pronto volverán nuevos posts. Estate atenta a twitter @migueldecompeta

      Eliminar