State Pattern for Online Checkout

The essence of a good checkout experience is that its easy for the customer to complete the sale and that it successfully hides all the work happening "under the covers". Because alot more is involved in checkout than the typical user realizes. As the user progresses through checkout, tasks such as data validation, ensuring cart consistency and reserving stock and price, address verification, add/remove promo codes, gift wrap and messaging, payment authorization, fraud controls, downstream order processing and persistence are all happening in a typical checkout.
Note: I do not consider add/remove to/from cart manipulations as part of checkout. Checkout starts when the user decides to checkout with whats already been added to their cart.

MVC is a standard pattern for building UIs but coding all that checkout logic and functionality into a controller/model of MVC is not a good fit for a number of reasons:
- decoupling the checkout business logic from model/controller allows it to be more easily resued by other controllers
- complex business logic such as checkout logic is not suitable to be in a controller (this article will identify at least one great alternative)
So if not controller/model then where? The answer is that additional abstractions and software patterns are required to decompose the problem further and encapsulate the business logic (i.e. you need "more objects" as Dice, my Fluid teammate likes to say).

Checkout can be considered as a series of state transitions from first checkout page to the last. There's a predefined sequence and you typically complete step 1 successfully before step 2 before step 3.
That's why the State Pattern is a great fit for online checkout. It allows the developer to partition checkout logic code to state classes instead of having monolithic if or switch statements. Lets explore further.

State Pattern
The State Pattern is a behavioral pattern which "allows an object to alter its behavior when its internal state changes". That was a quote from what many would consider the "bible" of patterns, the book "Design Patterns: Elements of Reusable Object-Oriented Software" written by 4 authors: Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. The authors are often referred to as the Gang of Four, or GoF.

The following is the vanilla UML diagram for State Pattern from the Gang of Four Design Patterns book.


Consider the participants in the State pattern:
Context: contains current state, provides public api to clients such as an MVC controller and delegates all state specific requests to a Concrete State object.
State: an abstract base class which defines the interface (to be called by the Context only) for Concrete States
ConcreteState: one or more concrete classes which extend the Abstract State and implement the functionality for a particular state. You code one Concrete Class per State, often implemented as a singleton, another GoF pattern!


Usage for Online Checkout
Consider a typical checkout flow:
  1. the first checkout page is usually Shipping information including delivery method and gift wrap
  2. the second page handles payment and card information including Billing address and
  3. the third page may be the Confirmation page or an Order Preview page then Confirmation.
The user can only proceed to the second page via the first page and only if all validations pass successfully for the first page. Likewise for second the third page.
We can model this using the state pattern with a Concrete State for each state in Checkout. The checkout Context provides the public api to the MVC Controller and the checkout Context has a reference to the current Concrete State. As the user moves from page to page, the MVC Controller calls methods on the checkout Context which in turn calls methods on Concrete States. Each Concrete State completes its task and calls checkout Context setState() to change state to the next state. Control returns to the MVC Controller which decides what to do next based on success or failure (e.g. redirect to next page or redisplay current page with errors).
Here's what a class diagram could look like in the scenario where we have 3 pages for checkout (shipping, pay and billing and confirmation).


In this UML model we modeled 2 states for Shipping page:
- ShippingStart state which implements shippingStart() method to assemble the data to be displayed on the shipping page (e.g. address drop downs, logged in user address book etc.) and returned it in a simple response object
- ShippingCommit state which implements shippingCommit() method to handle shipping page submit and validate user entered data, save the data, perform any order creation etc. before transitioning the current state to PayStart state.


An important guideline is each Concrete State should really only do one task e.g. commit an order, authorize etc. That way its easier to rearrange, reuse and add Concrete States over time (which you will surely need to do).


How the MVC Controller and CheckoutContext interact including error handling
The MVC Controller will receive the users page action, instantiate the CheckoutContext and call the appropriate method passing needed data (such as shipping address data). The CheckoutContext api can return a data structure (object/array) with response data including indication of success or failure and any additional data such as error codes.

A good separation is to have the MVC Controller (and the client side javascript) be responsible for ensuring data entered is complete and superficially valid before a CheckoutContext method is invoked. "Deeper" semantic validations such as confirming the card number matches the cvv or the shipping zip matches city are done by the appropriate Concrete State and often involve the Concrete State interacting with 3rd party apis such as payment gateways and so forth. If CheckoutContext method call returns a response with "success" set to true then the MVC Controller proceeds, otherwise handle the error.
The MVC Controller is responsible for page transtions, the State Pattern is responsible for state transitions and there can be multiple State transitions for one page transition.


php implementation
php has all the language features of a classical OO language and is well capable of implementing the State (and the other GoF) patterns.
The following is sample code for the Context, Abstract State and Concrete classes from the State pattern.
For our implementation we had each Concrete State change the current state in the Context, thus Context provides a setState() method to change state. ConcreteState is passed a reference to the Context and can call setState() on the Context to transition to the next state. You could also have the Context do the state transitions but States doing so is "generally more flexible and appropriate" (quote from GoF).


class CheckoutContext {
  private $state;

  public function __construct() {
  // default state is SingleShip
    $this->state = ShipStart::instance();
  }

  public function changeState(CheckoutState $newState) {
    $this->state = $newState;
  }

  public function shipStart(array $params) {
    $this->state = SingleShipStart::instance();
    return $this->state->shipStart($this, $params);
  }

  public function shipCommit(array $params) {

  } 
}


abstract class CheckoutState {
  abstract public function shipStart(CheckoutContext $context, array $params);
  abstract public function shipCommit(CheckoutContext $context, array $params);
  ....
  ...other methods here  
  ....
}


class ShipStart extends CheckoutState {
  private static $instance;

  private function __construct() {
  }

  public static function instance() {
    if (!isset(self::$instance)) {
    $className = __CLASS__;
    self::$instance = new $className;
  }

  return self::$instance;
  }

  public function __clone() {
    throw new Exception("Clone not allowed");
  }

  public function __wakeup() {
    throw new Exception("Unserializing not allowed");
  } 

  public function shipStart(CheckoutContext $context, array $params) {
    // we created a response object to encapsulate data returned to MVC
    $response = new MyResponse();

    try {
      // code here to retrieve address book data, initialize form data etc.

      // all is ok so transition to next state
      $this->changeState($context, ShipCommit::instance()); 

      $response->setSuccessTrue();   // set response to success

    } catch (Exception $e) {
      // log and handle error
      $response->setError("some error info"); 
    }

  return $response;
}

// default empty implementations of other methods here
public function shipCommit(CheckoutContext $context, array $params) {}
...other methods with empty implementations
}


Cons
Following the state pattern has introduced more code but is a small price to pay for the many benefits provided.


Alternative
An alternative could be to instead use the Command pattern for each state in checkout. Command is a great pattern which I've used many times for encapsulating business logic (and is the basis of some frameworks such as PureMVC). Command could also be a good fit for the code in checkout but I believe State pattern is a better fit because of the nature of checkout: a series of pre-defined steps (aka transitions) from start to finish.
An interesting variation could be to have both i.e. the States pattern use Command instances to do tasks.

Comments

Popular posts from this blog

deep dive into Material UI TextField built by mui

angular js protractor e2e cheatsheet

react-router v6.4+ loaders, actions, forms and more