Patrón de diseño: ¿qué es y por qué utilizarlo?

El patrón de diseño es un elemento esencial en la programación orientada a objetos. Se trata de una infraestructura de software formada por un pequeño número de clases que se utiliza para resolver un problema técnico.

· 7 min de lectura
Patrón de diseño: ¿qué es y por qué utilizarlo?

⚠️ Es importante que tengas presente que esté blog se basará en parte teórica y parte práctica. Dentro de nuestra filosofía está incentivar la habilidad lectora en los programadores ya que es de valiosa utilidad.

Este artículo se basa en la explicación creada por Abhijit Saha y Tanuja Praharaj.

Un patrón de diseño proporciona una solución general reutilizable para los problemas comunes que se producen en el diseño de software.

El patrón suele mostrar las relaciones e interacciones entre clases u objetos. La idea es acelerar el proceso de desarrollo proporcionando paradigmas de desarrollo/diseño bien probados.

¿Qué es un patrón de diseño?

Los patrones de diseño son estrategias independientes del lenguaje de programación para resolver un problema común. Esto significa que un patrón de diseño representa una idea, no una implementación concreta. Al utilizar patrones de diseño, puede hacer que su código sea más flexible, reutilizable y mantenible.

Ejemplo:


En muchas situaciones del mundo real, queremos crear sólo una instancia de una clase.

Por ejemplo, sólo puede haber un presidente activo de un país en un momento dado. Este patrón se llama patrón Singleton. Otros ejemplos de software podrían ser una única conexión a la base de datos compartida por múltiples objetos, ya que crear una conexión a la base de datos distinta para cada objeto es costoso.

Del mismo modo, puede haber un único gestor de configuración o de errores en una aplicación que maneje todos los problemas en lugar de crear múltiples gestores.

via GIPHY

Tipos de patrones de diseño


Existen principalmente tres tipos de patrones de diseño:

  1. Creativos

Estos patrones de diseño tienen que ver con la instanciación de clases o la creación de objetos. Pueden clasificarse a su vez en patrones de creación de clases y patrones de creación de objetos.

Mientras que los patrones de creación de clases utilizan la herencia de forma efectiva en el proceso de instanciación, los patrones de creación de objetos utilizan la delegación de forma efectiva para realizar el trabajo.
Los patrones de diseño de creación son el Método de Fábrica, la Fábrica Abstracta, el Constructor, el Singleton, la Reserva de Objetos y el Prototipo.

Caso de uso del patrón de diseño de creación

CASO 1

Supongamos que queremos crear una simple clase DBConnection para conectarnos a una base de datos y queremos acceder a la base de datos en múltiples lugares desde el código, generalmente lo que el hacemos  es crear una instancia de la clase DBConnection y usarla para hacer operaciones con la base de datos donde sea necesario.

Esto resulta en la creación de múltiples conexiones de la base de datos ya que cada instancia de la clase DBConnection tendrá una conexión separada a la base de datos.

Para poder lidiar con esto, creamos la clase DBConnection como una clase singleton, para que sólo se cree una instancia de DBConnection y se establezca una única conexión. Debido a que podemos manejar la conexión a la base de datos a través de una instancia, podemos controlar el balance de carga, las conexiones innecesarias, entre otros.

Caso 2

Supongamos que quieres crear múltiples instancias de una clase similar y quieres conseguir un acoplamiento loose, entonces puedes optar por el patrón Factory.

Una clase que implementa el patrón de diseño de fábrica funciona como un puente entre múltiples clases. Considera un ejemplo de uso de múltiples servidores de bases de datos como SQL Server y Oracle.

Si estás desarrollando una aplicación usando la base de datos SQL Server como backend, pero en el futuro necesitas cambiar la base de datos a Oracle, necesitarás modificar todo tu código, así que como los patrones de diseño de fábrica mantienen un acoplamiento loose y una fácil implementación, deberíamos ir por el patrón de diseño de fábrica para lograr un acoplamiento y la creación de un tipo de objeto similar.

2. Estructurales
Estos patrones de diseño consisten en organizar diferentes clases y objetos para formar estructuras más grandes y proporcionar nuevas funcionalidades.
Los patrones de diseño estructural son Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Private Class Data y Proxy.

via GIPHY

Caso de uso del patrón de diseño estructural

Cuando 2 interfaces no son compatibles entre sí y se quiere establecer una relación entre ellas a través de un adaptador se llama patrón de diseño adaptador.

El patrón adaptador convierte la interfaz de una clase en otra interfaz o clase que el cliente espera, es decir, el adaptador permite que las clases trabajen juntas que de otra manera no podrían debido a la incompatibilidad, así que en estos tipos de escenarios incompatibles, podemos ir por el patrón adaptador.

3. Comportamiento
Los patrones de comportamiento consisten en identificar patrones de comunicación comunes entre los objetos y realizar estos patrones.
Los patrones de comportamiento son Cadena de responsabilidad, Comando, Intérprete, Iterador, Mediador, Memento, Objeto nulo, Observador, Estado, Estrategia, Método de plantilla, Visitante.

Caso de uso del patrón de diseño de comportamiento

El patrón de plantilla define el esqueleto de un algoritmo en una operación difiriendo algunos pasos a las subclases.

El método de la plantilla permite a las subclases redefinir ciertos pasos de un algoritmo sin cambiar la estructura del algoritmo.

Por ejemplo, en tu proyecto, quieres que el comportamiento del módulo pueda extenderse, de forma que podamos hacer que el módulo se comporte de formas nuevas y diferentes según cambien los requisitos de la aplicación, o para satisfacer las necesidades de nuevas aplicaciones; Sin embargo, no se permite hacer cambios en el código fuente, es decir, se puede añadir pero no se puede modificar la estructura en esos escenarios un desarrollador puede acercarse al patrón de diseño de plantillas.

Consideremos primero el siguiente escenario para entender el patrón del observador.

Escenario:

Supongamos que estamos construyendo una aplicación de cricket que notifica a los espectadores sobre la información como la puntuación actual, la tasa de carreras, entre otros.

En el supuesto que hemos creado dos elementos de visualización CurrentScoreDisplay y AverageScoreDisplay. CricketData tiene todos los datos (carreras, bolos, entre otros) y cada vez que los datos cambian los elementos de visualización son notificados con los nuevos datos y muestran los últimos datos en consecuencia.

A continuación se muestra la implementación en java de este diseño, es importante que tengas presente que los patrones de diseño los puedes implementar también en typescript.

// Java implementation of above design for Cricket App. The
// problems with this design are discussed below.

// A class that gets information from stadium and notifies
// display elements, CurrentScoreDisplay & AverageScoreDisplay
class CricketData
{
	int runs, wickets;
	float overs;
	CurrentScoreDisplay currentScoreDisplay;
	AverageScoreDisplay averageScoreDisplay;

	// Constructor
	public CricketData(CurrentScoreDisplay currentScoreDisplay,
					AverageScoreDisplay averageScoreDisplay)
	{
		this.currentScoreDisplay = currentScoreDisplay;
		this.averageScoreDisplay = averageScoreDisplay;
	}

	// Get latest runs from stadium
	private int getLatestRuns()
	{
		// return 90 for simplicity
		return 90;
	}

	// Get latest wickets from stadium
	private int getLatestWickets()
	{
		// return 2 for simplicity
		return 2;
	}

	// Get latest overs from stadium
	private float getLatestOvers()
	{
		// return 10.2 for simplicity
		return (float)10.2;
	}

	// This method is used update displays when data changes
	public void dataChanged()
	{
		// get latest data
		runs = getLatestRuns();
		wickets = getLatestWickets();
		overs = getLatestOvers();

		currentScoreDisplay.update(runs,wickets,overs);
		averageScoreDisplay.update(runs,wickets,overs);
	}
}

// A class to display average score. Data of this class is
// updated by CricketData
class AverageScoreDisplay
{
	private float runRate;
	private int predictedScore;

	public void update(int runs, int wickets, float overs)
	{
		this.runRate = (float)runs/overs;
		this.predictedScore = (int) (this.runRate * 50);
		display();
	}

	public void display()
	{
		System.out.println("\nAverage Score Display:\n" +
						"Run Rate: " + runRate +
						"\nPredictedScore: " + predictedScore);
	}
}

// A class to display score. Data of this class is
// updated by CricketData
class CurrentScoreDisplay
{
	private int runs, wickets;
	private float overs;

	public void update(int runs,int wickets,float overs)
	{
		this.runs = runs;
		this.wickets = wickets;
		this.overs = overs;
		display();
	}

	public void display()
	{
		System.out.println("\nCurrent Score Display: \n" +
						"Runs: " + runs +"\nWickets:"
						+ wickets + "\nOvers: " + overs );
	}
}

// Driver class
class Main
{
	public static void main(String args[])
	{
		// Create objects for testing
		AverageScoreDisplay averageScoreDisplay =
									new AverageScoreDisplay();
		CurrentScoreDisplay currentScoreDisplay =
									new CurrentScoreDisplay();

		// Pass the displays to Cricket data
		CricketData cricketData = new CricketData(currentScoreDisplay,
												averageScoreDisplay);

		// In real app you would have some logic to call this
		// function when data changes
		cricketData.dataChanged();
	}
}
Current Score Display: 
Runs: 90
Wickets:2
Overs: 10.2

Average Score Display:
Run Rate: 8.823529
PredictedScore: 441

Problemas con el diseño anterior:

CricketData mantiene referencias a objetos concretos de elementos de visualización aunque sólo necesita llamar al método de actualización de estos objetos.

Tiene acceso a demasiada información adicional de la que necesita.
Esta declaración currentScoreDisplay.update(runs,wickets,overs);" viola uno de los principios de diseño más importantes "Programar para interfaces, no para implementaciones", ya que estamos utilizando objetos concretos para compartir datos en lugar de interfaces abstractas.

CricketData y los elementos de visualización están estrechamente acoplados.
Si en el futuro surge otro requisito y necesitamos añadir otro elemento de visualización, tendremos que hacer cambios en la parte no variable de nuestro código (CricketData). Esto definitivamente no es una buena práctica de diseño y la aplicación podría no ser capaz de manejar los cambios y no es fácil de mantener.

¿Cómo evitar estos problemas?

  • Utiliza el patrón observador

Patrón observador

Para entender el patrón observador, primero hay que entender los objetos sujeto y observador.

La relación entre el sujeto y el observador puede entenderse fácilmente como una analogía con la suscripción a una revista.

Caso práctico

Un editor de revistas (sujeto) está en el negocio y publica revistas (datos).
Si usted (usuario de los datos/observador) está interesado en la revista, se suscribe (se registra) y, si se publica una nueva edición, se le entrega.


Si te das de baja, dejas de recibir nuevas ediciones.
La editorial no sabe quién eres y cómo utilizas la revista, sólo te la entrega porque eres un suscriptor (loose coupling).

Definición:

El Patrón Observador define una dependencia de uno a muchos entre los objetos, de manera que si un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente.

Explicación:

La dependencia uno a muchos es entre Sujeto(Uno) y Observador(Muchos).
Hay dependencia ya que los Observadores por sí mismos no tienen acceso a los datos. Dependen del Sujeto para que les proporcione los datos.

  • Aquí el observador y el sujeto son interfaces (puede ser cualquier supertipo abstracto, no necesariamente una interfaz java).
  • Todos los observadores que necesitan los datos necesitan implementar la interfaz del observador.
  • El método notify() en la interfaz del observador define la acción a realizar cuando el sujeto le proporciona datos.
  • El sujeto mantiene una observerCollection que es simplemente la lista de observadores actualmente registrados (suscritos).
  • registerObserver(observer) y unregisterObserver(observer) son métodos para añadir y eliminar observadores respectivamente.
  • notifyObservers() se llama cuando los datos cambian y los observadores necesitan recibir nuevos datos.


Ventajas:
Proporciona un diseño débilmente acoplado entre objetos que interactúan. Los objetos débilmente acoplados son flexibles con los requisitos cambiantes. Aquí acoplamiento loose significa que los objetos que interactúan deben tener menos información sobre los demás.

El patrón observador proporciona este acoplamiento loose como:

  • El sujeto sólo sabe que el observador implementa la interfaz del observador, nada más.
  • No hay necesidad de modificar el sujeto para añadir o eliminar observadores.

Podemos reutilizar las clases de sujeto y observador de forma independiente.

Plataforma de cursos gratis sobre programación

Artículos Relacionados

Pair Programming ¿Lo aplicas?
· 5 min de lectura
Sin Junior no hay Senior ¿Cómo podemos ayudar?
· 3 min de lectura
¿Cómo recuperar llave .pem AWS ?
· 2 min de lectura
Estructura de Carpetas en Angular 🔥
· 6 min de lectura