⚠️ 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.
Tipos de patrones de diseño
Existen principalmente tres tipos de patrones de diseño:
- 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.
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óncurrentScoreDisplay.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) yunregisterObserver
(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.