Ahora sí vamos a entrar en materia, y qué mejor forma de hacerlo que con uno de los patrones de diseño más sencillos: Patrón Estrategia, pero que nos soluciona muchos problemas (especialmente si nos piden cambios) cuando se implementa de forma correcta.
Como habíamos comentado en el post anterior, los patrones de diseño son un conjunto de principios o buenas prácticas de desarrollo orientado a objetos. Sin embargo, a veces pensamos que, con solo saber qué es encapsular, herencia y polimorfismo, estamos haciendo un buen trabajo.
Mi intención es poderles ofrecer ejemplos que estén un poco más apegados a la realidad, voy a tratar de no usar juegos o escenarios poco reales porque considero que, como ejemplos, son buenos, pero son difíciles de traducir a casos más reales. Vamos a discutir juntos un pequeño ejemplo de buenas prácticas de programación: Salvando Archivos.
Pensemos que nos piden desarrollar una aplicación que se encargue de salvar archivos en diferentes formatos, como por ejemplo: texto, PDF, XML, CSV, Imagen, etc. En este caso, usamos una clase padre, llamada MyFile, que contiene un único método llamado save(). Luego creamos las diferentes implementaciones (hijos) para esta clase que nos va a definir cómo se salva cada una de ellas (sobre escribimos el método save).
¡Nuestra aplicación esta lista! Todo un ejemplo de herencia y encapsulamiento. Pero sorpresa, luego de una reunión con los usuarios, le han solicitado al Project Manager unos cambios, necesitan poder exportar los archivos actuales a otros formatos. Por ejemplo, si tengo un archivo texto, poderlo salvar como PDF. Y además, necesitan que los archivos se puedan traducir en diferentes idiomas….
Interesante, no hay razón para preocuparse. ¿Qúe les parece si agregamos dos métodos más a la clase MyFile? Uno que sea saveAs() que nos solucione el poder salvar en diferentes formatos, y además el método translate() que se encargará de traducir el archivo a cualquier idioma.
¡Claro!… ¡que no! Se nos olvida los archivos de imagen, ¿cómo traducir una imagen, y qué pasa si en un futuro agregamos un archivo de video sin audio? Pues en ese caso, ¿qué tal si creamos una interfaz que sólo sea implementada por los archivos que sí se puedan traducir? Suena razonable y además estoy utilizando buenas prácticas de orientación a objetos también.
¿Qué opinan de esta solución? Estamos utilizando herencia, encapsulamiento e inclusive logramos crear una interfaz. Podríamos decir que es un diseño robusto, ¿cierto?
Lastimosamente, la respuesta es no. Ninguna de las soluciones anteriores resuelve el problema principal: evitar cambios excesivos en clases superiores y en las implementaciones. Por ejemplo, ¿qué pasa si agregamos 10 tipos de archivos más y la forma en que se traducen cambia? Tendríamos que ir a cada implementación y modificar el método translate().
No dejemos de lado el método saveAs(), cada vez que nos pidan agregar un nuevo tipo de archivo, habría que modificar ese método en todas las implementaciones para darle soporte al nuevo tipo. Lo cual puede introducir más errores en la aplicación.
Recordemos que nuestro trabajo como creadores de aplicaciones es lograr que funcionen, pero además, que sean mantenibles. ¿Cuántas veces revisamos código hecho por alguien más y continuamente nos perdemos en él, luego de poco tiempo ya ni sabemos que hacían las 3 líneas anteriores? Pues esto y más lo soluciona el patrón de diseño adecuado, en este caso el patrón estrategia.
Veamos lo que nos dicen los 3 principios del patrón estrategia según el libro “Head First Design Patterns“:
- Identifique las partes de su aplicación que cambian constantemente y apártelas de lo que se mantiene igual.
- Programe en las interfaces, no en las implementaciones.
- Si debe escoger entre herencia y composición, prefiera composición.
En este caso, lo que siempre cambia va a ser la forma en que se salvan los archivos y además cómo se traduce cada uno. Esas cosas que cambian se les conoce como comportamientos. Cada ser humano se comporta diferente ante la misma situación. Los objetos son muy parecidos, tienen sus comportamientos que hacen que cambien según el escenario. En este caso, lo que siempre puede cambiar, es la forma de salvar archivos y cómo traducirlos:
Esos comportamientos los debemos abstraer y separar usando interfaces. (En mi opinión, las clases abstractas no son comportamientos, ya que, son objetos en sí mismas, mientras que, las interfaces, nos brindan comportamientos que llamamos métodos. Sin embargo, en este ejemplo, se podría utilizar cualquiera, yo prefiero interfaces, ya que, es una opción un poco más elegante).
El paso #2 se refiere a agregar las cosas que cambian en interfaces (de nuevo, dependiendo del escenario, pueden ser interfaces o clases abstractas). Aquí es donde el patrón toma más sentido, ya que, al programar en otras clases lo que cambia, nos garantizamos un único punto en el que debemos realizar las implementaciones, en caso de que se agregan más y nuevos requerimientos.
Para finalizar, el paso #3 nos indica que en lugar de utilizar herencia, es mejor crear composiciones de objetos, ya que, los cambios son más fáciles de controlar por medio de una relación tipo “Tiene un” que al utilizar “Es un”.
¡Ya tenemos listo nuestro diseño! Ahora sí podemos decir que estamos siguiendo los 3 principios que utiliza el patrón estrategia. Lo más importante es que logramos pasar de un diseño difícil de cambiar sin afectar varios lugares, a un modelo un poco más robusto que nos da esa flexibilidad que necesitábamos.
Podemos realizar nuestras pruebas en código (recuerden, es un patrón de diseño por ende puede utilizarse cualquier lenguaje de programación mientras permita realizar herencia/implementaciones y asignaciones por composición). Por ejemplo el código de abajo esta hecho en Java 8:
public static void main(String[] args) {
// create ISaveable behavior
ISaveable saveTxt = new SaveTxt();
// create ITranslateable behavior
ITranslateable translate = new Spanish();
// create MyFile object using the 2 default behaviors
MyFile myFile = new MyFile(saveTxt, translate);
// see the current configuration
System.out.println("Using default settings.");
System.out.println(myFile.saveMyFileAs());
System.out.println(myFile.trasnlateMyFile());
System.out.println();
// now changing the export to PDF at runtime.
// notice that this is the only place that changes.
myFile.setSaveFormat(new SavePDF());
System.out.println("Changing file format as needed.");
// get the new format
System.out.println(myFile.saveMyFileAs());
System.out.println();
// now changing the translate settings
myFile.setTranslate(new Chinese());
System.out.println("Translating to new language");
// get the new translation
System.out.println(myFile.trasnlateMyFile());
}
En el código, podemos ver cómo creamos dos comportamientos usando las interfaces para definir el tipo de dato y las inicializamos con las implementaciones específicas que queremos, en este caso yo escogí archivo TXT y lenguaje español.
Luego, creamos la clase MyFile usando esos dos comportamientos. Por defecto, se creó un constructor que recibe estos valores para asegurarnos que siempre se inicialice con al menos esos dos comportamientos. Una vez creado el objeto, podemos acceder a las acciones de cada comportamiento y obtener el resultado.
Para finalizar, podemos cambiar la forma en que se comporta MyFile, por medio de los métodos “set” definidos para cada comportamiento. Esto sin necesidad de modificar las implementaciones o varias partes del código. Abajo se muestra el resultado de correr nuestra aplicación:
Recuerden que los patrones deben de ser adaptados a cada caso, es decir, no son diseños que “funcionan” en cualquier escenario sin modificarlos. A veces, es necesario realizar pequeños cambios para poderlos adaptar, pero, lo más importante, es seguir los principios que cada patrón define tanto como sea posible.
En este link pueden descargar el código completo en Java de este ejemplo para el Patrón Estrategia
En el siguiente post hablaremos del Patrón Singleton.
Recordá suscribirte aquí para recibir las últimas actualizaciones todas las semanas.
Referencias:
Libro Head First Design Patterns. Versión digital.