SOLID Principles

These principles establish practices that helps maintain and extend software as it grows.

S - single responsibility
O - open/closed
L - liskov substitution
I - interface segregation
D - dependency inversion

Single responsibility

A class should have only one job - it should have only one reason to change

Una clase debe ser responsable solo de una única funcionalidad.

Pensar en él si sientes que se te complica el centrarte de uno en uno con determinados aspectos de la aplicación.

Antes de aplicar

Tenemos una clase GestorEmpleados que se encarga de diversas tareas relacionadas a empleados.

public class GestorEmpleados
{
	public void AgregarEmpleado(Empleado empleado)
	{
		// añadir empleado a BBDD
	}

	public void GenerarInformeEmpleado(int empleadoId)
	{
		// generar un informe de un empleado especifico
	}
}

Esta clase viola el principio porque tiene más de una razón para cambiar.

Despues de aplicar

Para adherirnos al principio, separamos las responsabilidades en dos clases distintas. Una para la gestión de empleados y otra para generación de informes.

public class RepositorioEmpleado
{
	public void AgregarEmpleado(Empleado empleado)
	{
		// añadir empleado a BBDD
	}
}

public class ServicioInformeEmpleado
{
	public void GenerarInformeEmpleado(int empleadoId)
	{
		// generar un informe de un empleado especifico
	}
}

Open/Closed

Objects or entities should be open for extension but closed for modification

This means a class should be extendable without modifying the class itself.

Este principio no está pensado para aplicarse en el 100% de los casos.

Si una clase ya está desarrollada, testeada y productiva, existen riesgos al modificarla. En vez de cambiar el código de la clase directamente, puedes crear una subclase y modificar las partes de la clase original que quieres que se comporten de manera diferente. De esta manera no rompes el código original.

Antes de aplicar

Tenemos una clase CalculadorImpuestos que calcula basándose en el tipo de producto. Sin embargo este diseño requiere modificar la clase cada vez que se añade un nuevo tipo de producto, violando el principio.

public class Producto
{
	public string Tipo { get; set; }
	public decimal Precio { get; set; }
}

public class CalculadorImpuestos
{
	public decimal CalculadorImpuestos(Producto producto)
	{
		switch(producto.Tipo) 
		{
			case "Alimento":
				return producto.Precio * 0.05m; // 5%
			case "Electronico":
				return producto.Precio * 0.2m; // 20%
			default:
				return 0;
		}
	}
}

Despues de aplicar

Añadimos una abstraccion para extender los tipos de impuestos. Ahora si queremos añadir un nuevo tipo, solo se tiene que añadir una nueva clase concreta.

public interface IImpuestoProducto
{
	decimal CalcularImpuesto(Producto producto);
}

public class ImpuestoAlimento : IImpuestoProducto
{
	public decimal CalcularImpuesto(Producto producto)
	{
		return producto.Precio * 0.05m;
	}
}

public class ImpuestoElectronico : IImpuestoProducto
{
	public decimal CalcularImpuesto(Producto producto)
	{
		return producto.Precio * 0.2m;
	}
}

public class CalculadorImpuestos
{
	public decimal CalcularImpuesto(Producto producto, IImpuestoProducto impuestoProducto)
	{
		return impuestoProducto.CalcularImpuesto(producto);
	}
}

Liskov Substitution

When extending a class, you should be able to pass objects of the subclass in place of objects of the parent class without breaking code

Una subclase debe mantenerse compatible con el comportamiento de la superclase. Al hacer override de un metodo se debe extender el comportamiento base en vez de sustituirlo por algo completamente nuevo.

Antes de aplicar

Supongamos que tenemos una clase base Ave y sus derivadas. Esta clase base tiene un metodo Volar(). Con el tiempo añadimos clases y de repente tenemos una clase Pinguino que no puede volar. Estamos violando el principio.

public class Ave
{
	public virtual void Volar()
	{
		// implementacion para volar
	}
}

public class Aguila : Ave
{
	public override void Volar()
	{
		// implementacion especifica para el vuelo de un aguila
	}
}

public class Pinguino : Ave
{
	public override void Volar()
	{
		throw new NotImplementedException("cannot fly");
	}
}

Despues de aplicar

Para adherirnos al principio podemos crear una jerarquia de clases que no obligue a todas las subclases a ejecutar comportamientos que igual no pueden realizar.

public interface IVolador
{
	void Volar();
}

public class Ave
{
	// clase base para todas las aves
}

public class Aguila : Ave, IVolador
{
	public void Volar()
	{
		// implementacion especifica para el vuelo de un aguila
	}
}

public class Pinguino : Ave
{
	// no necesita implementar Volar() ya que no puede hacerlo
}

Normas

A diferencia del resto de principios, el de Liskov tiene una serie de normas específicas.

Los parámetros de un método en una subclase deben ser siempre igual or más abstractos que los de la superclase

Imagina que tienes un método que alimenta a gatos

feed(Cat c)

GOOD: hacer una subclase de esta clase para alimentar a cualquier animal

// de esta manera si pasan un Cat, puedes seguir alimentando gatos y es válido
feed(Animal a)

BAD: hacer una subclase y restringir más el parámetro aceptado

// BengalCat es un subtipo de Cat. que pasa si ahora queremos pasar Cat a este parámetro? no se puede y rompemos la intencion original
feed(BengalCat bc)

El valor de retorno de un método en una subclase debe ser siempre igual o más abstracto que el de la superclase

(!) Este es inverso al anterior (!).

Imagina que tienes el siguiente método. El código espera recibir un gato como resultado de ejecutarlo.

Cat BuyCat();

GOOD: hacer una subclase que devuelva un tipo específico de gato

BengalCat BuyCat();

BAD: hacer una subclase que devuelva algo más abierto que un gato, con lo que el código no sabe como proceder

// it'd break code
Animal BuyCat();

Un método en una subclase no deberia lanzar excepciones de un tipo, que el método base mismo no lanza

Las excepciones que lance deberian ser igual o en todo caso más especificas (subtipos de esas excepciones). Una excepcion de un tipo no relacionado puede romper todo el código.

Interface Segregation

A client should never be forced to implement an interface that it doesn't use, or clients shouldn't be forced to depend on methods they do not use. 

Ningún cliente debería verse forzado a depender de métodos que no utiliza. Es mejor tener muchas interfaces específicas a una general. De esta manera las clases que las implementan no se ven obligadas a implementar métodos que no utilizan.

Antes de aplicar

Pensamos que todos los proveedores de cloud ofrecen las mismas opciones y programamos en consecuencia.

public interface CloudProvider
{
	void StoreFile(string name);
	string GetFile(string name);
	void CreateServer(string region);
	List<Server> ListServers(string region);
	string GetCDNAddress();
}
public class Amazon : CloudProvider
{
	void StoreFile(string name);
	string GetFile(string name);
	void CreateServer(string region);
	List<Server> ListServers(string region);
	string GetCDNAddress();
}

El problema es que tenemos una clase que implementa interfaces que no puede resolver.

public class Dropbox : CloudProvider
{
	void StoreFile(string name);
	string GetFile(string name);
	// no existe
	void CreateServer(string region);
	// no existe
	List<Server> ListServers(string region);
	// no existe
	string GetCDNAddress();
}

Después de aplicar

Rompemos estas interfaces en partes para que cada clase implemente solo las que necesite.

public interface CloudHostingProvider
{
	void CreateServer(string region);
	List<Server> ListServers(string region);
}

public interface CDNProvider
{
	string GetCDNAddress();
}

public interface CloudStorageProvider
{
	void StoreFile(string name);
	string GetFile(string name);
}

Ahora cada clase implementa solo las que necesita.

public class Amazon : CloudHostingProvider, CDNProvider, CloudStorageProvider
{
	void StoreFile(string name);
	string GetFile(string name);
	void CreateServer(string region);
	List<Server> ListServers(string region);
	string GetCDNAddress();
}
public class Dropbox : CloudStorageProvider
{
	void StoreFile(string name);
	string GetFile(string name);
}

Dependency Inversion

Entities must depend on abstractions, not on concretions

Permite reducir el acoplamiento entre módulos de software para mejorar la mantenibilidad y la flexibilidad.

ejemplo básico

// definicion de la interfaz que representa la abstraccion
public interface IRepositorioAnimal
{
	void AgregarAnimal(Animal animal);
}

// implementacion concreta de la interfaz
public class RepositorioAnimal : IRepositorioAnimal
{
	public void AgregarAnimal(Animal animal)
	{
		// implementacion para añadirlo a la BBDD
	}
}

// consumidor de alto nivel que debe depender de la abstraccion y no de la implementacion para cumplir el principio
public class ServicioAnimal
{
	private readonly IRepositorioAnimal _repositorio;

	public ServicioAnimal(IRepositorioAnimal repositorio)
	{
		_repositorio = repositorio;
	}

	public void AgregarAnimal(Animal animal)
	{
		_repositorio.AgregarAnimal(animal);
	}
}

Este principio suena muy similar a Programa a una interfaz y no una implementacion. La diferencia es que dependency inversion hace un enfoque más fuerte sobre la abstracción. Establece que los componentes de alto nivel no deben depender de componentes de bajo nivel. Ambos deben depender de abstracciones.