CodeGym /Blog Java /Random-ES /Patrón de diseño de estrategia
Autor
John Selawsky
Senior Java Developer and Tutor at LearningTree

Patrón de diseño de estrategia

Publicado en el grupo Random-ES
¡Hola! En la lección de hoy, hablaremos sobre el patrón de estrategia. En lecciones anteriores, ya nos hemos familiarizado brevemente con el concepto de herencia. En caso de que lo hayas olvidado, te recuerdo que este término se refiere a una solución estándar para una tarea de programación común. En CodeGym, a menudo decimos que puedes buscar en Google la respuesta a casi cualquier pregunta. Esto se debe a que su tarea, sea cual sea, probablemente ya haya sido resuelta con éxito por otra persona. Los patrones son soluciones probadas y verdaderas para las tareas más comunes o métodos para resolver situaciones problemáticas. Son como "ruedas" que no necesita reinventar por su cuenta, pero sí necesita saber cómo y cuándo usarlas :) Otro propósito de los patrones es promover una arquitectura uniforme. ¡Leer el código de otra persona no es tarea fácil! Todos escriben un código diferente, porque una misma tarea se puede resolver de muchas maneras. Pero el uso de patrones ayuda a diferentes programadores a comprender la lógica de programación sin profundizar en cada línea de código (¡incluso cuando lo ven por primera vez!). Hoy veremos uno de los patrones de diseño más comunes llamado "Estrategia". Patrón de diseño: Estrategia - 2Imagine que estamos escribiendo un programa que trabajará activamente con objetos de transporte. Realmente no importa qué hace exactamente nuestro programa. Hemos creado una jerarquía de clases con una clase principal Transporte y tres clases secundarias: Sedan , Truck y F1Car .

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {
}

public class Truck extends Conveyance {
}

public class F1Car extends Conveyance {
}
Las tres clases secundarias heredan dos métodos estándar del padre: go() y stop() . Nuestro programa es muy simple: nuestros autos solo pueden avanzar y aplicar los frenos. Continuando con nuestro trabajo, decidimos dar a los autos un nuevo método: llenar () (que significa "llenar el tanque de gasolina"). Lo agregamos a la clase padre Transporte :

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
  
   public void fill() {
       System.out.println("Refueling!");
   }
}
¿Pueden realmente surgir problemas en una situación tan simple? De hecho, ya tienen... Patrón de diseño: Estrategia - 3

public class Stroller extends Conveyance {

   public void fill() {
      
       // Hmm... This is a stroller for children. It doesn't need to be refueled :/
   }
}
Nuestro programa ahora tiene un medio de transporte (un cochecito de bebé) que no encaja muy bien en el concepto general. Podría tener pedales o ser controlado por radio, pero una cosa es segura: no tendrá ningún lugar para verter gasolina. Nuestra jerarquía de clases ha hecho que los métodos comunes sean heredados por clases que no los necesitan. ¿Qué debemos hacer en esta situación? Bueno, podríamos anular el método fill() en la clase Cochecito para que no suceda nada cuando intentes repostar el cochecito:

public class Stroller extends Conveyance {

   @Override
   public void fill() {
       System.out.println("A stroller cannot be refueled!");
   }
}
Pero esto difícilmente puede llamarse una solución exitosa si no es por otra razón que el código duplicado. Por ejemplo, la mayoría de las clases usarán el método de la clase principal, pero el resto se verá obligado a anularlo. Si tenemos 15 clases y debemos anular el comportamiento en 5-6 de ellas, la duplicación de código será bastante extensa. ¿Quizás las interfaces pueden ayudarnos? Por ejemplo, así:

public interface Fillable {
  
   public void fill();
}
Crearemos una interfaz Rellenable con un método fill() . Entonces, aquellos medios de transporte que necesiten repostar implementarán esta interfaz, mientras que otros medios de transporte (por ejemplo, nuestro cochecito de bebé) no lo harán. Pero esta opción no nos conviene. En el futuro, nuestra jerarquía de clases puede crecer hasta volverse muy grande (imagínese cuántos tipos diferentes de medios de transporte hay en el mundo). Abandonamos la versión anterior relacionada con la herencia, porque no queremos anular el relleno ()método muchas, muchas veces. ¡Ahora tenemos que implementarlo en cada clase! ¿Y si tenemos 50? Y si se van a realizar cambios frecuentes en nuestro programa (¡y esto es casi siempre cierto para los programas reales!), tendríamos que pasar rápidamente por las 50 clases y cambiar manualmente el comportamiento de cada una de ellas. Entonces, ¿qué debemos hacer al final en esta situación? Para resolver nuestro problema, elegiremos una forma diferente. Es decir, separaremos el comportamiento de nuestra clase de la clase misma. ¿Qué significa eso? Como sabes, cada objeto tiene un estado (un conjunto de datos) y un comportamiento (un conjunto de métodos). El comportamiento de nuestra clase de transporte consta de tres métodos: go() , stop() y fill() . Los dos primeros métodos están bien tal como están. Pero sacaremos el tercer método delClase de transporte . Esto separará el comportamiento de la clase (más precisamente, separará solo una parte del comportamiento, ya que los dos primeros métodos permanecerán donde están). Entonces, ¿dónde deberíamos poner nuestro método fill() ? No se me ocurre nada :/ Parece que está exactamente donde debería estar. Lo moveremos a una interfaz separada: FillStrategy !

public interface FillStrategy {

   public void fill();
}
¿Por qué necesitamos una interfaz así? Todo es sencillo. Ahora podemos crear varias clases que implementen esta interfaz:

public class HybridFillStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas or electricity — your choice!");
   }
}

public class F1PitstopStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas only after all other pit stop procedures are complete!");
   }
}

public class StandardFillStrategy implements FillStrategy {
   @Override
   public void fill() {
       System.out.println("Just refuel with gas!");
   }
}
Creamos tres estrategias de comportamiento: una para autos comunes, otra para híbridos y otra para autos de carreras de Fórmula 1. Cada estrategia implementa un algoritmo de reabastecimiento diferente. En nuestro caso, simplemente mostramos una cadena en la consola, pero cada método podría contener una lógica compleja. ¿Qué hacemos a continuación?

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
  
}
Usamos nuestra interfaz de FillStrategy como un campo en la clase principal Transporte . Tenga en cuenta que no estamos indicando una implementación específica, estamos usando una interfaz. Las clases de automóviles necesitarán implementaciones específicas de la interfaz de FillStrategy :

public class F1Car extends Conveyance {

   public F1Car() {
       this.fillStrategy = new F1PitstopStrategy();
   }
}

public class HybridCar extends Conveyance {

   public HybridCar() {
       this.fillStrategy = new HybridFillStrategy();
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       this.fillStrategy = new StandardFillStrategy();
   }
}

¡Veamos lo que tenemos!

public class Main {

   public static void main(String[] args) {

       Conveyance sedan = new Sedan();
       Conveyance hybrid = new HybridCar();
       Conveyance f1car = new F1Car();

       sedan.fill();
       hybrid.fill();
       f1car.fill();
   }
}
Salida de la consola:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
¡Excelente! ¡El proceso de repostaje funciona como debería! Por cierto, ¡nada nos impide usar la estrategia como parámetro en el constructor! Por ejemplo, así:

public class Conveyance {

   private FillStrategy fillStrategy;

   public Conveyance(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }

   public void fill() {
       this.fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       super(new StandardFillStrategy());
   }
}



public class HybridCar extends Conveyance {

   public HybridCar() {
       super(new HybridFillStrategy());
   }
}

public class F1Car extends Conveyance {

   public F1Car() {
       super(new F1PitstopStrategy());
   }
}
Ejecutemos nuestro método main() (que permanece sin cambios). ¡Obtenemos el mismo resultado! Salida de la consola:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
El patrón de diseño de estrategia define una familia de algoritmos, encapsula cada uno de ellos y asegura que sean intercambiables. Te permite modificar los algoritmos independientemente de cómo los utilice el cliente (esta definición, tomada del libro "Head First Design Patterns", me parece excelente). Patrón de diseño: Estrategia - 4Ya especificamos la familia de algoritmos que nos interesan (formas de repostar automóviles) en interfaces separadas con diferentes implementaciones. Los separamos del propio coche. Ahora, si necesitamos hacer algún cambio en un algoritmo de reabastecimiento de combustible en particular, no afectará nuestras clases de autos de ninguna manera. Y para lograr la intercambiabilidad, solo necesitamos agregar un único método setter a nuestra clase Transporte :

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }

   public void setFillStrategy(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }
}
Ahora podemos cambiar de estrategia sobre la marcha:

public class Main {

   public static void main(String[] args) {

       Stroller stroller= new Stroller();
       stroller.setFillStrategy(new StandardFillStrategy());

       stroller.fill();
   }
}
Si los cochecitos de bebé de repente comienzan a funcionar con gasolina, nuestro programa estará listo para manejar este escenario :) ¡Y eso es todo! Has aprendido un patrón de diseño más que sin duda será esencial y útil cuando trabajes en proyectos reales :) ¡Hasta la próxima!
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION