Search code examples
dartenumsstate-pattern

How to apply the State Pattern with Dart enhanced enums


Dart 2.17 now has enhanced enums. Matt Carroll mentioned in his review that you could use the new enums to implement the State Pattern. I'm having trouble with this because I'm weak on both the State Pattern and on enhanced enums.

Here is an example of the State Pattern without enums from this YouTube video that I converted to Dart:

// https://www.youtube.com/watch?v=MGEx35FjBuo&t=12s

void main() {
  final atmMachine = AtmMachine();
  atmMachine.insertCard();
  atmMachine.ejectCard();
  atmMachine.insertCard();
  atmMachine.insertPin(1234);
  atmMachine.requestCash(2000);
  atmMachine.insertCard();
  atmMachine.insertPin(1234);
}

abstract class AtmState {
  void insertCard();
  void ejectCard();
  void insertPin(int pin);
  void requestCash(int cashToWithdraw);
}

class AtmMachine {
  AtmMachine() {
    _hasCard = HasCard(this);
    _noCard = NoCard(this);
    _hasPin = HasPin(this);
    _outOfMoney = NoCash(this);

    atmState = _noCard;

    if (cashInMachine <= 0) {
      atmState = _outOfMoney;
    }
  }

  late AtmState _hasCard;
  late AtmState _noCard;
  late AtmState _hasPin;
  late AtmState _outOfMoney;

  late AtmState atmState;

  int cashInMachine = 2000;
  bool correctPinEntered = false;

  void setNewAtmState(AtmState value) {
    atmState = value;
  }

  void setCashInMachine(int value) {
    cashInMachine = value;
  }

  void insertCard() {
    atmState.insertCard();
  }

  void ejectCard() {
    atmState.ejectCard();
  }

  void requestCash(int amount) {
    atmState.requestCash(amount);
  }

  void insertPin(int pin) {
    atmState.insertPin(pin);
  }

  AtmState get hasCardState => _hasCard;
  AtmState get noCardState => _noCard;
  AtmState get hasPinState => _hasPin;
  AtmState get outOfMoneyState => _outOfMoney;
}

class HasCard implements AtmState {
  HasCard(this.atmMachine);
  final AtmMachine atmMachine;

  @override
  void ejectCard() {
    print('Card ejected');
    atmMachine.setNewAtmState(atmMachine.noCardState);
  }

  @override
  void insertCard() {
    print('You cannot enter more than one card.');
  }

  @override
  void insertPin(int pin) {
    if (pin == 1234) {
      print('Correct pin');
      atmMachine.correctPinEntered = true;
      atmMachine.setNewAtmState(atmMachine.hasPinState);
    } else {
      print('Wrong pin');
      atmMachine.correctPinEntered = false;
      print('Card ejected');
      atmMachine.setNewAtmState(atmMachine.noCardState);
    }
  }

  @override
  void requestCash(int cashToWithdraw) {
    print('Enter pin first');
  }
}

class NoCard implements AtmState {
  NoCard(this.atmMachine);
  final AtmMachine atmMachine;

  @override
  void ejectCard() {
    print('Please enter a card first.');
  }

  @override
  void insertCard() {
    print('Please enter a pin.');
    atmMachine.setNewAtmState(atmMachine.hasCardState);
  }

  @override
  void insertPin(int pin) {
    print('Please enter a card first.');
  }

  @override
  void requestCash(int cashToWithdraw) {
    print('Please enter a card first.');
  }
}

class HasPin implements AtmState {
  HasPin(this.atmMachine);
  final AtmMachine atmMachine;

  @override
  void ejectCard() {
    print('Card ejected');
    atmMachine.setNewAtmState(atmMachine.noCardState);
  }

  @override
  void insertCard() {
    print('You cannot enter more than one card.');
  }

  @override
  void insertPin(int pin) {
    print('Already entered pin');
  }

  @override
  void requestCash(int cashToWithdraw) {
    if (cashToWithdraw > atmMachine.cashInMachine) {
      print('There is not enough cash in this machine');
      ejectCard();
    } else {
      print('You got $cashToWithdraw dollars');
      atmMachine.setCashInMachine(atmMachine.cashInMachine - cashToWithdraw);
      ejectCard();
      if (atmMachine.cashInMachine <= 0) {
        atmMachine.setNewAtmState(atmMachine.outOfMoneyState);
      }
    }
  }
}

class NoCash implements AtmState {
  NoCash(this.atmMachine);
  final AtmMachine atmMachine;

  @override
  void ejectCard() {
    print('We do not have money');
  }

  @override
  void insertCard() {
    print('We do not have money');
  }

  @override
  void insertPin(int pin) {
    print('We do not have money');
  }

  @override
  void requestCash(int cashToWithdraw) {
    print('We do not have money');
  }
}

My next step was going to be to convert the state class to an enum, but here are the difficulties I had:

  • I can't pass in the AtmMachine context to the enum constructor because I would need to have an initial value for each of the enum states and I couldn't use AtmMachine() because the constructor needs to be const.
  • I suppose I could pass a reference to AtmMachine into each of the methods and then do a switch on the state, but is that how the State Pattern is supposed to work? Isn't the State Pattern supposed to avoid lots of switch statements? Or is that just outside the state?

Does anyone have an example of implementing the State Pattern with Dart's enhanced enums?


Solution

  • State pattern
    To the average developer that I am, patterns are often misleading. Your question made me associate the "State pattern" with a way to implement a "Finite state machine". This is not exactly the purpose of the state pattern. Instead, it allows to modify the behaviour of an object based on the state it is. This means calling the same function on the object will have a different effect/result/consequence depending on which is its current state.
    Basic explanation (Java)

    In your example, the ATM finite state machine looks a bit like this:

    enter image description here

    This is not complete nor 100% accurate, it's only to illustrate the point. You also have more than one operation, so you need to handle each operation based on each state. As always, depending on the use case, you should avoid trying to apply a pattern at any cost ("can I do it?" vs. "should I do it?").

    This QA has some interesting insight about enums vs state pattern.

    A first remark is that when you have more than one transitions possible from your current state, no matter which language or pattern you use, there will be a condition to be checked to determine what will be the next state. In this case ifs, switches, bools etc. will be required anyway. The State pattern will avoid these when the transitions are unique from one state to the other (see Java document above). Here's a similar example in Dart:
    State pattern in Dart

    Enhanced enums
    These are basically enums with members. Apparently already available in other languages (ex. Java), this has now been implemented in Dart. In the specs, we can see how the enum "desugars" into a class. Important to note: there is at the time of writting still a limitation that prevents adding a function to an enum member:
    https://github.com/dart-lang/language/issues/2241
    https://github.com/dart-lang/language/issues/1048

    However, it is possible to do it with a workaround (this is what I did in my code below; see this comment on issue 2241 above: https://github.com/dart-lang/language/issues/2241#issuecomment-1126322956).

    Usage in State pattern To illustrate this, I took the example pattern in Dart above (Lightswitch) and replaced the State classes with an enum. The rest of the code is pretty much the same. I removed some stuff like the toString() override as this is simple to understand:

    abstract class State {
      void handler(Stateful context);
    }
    
    // This is the enum that replace the different states class.
    // Each state is an enum member. As you can see, the "static function" trick
    // is required to associate a handler function to each state. Otherwise, code
    // would be even simpler.
    enum MyState implements State {
      stateOff("off", "  Handler of StatusOff is being called!", setStatusOn),
      stateOn("on", "  Handler of StatusOn is being called!", setStatusOff);
      
      final String name;
      final String message;
      final void Function(Stateful) switchStatus;
      
      const MyState(this.name, this.message, this.switchStatus);
      
      @override
      void handler(Stateful context){
        print(message);
        switchStatus(context);
      }
      
      static void setStatusOff(Stateful ctx) => ctx.state = stateOff;
      static void setStatusOn(Stateful ctx) => ctx.state = stateOn;
    }
    
    // From here, the rest of the code is exactly the same as in the example.
    // This illustrates how State class implementations can be replaced with
    // the enhanced enum above.
    class Stateful {
      State _state;
    
      Stateful(this._state);
    
      State get state => _state;
      set state(State newState) => _state = newState;
    
      void touch() {
        print("  Touching the Stateful...");
        _state.handler(this);
      }
    }
    
    void main() {
      var lightSwitch = Stateful(MyState.stateOff);
      print("The light switch is ${lightSwitch.state}.");
      print("Toggling the light switch...");
      lightSwitch.touch();
      print("The light switch is ${lightSwitch.state}.");
    }
    
    

    As reference in case the repo disappears, these are the State classes that have been replaced with the enum in the example:

    class StatusOn implements State {
      handler(Stateful context) {
        print("  Handler of StatusOn is being called!");
        context.state = StatusOff();
      }
    
      @override String toString() {
        return "on";
      }
    }
    
    class StatusOff implements State {
      handler(Stateful context) {
        print("  Handler of StatusOff is being called!");
        context.state = StatusOn();
      }
    
      @override String toString() {
        return "off";
      }
    }