State Pattern

It's possible for some objects to behave completely differently when they are in different states. Let's think about an easy example first. Consider rendering and interacting with a custom button in two states: enabled and disabled. When the button is enabled, it lights up and changes its style to active on a mouse hover, and of course, it handles clicks; when disabled, it dims and no longer cares about mouse events.

We may think of an abstraction with two operations: render (with a parameter that indicates whether the mouse is hovering) and click; along with two states: enabled and disabled. We can even divide deeper and have state active, but that won't be necessary in our case.

State Pattern

And now we can have StateEnabled with both render and click methods implemented, while having StateDisabled with only render method implemented because it does not care about the hover parameter. In this example, we are expecting every method of the states being callable. So we can have the abstract class State with empty render and click methods.

Participants

The participants of State Pattern include the following:

  • State

    Defines the interface of state objects that are being switched to internally.

  • Concrete state: StateEnabled and StateDisabled

    Implements the State interface with behavior corresponding to a specific state of the context. May have an optional reference back to its context.

  • Context

    Manages references to different states, and makes operations defined on the active one.

Pattern scope

State Pattern usually applies to the code of scopes with the size of a feature. It does not specify whom to transfer the state of context: it could be either the context itself, the state methods, or code that controls context.

Implementation

Start with the State interface (it could also be an abstract class if there are operations or logic to share):

interface State { 
  render(hover: boolean): void; 
  click(): void; 
} 

With the State interface defined, we can move to Context and sketch its outline:

class Context { 
  $element: JQuery; 
 
  state: State; 
 
  private render(hover: boolean): void { 
    this.state.render(hover); 
  } 
 
  private click(): void { 
    this.state.click(); 
  } 
   
  onclick(): void { 
    console.log('I am clicked.'); 
  } 
} 

Now we are going to have the two states, StateEnabled and StateDisabled implemented. First, let's address StateEnabled, it cares about hover status and handles click event:

class StateEnabled implements State { 
  constructor( 
    public context: Context 
  ) { } 
 
  render(hover: boolean): void { 
    this 
      .context 
      .$element 
      .removeClass('disabled') 
      .toggleClass('hover', hover); 
  } 
 
  click(): void { 
    this.context.onclick(); 
  } 
} 

Next, for StateDisabled it just ignores hover parameter and does nothing when click event emits:

class StateDisabled implements State { 
  constructor( 
    public context: Context 
  ) { } 
 
  render(): void { 
    this 
      .context 
      .$element 
      .addClass('disabled') 
      .removeClass('hover'); 
  } 
 
  click(): void { 
    // Do nothing. 
  } 
} 

Now we have classes of states enabled and disabled ready. As the instances of those classes are associated with the context, we need to initialize every state when a new Context is initiated:

class Context { 
  ... 
 
  private stateEnabled = new StateEnabled(this); 
  private stateDisabled = new StateDisabled(this); 
 
  state: State = this.stateEnabled; 
   
  ... 
} 

It is possible to use flyweights by passing context in when invoking every operation on the active state as well.

Now let's finish the Context by listening to and forwarding proper events:

constructor() { 
  this 
    .$element 
    .hover( 
      () => this.render(true), 
      () => this.render(false) 
    ) 
    .click(() => this.click()); 
 
  this.render(false); 
} 

Consequences

State Pattern reduces conditional branches in potentially multiple methods of context objects. As a trade-off, extra state objects are introduced, though it usually won't be a big problem.

The context object in State Pattern usually delegates operations and forwards them to the current state object. Thus operations defined by a concrete state may have access to the context itself. This makes reusing state objects possible with flyweights.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.26.22