1

Currently, I'm working on the Order Microservice, where I have two methods related to order status changes store(Order order) and updateStatus(int orderId, String status), I'll explain later.

There are four states of the Order:

Waiting -> Expired

Waiting -> Canceled

Waiting -> Purchased

Purchased -> Canceled

I've provided the state flow diagram below, to make it clear (hopefully)

Order status diagram flow

When the order created then the status will be "Waiting", if the user has paid it then the status becomes "Purchased", if the buyer or product owner cancel it then the status becomes "Canceled", and if the time exceeded then the status becomes "Expired".

For every microservice I want to work on, I'll be implementing Gang Of Four design pattern if possible, and for the order status I decided to implement state design pattern since it is related and from what I refer in many blogs, like in the document status stuff (DRAFT, ON REVIEW, etc), audio player stuff (PAUSED, PLAYED, etc) and so on.

This is what I've done:

Base State

public interface OrderStatus {
    void updateStatus(OrderContext orderContext);
}

Waiting state

public class WaitingState implements OrderStatus {
    // omited for brevity    
    
    @Override
    public void updateStatus(OrderContext orderContext) {
        orderContext.getOrder().setStatus("Waiting");
    }
}

Purchased State

public class PurchasedState implements OrderStatus {
    // omited for brevity

    @Override
    public void updateStatus(OrderContext orderContext) {
        orderContext.getOrder().setStatus("Purchased");
    }
}

Other states

..

Context:

public class OrderContext {
    private OrderStatus currentStatus;
    private Order order;

    public OrderContext(OrderStatus currentStatus, Order order) {
        this.currentStatus = currentStatus;
        this.order = order;
    }

    public void updateState() {
        currentStatus.updateStatus(this);
    }

    public OrderStatus getCurrentStatus() {
        return currentStatus;
    }

    public void setCurrentStatus(OrderStatus currentStatus) {
        this.currentStatus = currentStatus;
    }

    public Order getOrder() {
        return order;
    }

    public void setOrder(Order order) {
        this.order = order;
    }
}

The client is the OrderServiceImpl which I called from OrderController.

public class OrderServiceImpl implements OrderService {
    // omited for brevity
    
    @Override
    public Order store(Order order) {
        WaitingState state = WaitingState.getInstance();
        OrderContext context = new OrderContext(state, order);
        context.updateState();
    
        // do other stuff
    }

    @Override    
    public void updateStatus(int orderId, String status) {
        Order order = orderRepository.findById(id);
        
        // but how about this?? this still requires me to use if/else or switch
    }
}

As you can see, I can do it while creating the Order in the store(Order order) method, but I have no idea to do it in updateStatus(int orderId, String status) since it is still required to check status value to use the right state.

switch (status) {
    case "EXPIRED": {
        ExpiredState state = ExpiredState.getInstance();
        OrderContext context = new OrderContext(state, order);
        context.updateState();

        // do something
        break;
    }
    case "CANCELED": {
        CanceledState state = CanceledState.getInstance();
        OrderContext context = new OrderContext(state, order);
        context.updateState();

        // do something
        break;
    }
    // other case
    default:
        // do something
        break;
}

The exact reason to implement the state design pattern is to minimize "the switch stuff/hardcoded checking" and the flexibility for adding more state without breaking current code (Open/Close principle), but maybe I'm wrong, maybe I'm lack of knowledge, maybe I'm too naive to decide to use this pattern. But at the end of the day, I found out that I still need to use the switch stuff to use the state pattern.

Then, what's the right way to handle the order status changes?

Rych Emrycho
  • 85
  • 10
  • 1
    The status of an `Order` and the State Pattern are not necessarily the same thing. From a design perspective the question to ask is, _what can an `Order` do_ regardless of its status? You need a set of common behaviors to define an API for the State Pattern. The purpose of the pattern is not to update state. State change is only a side effect. The purpose is to implement the behaviors of the context in different (polymorphic) ways. So the first step is to define the behaviors of the context, other than status. – jaco0646 Jul 08 '20 at 14:43
  • @jaco0646 do you mean that I shouldn't implement State Pattern for this case since it is not the exact purpose of the pattern? – Rych Emrycho Jul 08 '20 at 15:25
  • I don't see a use case for the State Pattern so far; but all that is shown is the status changes. What can an `Order` _do_ when it has each of those statuses? Is there a set of common behaviors across every status? – jaco0646 Jul 08 '20 at 15:37
  • 1) `but all that is shown is the status changes` Yes exactly, my aim to use this pattern is to utilize the flexibility of adding more status (if any) in the future without breaking the current code (open/closed principle) and also remove the usage of switch-case or if-else. 2) `What can an Order do when it has each of those statuses?` I only check if the status value is valid (Waiting, Purchased, Canceled, Expired, etc) and then store the updated Order by delegate the rest to the OrderRepository. – Rych Emrycho Jul 08 '20 at 16:01
  • It sounds like status is merely an attribute of the `Order` rather than a behavioral state in the sense of the State Pattern. Have a look at this [answer](https://stackoverflow.com/a/50026088/1371329). I think you may want a _state machine_ rather than the State Pattern. – jaco0646 Jul 08 '20 at 17:58
  • @jaco0646 thanks, that brings me a new perspective – Rych Emrycho Jul 11 '20 at 09:35

1 Answers1

2

The exact reason to implement the state design pattern is to minimize "the switch stuff/hardcoded checking" and the flexibility for adding more state without breaking current code (Open/Close principle)

Polymorphism does not replace all conditional logic.

but maybe I'm wrong, maybe I'm lack of knowledge, maybe I'm too naive to decide to use this pattern.

Consider what behaviors actually change in response to an order's status change. If no behaviors change, there's no reason to use the State pattern.

For example, if the order's behavior does not change, assigning an integer (or enum) or string as an order status is fine:

enum OrderStatus {
    WAITING,
    CANCELLED,
    EXPIRED,
    PURCHASED
}

class Order {
    private OrderStatus status;
    
    public Order() {
        status = OrderStatus.WAITING;
    }
    
    public void setStatus(OrderStatus s) {
        status = s;
    }
    
    public void doOperation1() {
        System.out.println("order status does not affect this method's behavior");
    }
    
    public void doOperation2() {
        System.out.println("order status does not affect this method's behavior");
    }

    public void doOperation3() {
        System.out.println("order status does not affect this method's behavior");
    }
}

If doOperation()s remain the same despite status changes, this code works fine.

However, real problems start to occur when doOperation()s' behaviors change due to status changes. What you'll end up with is methods that look like this:

...
    public void doOperation3() {
        switch (status) {
        case OrderStatus.WAITING:
            // waiting behavior
            break;
        case OrderStatus.CANCELLED:
            // cancelled behavior
            break;
        case OrderStatus.PURCHASED:
            // etc
            break;
        }
    }
...

For many operations, this is unmaintainable. Adding more OrderStatus will become complex and affect many Order operations, violating the Open/Closed Principal.

The State pattern is meant to address this problem specifically. Once you identify which behaviors change, you extract them to an interface. Let's imagine doOperation1() changes:

interface OrderStatus {
  void doOperation1();
}

class WaitingOrderStatus implements OrderStatus {
  public void doOperation1() {
    System.out.println("waiting: doOperation1()");
  }

  public String toString() {
    return "WAITING";
  }
}

class CancelledOrderStatus implements OrderStatus {
  public void doOperation1() {
    System.out.println("cancelled: doOperation1()");
  }

  public String toString() {
    return "CANCELLED";
  }
}

class Order implements OrderStatus {
    private OrderStatus status;
    
    public Order() {
        status = new WaitingOrderStatus();
    }
    
    public void setStatus(OrderStatus s) {
        status = s;
    }
    
    public void doOperation1() {
      status.doOperation1();
    }
    
    public void doOperation2() {
        System.out.println("order status does not affect this method's behavior");
    }

    public void doOperation3() {
        System.out.println("order status does not affect this method's behavior");
    }
}

class Code {
    public static void main(String[ ] args) {
        Order o = new Order();
        
        o.doOperation1();
    }
}

Adding new states is easy and it adheres to the Open/Closed Principal.

Rafael
  • 7,605
  • 13
  • 31
  • 46