Qué es el patron de diseño State

Qué es el patron de diseño State

Siguiendo nuestro curso de programación hoy veremos el patrón State que permite a un objeto alterar su comportamiento cuando cambia su estado interno. Es decir, parecerá como si el objeto hubiese cambiado sus clases.

uml_state

Aplicaciones

El patrón State se utiliza cuando:

  • El comportamiento de un objeto depende de su estado y este puede cambiar en tiempo de ejecución.
  • Las operaciones tienen sentencias condicionales anidadas que tratan con los estados. Siendo el estado normalmente una constante. La idea es mover cada rama de la lógica condicional a una clase aparte.

Ejemplo

Supónganse que una conexión de red es presentado en una implementación de TCP como TCPConnection y la conexión puede estar en uno de estos estados abierta, escuchando o cerrada. Cuando un objeto TCPConnection reciba peticiones de otros objetos debe responder de modo distinto dependiendo de cuál sea su estado. Además es vital eliminar toda lógica condicional.

Estructura

estructura_patron_state

  • Context (TCPConnection): Define la interfaz que interesa a los clientes. También mantiene una referencia a una subclase de estado concreto que representa el estado actual.
  • State (TCPState): define la interfaz para encapsular el comportamiento asociado con el estado del contexto.
  • Subclases ConcreteteState (TCPStablished, TCPListen, TCPClosed): cada subclase implementa las operaciones anteriores para el estado concreto.

Colaboraciones

  • El contexto delega las operaciones dependientes del estado al objeto que representa el estado actual.
  • El contextro podría pasarse a si mismo como parámetro.
  • Cuando el contexto es inicializado en un determinado estado, los clientes no necesitan tratar directamente con los estados. O bien el contexto o bien los estados concretos deciden cuando se pasa de una estado a otro.

Consecuencias

  • Localiza el comportamiento específico del estado y lo aísla en un objeto.
  • Se pueden añadir nuevos estados y transacciones fácilmente simplemente definiendo nuevas subclases de State.
  • Hace explicitas las transacciones entre los estados.

Código

Supongamos que tenemos un videoclub con una serie de películas. Cada película tiene una serie de estados que son diferentes. Cuando una película se estrena es novedad pero pasado un tiempo deja de serlo. El cliente quiere poder introducir nuevos tipos de película. Tenemos que huir de la lógica condicional hacia el polimorfismo.

Veamos el código:

Clase película que encapsula el comportamiento típico de una película:

public class Movie {public static final int CHILDRENS = 2;public static final int NEW_RELEASE = 1;public static final int REGULAR = 0;private String title;private int priceCode;public Movie(String title, int priceCode) {    this.title = title;    this.priceCode = priceCode;}public int getPriceCode() {    return priceCode;}public void setPriceCode(int priceCode) {    this.priceCode = priceCode;}public String getTitle() {    return title;}   }

Clase Rental o Alquiler que representa los días que una película ha estado alquilada.

public class Rental {private Movie movie;private int daysRented;public Rental(Movie movie, int daysRented) {    this.movie = movie;    this.daysRented = daysRented;}public int getDaysRented() {    return daysRented;}public Movie getMovie() {    return movie;}}

La clase Customer o Cliente que tendrá una lista de los alquileres que tiene.

public class Customer {private String name;private List<Rental> rentals = new ArrayList<Rental>();public Customer(String name) {    this.name = name;}public void addRental(Rental rental) {    rentals.add(rental);}public String getName() {    return name;    }    public String statement()     {        double totalAmount = 0;        int frequentRenterPoints = 0;        String result = "Rental Record for " + getName() + "n";        for (Rental each : rentals) {            double thisAmount = 0;            // Calcula el importe de cada alquiler            switch (each.getMovie().getPriceCode()) {                    case Movie.REGULAR:                        thisAmount += 2;                        if (each.getDaysRented() > 2)                                             thisAmount += (each.getDaysRented() - 2) * 1.5;                        break;                    case Movie.NEW_RELEASE:                        thisAmount += each.getDaysRented() * 3;                        break;                    case Movie.CHILDRENS:                        thisAmount += 1.5;                        if (each.getDaysRented() > 3)                            thisAmount += (each.getDaysRented() - 3) * 1.5;                        break;        }        // Añade los puntos de alquiler frecuente        frequentRenterPoints++;            // Un punto extra en el caso de las novedades alquiladas por un  período de dos o más días            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&   each.getDaysRented() > 1)            frequentRenterPoints++;    // Muestra el importe de esta película alquilada            result += "t" + each.getMovie().getTitle() + "t" +   String.valueOf(thisAmount) + "n";    totalAmount += thisAmount;        // Añade las líneas de total        result += "Amount owed is " + String.valueOf(totalAmount) + "n";            result += "You earned " + String.valueOf(frequentRenterPoints) + "   frequent renter points";       return result;    }}

Aplicándole el patrón

Refactorizando el código quedarían una subclase por cada estado.

public class Customer {private String name;private List<Rental> rentals = new ArrayList<Rental>();public Customer(String name) {    this.name = name;}public void addRental(Rental rental) {    rentals.add(rental);}public String getName() {    return name;}public String statement() {    String result = "Rental Record for " + getName() + "n";     for (Rental each : rentals)                 result += "t" + each.getMovie().getTitle() + "t" + String.valueOf(each.getCharge()) + "n";                result += "Amount owed is " + String.valueOf(getTotalCharge()) + "n";                result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points";           return result;}private double getTotalCharge(){    double result = 0;    for (Rental each : rentals)         result += each.getCharge();    return result;}private int getTotalFrequentRenterPoints(){    int result = 0;    for (Rental each : rentals)         result += each.getFrequentRenterPoints();    return result;}}public class Movie {public static final MovieType REGULAR = new RegularMovie();public static final MovieType CHILDRENS = new ChildrensMovie();public static final MovieType NEW_RELEASE = new NewReleaseMovie();private MovieType type;private String title;public Movie(String title, MovieType type) {    this.title = title;    this.type = type;}public void setType(MovieType type) {    this.type = type;}public String getTitle() {    return title;}   public double getCharge(int daysRented){    return type.getCharge(daysRented);}public int getFrequentRenterPoints(int daysRented){    return type.getFrequentRenterPoints(daysRented);}}

MovieType será una clase que se encargue de encapsular el estado de una Película.

public abstract class MovieType {public abstract double getCharge(int daysRented);public int getFrequentRenterPoints(int daysRented){    return 1;}}

Ahora tendremos tres subclases de MovieType que serán los estados posibles de una Película.

public class ChildrensMovie extends MovieType {@Overridepublic double getCharge(int daysRented) {    double result = 1.5;    if (daysRented > 3)        result += (daysRented - 3) * 1.5;    return result;}}public class RegularMovie extends MovieType {@Overridepublic double getCharge(int daysRented) {    double result = 2;    if (daysRented > 2)         result += (daysRented - 2) * 1.5;    return result;}}public class NewReleaseMovie extends MovieType {@Overridepublic double getCharge(int daysRented) {    return daysRented * 3;}@Overridepublic int getFrequentRenterPoints(int daysRented){    return (daysRented > 1) ? 2 : 1;}}public class Rental {private Movie movie;private int daysRented;public Rental(Movie movie, int daysRented) {    this.movie = movie;    this.daysRented = daysRented;}public int getDaysRented() {    return daysRented;}public Movie getMovie() {    return movie;}public double getCharge() {    return movie.getCharge(daysRented);}   public int getFrequentRenterPoints() {    return movie.getFrequentRenterPoints(daysRented);}}

Diagrama de clases

Vamos a explicarlo todo a partir de su diagrama de clases.

uml_state

Lo más importante es ver como los posibles estados de una película ahora son subclases de Película. Por tanto, el tratamiento de una película será dependiente del estado interno en el que se encuentre, y el estado interno en el que se encuentre dependerá de que tipo de película sea, es decir, dependerá si es una instancia de una subclase o de otra.

Si nos fijamos no solo hemos utilizado el patrón también hemos usado nuestros principios del diseño del software para refactorizar algunas de las funciones. ¿Sabrías decir cuales han sido y en que clases?

Como siempre aquí tenéis el código para que lo probéis y lo entendáis. En los siguientes artículos veremos como este patrón se comporta frente a otros patrones de comportamiento.

Para ti
Queremos saber tu opinión. ¡Comenta!