SOLID: Principio de Responsabilidad Única

SOLID es un acrónimo introducido por Robert C. Martin (Uncle Bob) en la década del 2000.
SOLID representa los 5 principios básicos de la programación orientada a objetos.
Aplicando los principios SOLID se consigue un código más escalable y flexible.
En este artículo vamos a ver el primero de ellos: El Principio de Única Responsabilidad.

Introducción al principio

Este principio fue introducido por Robert C. Martin, básicamente lo que dice es que una clase sólo debería tener una razón para cambiar. En palabras de Robert C. Martin:

"No debe existir más que una razón para que una clase cambie".

Por lo tanto una clase debe tener solo una responsabilidad, si una clase tiene múltiples responsabilidades hay riesgo de modificar una responsabilidad pero alterar otra al estar ambas en el mismo sitio.
Las ventajas de aplicar este principio es conseguir una alta cohesión y un bajo acoplamiento. También se evita duplicar código.
Por lo tanto hay que evitar crear clases muy grandes que hacen muchas cosas, si una clase asume más de una responsabilidad será más sensible al cambio.

Ejemplo visual

La siguiente imagen es un ejemplo de no seguir el principio de responsabilidad única:

La siguiente imagen es un ejemplo de seguir el principio de responsabilidad única:

Ejemplo de código

Fijaros en en siguiente código y pensar cuantas responsabilidades tiene:


public ProductDto GetById(int id)
{
  _logger.Debug(string.Format("Retrieving Product by Id:{0}", id));

    Product product = Context.Products.Where(p => p.ProductId == id);

    ProductDto Product = MapProductToProductDto(product);

    if (product == null)
    {
            throw new Exception("Product not found");
            _logger.Debug(string.Format("ProductId:{0} not found", id));
    }
    else
    {
        _logger.Debug(string.Format("Filling cache for product id:{0}", id));
        System.Web.HttpContext.Current.Cache.Insert("Product_" + id, product, null, 
                                                   Cache.NoAbsoluteExpiration,  
                                                   TimeSpan.FromHours(24));
        _logger.Debug(string.Format("Filled cache for product id:{0}", id));
    }

    _logger.Debug(string.Format("Retrieved Product by Id:{0}", id));

    return product;
}

¿Qué responsabilidades tiene este código?:

  • Logging, escribe trazas en un log.
  • Persistencia, recupera productos de una base de datos.
  • Mapeo, se encarga de transformar de Product a ProductDTO.
  • Cache, almacena el producto recuperado de base de datos en cache.
  • Orquestación, esta responsabilidad no se suele identificar fácilmente pero es fundamental. Tiene que haber alguien encargado de coordinar todas las acciones a realizar.

Refactorizando

Ahora fijaros en el mismo código pero refactorizado y volver a pensar cuantas reponsabilidades tiene:


public ProductDto GetById(int id)
{
    this.ProductLogger.Reading(id);

    Product product = this.Repository.GetById(id);

    ProductDto Product = this.productMapper.MapToDto(product);

    if (product == null)
    {
            throw new Exception("Product not found");
            this.ProductLogger.NotFound(id);
    }
    else
    {
        this.ProductLogger.FillingCache(id);
        this.cache.Insert("Product_" + id, product);
        this.ProductLogger.FilledCache(id);
    }

    this.ProductLogger.Readed(id);

    return product;
}

Ahora este código tiene solo una responsabilidad, coordinar todas las acciones a realizar pero no se encarga de ninguna.
Aparte es necesario crear una clase específica que se encargue de cada responsabilidad.


//Orquestacion
public class ProductService
{
   ...

   public ProductDto GetById(int id)
   {
       ...
   }
}

//Logging
public class ProductLogger
{
    ...

    public void Reading (int id)
    {
           _logger.Debug(string.Format("Reading Product by Id:{0}", id));
    }

    public void Readed(int id)
    {
           _logger.Debug(string.Format("Readed Product by Id:{0}", id));
    }

    public void NotFound(int id)
    {
           _logger.Debug(string.Format("ProductId:{0} not found", id));
    }

    ...
}

//Cache
public class Cache
{
   ...
   public void Add(string key, object obj)
   {
      System.Web.HttpContext.Current.Cache.Insert("Product_" + id, product, null, Cache.NoAbsoluteExpiration,TimeSpan.FromHours(24));
   }
   ...
}

//Mapeo
public class ProductMapper
{
   ...
   public ProductDto MapToDto(Product product)
   {
      ...      
   }
   ...
}

//Persistencia
public class ProductRepository
{
   ...
   public Product GetById(int id)
   {
      ...      
   }
   ...
}

Conclusiones

Aplicando el Principio de Responsabilidad Única conseguimos un código menos frágil a cambios, se puede modificar una responsabilidad sin afectar a las demás. De esta forma nuestro código es más flexible y escalable. Además es un código mucho más legible.