viernes, 1 de agosto de 2014

Open Closed Principle

Como todos los desarrolladores de software sabemos, lo único constante en nuestro perfil es que siempre habrá cambios, por ello la necesidad de crear una metodología capaz de adaptarse a esos cambios y lograr un diseño que nos permita minimizar los posibles impactos de los mismos.

En este post explicaré uno de los principios Solid introducidos por Bob Martin (“Uncle Bob”), El principio Open/Closed el cual dice lo siguiente.

“Las entidades del software (clases, módulos, métodos, etc.) deben estar abiertas para extensión, pero cerradas para modificación.”

Este principio nos ayuda a tener un diseño en nuestros sistemas altamente desacoplado, ya que está basado en la creación de clases extensibles sin necesidad de modificar el código existente, es decir, se debe lograr un diseño que nos permita extender funcionalidad sin modificar lo que sabemos no se debe modificar. Sin embargo lograr esto es difícil, se debe conocer claramente cómo funciona el sistema, cómo interactúan y se comunican los componentes que lo integran y así poder predecir los puntos del sistema  en los cuales podría llegar a extenderse la funcionalidad.

Por ejemplo, tenemos un sistema de productos que se encarga de actualizar la calidad de los mismos, la calidad es solo un valor numérico que se incrementa (siendo 50 el valor máximo) o decrementa (siendo 0 el valor mínimo) según las reglas de cada producto y existen productos que no se actualizan.

En este tipo de sistemas es normal encontrarnos con algo como esto, para facilitar la explicación todos los atributos de la clase serán públicos.

class SistemaProductos{
    .
    .
    .
    public void updateQuality() {
        for (int i = 0; i < products.length; i++) {
            if (!products[i].name.equals("Increase")
                    && !products[i].name.equals("KeepQuality")) {
                if (products[i].quality > 0) {
                    products[i].quality = products[i].quality - 1;
                }
            } else {
                if (products[i].quality < 50) {
                    if (products[i].name.equals("Increase")
                            && !products[i].name.equals("KeepQuality")){
                        products[i].quality = products[i].quality + 1;                        
                    }
                }
            }         
        }
    }
}

Lo primero para poner en práctica este principio es identificar en tu diseño las partes del sistema que pueden llegar a extender su funcionalidad, en el ejemplo anterior vemos que existen diferentes requerimientos que nos informan de posibles cambios futuros como por ejemplo, productos que actualicen su calidad el doble de rápido, o productos que después de determinada calidad comiencen a disminuir en lugar de seguir aumentando, si no prestamos atención a este diseño, en un futuro podríamos tener un sistema tan grande y difícil de entender que el agregar una funcionalidad sería muy costoso, para evitar esto se puede aplicar el principio Open/Closed con ayuda de la abstracción ya sea por Herencia o con uso de interfaces, en este ejemplo, usaremos las interfaces.

Creamos una interfaz que nos ayuda a entender la tarea del sistema, QualityUpdater (No entraremos en detalle sobre nomenclatura de interfaces).

public interface QualityUpdater {
  public void updateQuality(final Item item);
}

esa interfaz formará parte de la clase producto.

public class Producto {
  public String name;

  public int quality;

  public QualityUpdater qualityUpdater;

  public Producto(final String name, final int quality) {
      this.name = name;
      this.quality = quality;
  }
}

Ahora el sistema debe hacer uso de la interfaz para actualizar el producto, y cada producto puede tener su propia versión concreta del QualityUpdater, de esa manera logramos desacoplar esa funcionalidad del sistema.

public class SustemaProductos{
   .
   .
   .

   public void updateQuality() {
      for (Producto product : products) {    
          product.qualityUpdater.updateQuality(product);
      }
   }
}

Es importante mencionar que un producto debe tener un QualityUpdater para poder actualizarse si no se agrega un QualityUpdater al producto tendremos una Excepcion.

Para los productos que aumentan su calidad tendríamos una implementación de la interfaz que contenga su propia lógica, algo como esto:

public class IncrementProduct implements QualityUpdater {
  private static final int MAX_QUALITY_PRODUCT = 50;

  @Override
  public void updateQuality(final Producto producto) {
      if (hasQualityToIncrease(producto)) {
          increaseQuality(producto);
      }
  }

  private static boolean hasQualityToIncrease(final Producto producto) {
      return producto.quality < MAX_QUALITY_PRODUCT;
  }

  private static void increaseQuality(final Producto producto) {
      producto.quality ++;
  }
}

Para los productos que disminuyen su calidad tendriamos una implementacion de la interfaz que posee su propia logica al igual que la anterior, seria asi.

public class DecrementProduct implements QualityUpdater {
  private static final int MIN_QUALITY_PRODUCT = 0;

  @Override
  public void updateQuality(final Producto producto) {
      if (hasQualityToDecrease(producto)) {
          decreaseQuality(producto);
      }
  }

  private static boolean hasQualityToDecrease(final Producto producto) {
      return producto.quality > MIN_QUALITY_PRODUCT;
  }

  private static void decreaseQuality(final Producto producto) {
      producto.quality --;
  }
}

Al agregar una nueva funcionalidad a un sistema no nos debemos olvidar de seguir soportando las funciones actuales del sistema, en el ejemplo anterior pasamos por alto que existen productos que no se actualizan, para solucionar esto creamos una función que nos informa si el producto actual es actualizable o no.

public class Producto {
  .
  .
  .

  public boolean isUpdatableQuality() {
      return qualityUpdater != null;
  }
}

Y nuestro sistema ahora sera capaz incluso de evaluar productos que no son actualizables.

public class SistemaProductos{
  .
  .
  .

  public void updateQuality() {
      for (Producto product : products) {
          if (product.isUpdatableQuality()) {
              product.qualityUpdater.updateQuality(product);
          }
       }
  }
}

Y así nuestro sistema de vuelve extendible de funcionalidad (Creando implementaciones para el QualityUpdater) sin necesidad de modificar lo existente (Para la clase sistema es transparente la implementacion de QualityUpdater que posee el producto).

En este ejemplo usamos un diseño por composición, en este tipo de diseños es muy común encontrarse con clases que parecieran no conocer todos sus atributos, es decir, la clase Producto desconoce la existencia de la interfaz QualityUpdater que posee, por ello la necesidad de llamar la interfaz dentro de la clase producto.

product.qualityUpdater.updateQuality(product);

Y no solo eso sino que la interfaz no tiene acceso directo a los atributos del producto por lo que se debe pasar como parámetro, si bien es un diseño que para muchos será incorrecto, es mejor tener un sistema desacoplado por composición que sufrir los dolores de cabeza de agregar funcionalidad a un sistema mal diseñado.

Es importante recalcar que el cambio siempre está presente y puede llegar un momento en que las necesidades del negocio puedan llegar a ser tan grandes que nos topemos con el hecho de que lo existente ya no es suficiente para cubrir todos los requerimientos, en este caso no habrá otra solución que cambiar lo existente, mas sin embargo, el impacto será menor por el alto desacoplamiento que ofrece un sistema regido por este principio.