Regístrate para acceder a más de 15 cursos gratuitos de programación con un simulador

Patrón Estado (State) JS: Polimorfismo

El patrón Estado es un claro ejemplo de reemplazo de construcciones condicionales por polimorfismo de subtipos. Se utiliza ampliamente y puede reducir realmente la complejidad del código. Vamos a analizarlo utilizando como ejemplo el comportamiento de las pantallas de los celulares.

Aunque no todos los celulares se comportan igual, para esta lección elegiremos un ejemplo específico..

El celular tiene tres estados básicos:

  1. Сelular apagado. La pantalla no responde a los toques.
  2. Сelular encendido, pero pantalla apagada. La pantalla responde solo a los toques (no a los deslizamientos) y se enciende.
  3. Сelular y pantalla encendidos. La respuesta a los toques y gestos depende de la aplicación activa.

Vamos a modelar esta lógica en una clase que se encargue de la pantalla, y agregaremos dos eventos: toque (tap) y deslizamiento (swipe).

class MobileScreen  {
  constructor() {
    // Al principio, el celular está apagado
    this.powerOn = false;
    this.screenOn = false;
  }

  // Encender el celular
  powerOn() {
    this.powerOn = true;
  }

  // Tocar la pantalla
  tap() {
    // Si el celular está apagado, no sucede nada
    if (!this.powerOn) {
      return;
    }

    // Si la pantalla estaba apagada, se debe encender
    if (!this.screenOn) {
      this.screenOn = true;
    }

    // La aplicación activa actual debe responder al evento
    this.notify('tap');
  }

  // Deslizar la pantalla
  swipe() {
    // Si el celular o la pantalla están apagados, no sucede nada
    if (!this.powerOn || !this.screenOn) {
      return;
    }

    // La aplicación activa actual debe responder al evento
    this.notify('swipe');
  }
}

Hay solo dos eventos, pero ya hay muchas construcciones condicionales. En realidad, habría muchos más eventos y todos ellos deberían tener en cuenta el estado activo del celular y la pantalla.

Si resolvemos este problema de manera directa, obtendremos una gran cantidad de construcciones condicionales en cada método de evento. Este código es muy complejo y frágil. Cambiar la cantidad de estados y agregar nuevos eventos conlleva el riesgo de introducir errores constantemente. Es difícil ver el panorama general y no perder nada.

La complejidad de este código se puede reducir significativamente mediante dos transformaciones sucesivas: la extracción explícita del estado y la introducción del polimorfismo de subtipos.

Estado explícitamente extraído

La implementación actual de la pantalla se basa en banderas. En programación, se llaman banderas a las variables que contienen valores booleanos.

constructor() {
  this.powerOn = false;
  this.screenOn = false;
}

Las banderas a menudo (pero no siempre) son un indicio de una mala arquitectura. Tienden a multiplicarse y superponerse. La lógica basada en combinaciones de diferentes banderas dificulta el análisis del código:

if (!this.powerOn || !this.screenOn) {
  return;
}

Este estilo de programación tiene un nombre: "programación basada en banderas". Así se llama al código que es difícil de entender debido a la presencia de lógica basada en la combinación de banderas. Y la presencia de banderas casi siempre lleva a esto. Todo se debe a que el número de estados en los sistemas suele ser mayor que dos. Es decir, una sola bandera nunca es suficiente.

Es posible evitar las banderas introduciendo un estado explícito del sistema. En nuestro ejemplo, es fácil ver que hay tres estados:

  • Apagado: el celular está apagado (y, por lo tanto, la pantalla también está apagada).
  • Desactivado: la pantalla está apagada (pero el celular está encendido).
  • Encendido: la pantalla está encendida.

El siguiente paso es reemplazar las banderas por una sola variable que almacene el estado actual del sistema:

class MobileScreen {
  constructor() {
    this.stateName = 'powerOff';
  }

  powerOn() {
    this.stateName = 'screenDisabled';
  }

  tap() {
    if (this.stateName === 'powerOff') {
      return;
    }

    if (this.stateName === 'screenDisabled') {
      this.stateName = 'screenOn';
    }

    this.notify('tap');
  }

  swipe() {
    if (this.stateName !== 'screenOn') {
      return;
    }

    this.notify('swipe');
  }
}

Lo más importante que sucedió en el código anterior es que desaparecieron las comprobaciones de combinación de banderas. Esto no significa que no se puedan realizar comprobaciones con varios estados a la vez, pero es mucho más fácil entender los estados del sistema que las combinaciones de banderas.

Clases de estados

Para eliminar las construcciones condicionales, necesitaremos polimorfismo. ¿En qué se basa? Gracias a la presencia de un estado explícitamente extraído, es fácil ver la dependencia del comportamiento del estado. Son los estados los que deben convertirse en clases con su propio comportamiento específico para ese estado.

A su vez, la pantalla se librará de todas las comprobaciones y comenzará a interactuar con los estados:

import PowerOffState from './states/PowerOffState.js';
import ScreenDisabledState from './states/ScreenDisabledState.js';
import ScreenOnState from './states/ScreenOnState.js';

class MobileScreen {
  constructor() {
    // La lista de estados es necesaria para cambiar entre ellos
    // De lo contrario, puede haber dependencias cíclicas dentro de los estados
    this.states = {
      powerOff: PowerOffState,
      screenDisabled: ScreenDisabledState,
      screenOn: ScreenOnState,
    }
    // Estado inicial
    // Se pasa el objeto actual para cambiar de estado (ejemplos a continuación)
    this.state = new this.states.powerOff(this);
  }

  powerOn() {
    // No nos importa el estado anterior
    // Todos los datos se almacenan en la propia pantalla
    // Los objetos de estado no tienen sus propios datos
    this.state = new this.states.screenDisabled(this);
  }

  tap() {
    this.state.tap();
  }

  swipe() {
    this.state.swipe();
  }
}

// Ten en cuenta que desde el punto de vista del código externo (usuario de la pantalla)
// nada ha cambiado.

Ahora la pantalla no hace absolutamente nada. Todo su código es la inicialización del estado inicial y la transferencia del control al estado activo actual. ¿Cómo se ven las clases de estado?

class PowerOffState {
  constructor(screen) {
    this.screen = screen;
  }

  tap() {
    // no sucede nada
  }

  swipe() {
    // no sucede nada
  }
}

El estado del celular apagado es el más simple. En este estado no hay ninguna reacción, por lo que los métodos están vacíos. Veamos EstadoDesactivado:

class ScreenDisabledState {
  constructor(screen) {
    this.screen = screen;
  }

  tap() {
    // Encender la pantalla. Se debe pasar la propia pantalla al constructor.
    this.screen.state = new this.screen.states.screenOn(this.screen);
    // Notificar a la aplicación actual sobre la activación
    this.screen.notify('tap');
  }

  swipe() {
    // no sucede nada
  }
}

Tocar la pantalla la activa. Para ello, el estado EstadoDesactivado debe cambiar al estado EstadoEncendido. Por eso se pasa la propia pantalla a cada estado. De lo contrario, no sería posible cambiarla.

Y finalmente, el último estado EstadoEncendido. Este es el único estado en el que se produce la interacción con las aplicaciones.

class ScreenOnState {
  constructor(screen) {
    this.screen = screen;
  }

  tap() {
    this.screen.notify('tap');
  }

  swipe() {
    this.screen.notify('swipe');
  }
}

Es increíble, pero el código ya no tiene ninguna estructura condicional. Ahora es fácil ver el comportamiento del teléfono ante cualquier evento en un estado específico con solo abrir la clase correspondiente. La comodidad de este enfoque implica tener más archivos y código.

Es muy importante no perder la idea principal del patrón. Las clases de estado se introducen solo para introducir el polimorfismo, pero no tienen sus propios datos para trabajar. Al final, todas las acciones se realizan en la propia pantalla, la entidad que estamos simplificando.

Para acceder completo a curso necesitas un plan básico

El plan básico te dará acceso completo a todos los cursos, ejercicios y lecciones de Códica, proyectos y acceso de por vida a la teoría de las lecciones completadas. La suscripción se puede cancelar en cualquier momento.

Obtener acceso
130
cursos
1000
ejercicios
2000+
horas de teoría
3200
test

Obtén acceso

Cursos de programación para principiantes y desarrolladores experimentados. Comienza tu aprendizaje de forma gratuita

  • 130 cursos, 2000+ horas de teoría
  • 1000 ejercicios prácticos en el navegador
  • 360 000 estudiantes
Al enviar el formulario, aceptas el «Política de privacidad» y los términos de la «Oferta», y también aceptas los «Términos y condiciones de uso»

Nuestros graduados trabajan en empresas como:

Bookmate
Health Samurai
Dualboot
ABBYY