Siguiendo con el estudio de los patrones de diseño iniciado en Patrones de diseño - 1. Definición, es el turno ahora de describir algunas de las mejores prácticas (best practices) a la hora de desarrollar nuestro software.
Unas de las más conocidas son los principios SOLID, que responden al acrónimo de:
- Single responsibility: Responsabilidad única
- Open/Closed: Abierto/Cerrado
- Liskov Substitution: Sustitución de Liskov
- Interface Segregation: Segregación de interfaces
- Dependency Inversion: Inversion de dependencias
Single responsibility Principle: Principio de responsabilidad única
Una clase debe tener una unica razón de cambio
El objetivo principal de este principio es reducir la complejidad. De esta forma, cada clase debe tener un propósito específico. Si hace demasiadas cosas, te va a tocar cambiarla constantemente, con el riesgo de impactar en otras partes aumentando.
En definitiva: keep it simple.
Open/Closed: Abierto/Cerrado
Las clases deben ser abiertas para extensiones pero cerradas para modificaciones.
Evita que el código deje de funcionar cuando implementas nuevas características.
La clase, por tanto, será abierta cuando deba extenderse, es decir, cuando se generen nuevas subclases a partir de ella que vayan a implementar comportamientos específicos.
Y, por tanto, una vez la clase desarrollada sea específica, ésta debe ser cerrada: no permitiendo modificaciones adicionales.
Si queremos modificar algo de una clase ya implementada, usaremos la extensión como mecanismo.
Aunque parezca contraintuitivo, una clase puede ser abierta y cerrada a la vez: abierta puesto que permite generar subclases mediante extensión y, cerrada porque no debe modificarse más.
Liskov Substitution: Sustitución de Liskov
Cuando extendemos una clase, debes ser capaz de pasar objetos de la subclase en lugar de objetos de la clase padre sin que el código deje de funcionar.
En definitiva, que la subclase debe ser compatible con el comportamiento de la clase padre.
Esto implica que si sobreescribimos un método en la clase hija, el nuevo método debería extender la funcionalidad del método de la clase padre, sin que esto afecte a su funcionamiento.
A diferencia de otros principios, el principio de sustitución de Liskov tiene una lista de condiciones a cumplir:
Los tipos de los parámetros en un método de una subclase deben o bien corresponder o ser más abstractos que los tipos de los parámetros de la clase padre.
El tipo del valor devuelto (return) de un método en una subclase debe corresponder o ser un subtipo del tipo del valor devuelto en el método de la clase padre.
Un método en una subclase no debe lanzar tipos de excepciones que no se espera que lance el método de la clase padre: las excepciones lanzadas por la clase hija deben ser iguales o subtipos de las que ya lanzase la clase padre.
Una subclase no debe implementar limitaciones adicionales a las condiciones iniciales de la clase padre.
Una subclase no debe reducir las limitaciones impuestas al ejecutar el método en la clase padre.
La invariante de una clase padre debe mantenerse. Por invariante se entiende aquellas características de una clase que se mantienen sin variación a lo largo de todos los objetos creados: esto garantiza que el objeto cumplirá siempre con ciertas condiciones predefinidas.
Una subclase no debe cambiar los valores de los campos private de una clase padre.
Interface Segregation: Segregación de interfaces
Ninguna clase debería depender de métodos que no usa.
De acuerdo con este principio debemos implementar solo aquellos métodos que vayamos a utilizar, limitando al máximo la complejidad de nuestras interfaces.
Es, por ende recomendable, si nos encontramos con clases que necesitan implementar múltiples comportamientos, descomponer nuestras interfaces para hacerlas todo lo específicas que podamos.
Dependency Inversion: Inversion de dependencias
Los módulos de alto nivel no deberían depender de los de bajo nivel, ambos deberían depender de abstracciones. Las abstracciones no deben depender de los detalles, los detalles deben depender de las abstracciones.
Generalmente, al diseñar y desarrollar programas se puede encontrar la distinción entre dos niveles de clases:
- Clases de bajo nivel: implementan operaciones básicas.
- Clases de alto nivel: contienen lógica de negocio compleja que requiere a las clases de bajo nivel hacer algo.
Para cumplir con este principio se recomienda seguir un ciclo de desarrollo en 3 fases:
- Describir las interfaces para las operaciones de bajo nivel sobre las que actuarán las clases de alto nivel. Las clases de alto nivel usarán estas interfaces en lugar de hacer llamadas directamente a las clases de bajo nivel.
- Hacer dependientes a las clases de alto nivel de las interfaces intermedias definidas.
- Una vez las clases de bajo nivel implementen estas interfaces, se harán dependientes del nivel de lógica de negocio, habiéndose producido una inversión de dependencias.