0

I have the following (hypothetical) setup and am wondering if anyone has design suggestions, keywords, or nudges.

I have a class Walk that implements a method doActivity. This method keeps track of state from a limited set, e.g. "LOST", "HOME", "ON_TRAIL". Any time my state is updated, I would like to perform some (asynchronous) behavior. Here is how it looks now:

class Walk {
   // Constructor etc
   public doActivity: string {
      let currentState = "HOME"
      // Some instances I might just return state
      if (gps.battery === 0 && surroundings.view === "unfamiliar") {
         currentState = "LOST"
         return currentState
      // Won't always return, in this case we continue walk after state update.
      } else if (gps.battery >= 20 && surroundings.view === "familiar") {
         currentState = "ON_TRAIL"
      }
   }
}

The notable point there is that a walks state is updated over time, and there may be some instances where I reach a state "LOST" and continue walking and the state later becomes "ON_TRAIL" (and even maybe later "HOME"), and there are other instances where I go from "LOST" and immediately return that value.

goal/question: I would now like to update states such that any time a state is encountered, I perform some (potentially complex) operation. For example, any time the state "LOST" is encountered, I run checkCellService().

A few unsatisfying ideas I have for how to do this

  1. At the top of every doActivity, initialize a State object, and any time my state changes I call await currentState.updateState(newState). updateState would handle what needs to be done like if newState === "LOST" checkCellSignal() etc. I don't love this because (1) there would be a number of lines like currentState.updateState(newState) //nextline return newState which doesn't feel clean and (2) it relies on anyone implementing doActivity logic to remember to call updateState() before returning the string.
  2. Create a State superclass and a factory method to create them and require that doActivity's return type is State. This might look below. The benefits of this are that I can now enforce on a type level that doActivity returns a State. What I don't like is that there would be many lines of return await SetState(newState) in doActivity (and that asynchronous activity could be somewhat unpredictable such as making HTTP request).
class State {
    private state;

    constructor(state: string) {
        this.state = state
    }   

    public async finalize(): Promise<void> {
        return;
    }
}

class Lost extends State {
    constructor() {
        super("LOST")
    }

    public async finalize(): Promise<void> {
        await checkCellSignal()
    }
}

function SetState(newState: string) {
    let currentState: State
    switch(newState) {
        case 'LOST' {
            currentState = new Lost()
            break
        }
        case 'ON_TRAIL' {
            // set currentState
        }
    }
    await currentState.finalize()
    return currentState;
}

I apologize for all of the text but I hope it is a quick read. Any suggestions on a good method for how to manage these states would be appreciated, thanks!

1
  • it's hard to comment on this without understanding the larger context, but I'll give it a shot. My first instinct is that the use of strings both inside and outside of doActivity() indicates a design issue. I think your suggestion of creating State classes is more on track, but it doesn't quite go far enough. Whenever I've used something like a State Pattern in the past, each State object either modifies some shared State instance, or returns a new instance of State (I prefer the latter approach). I don't recommend making decisions based on the strings.
    – TallChuck
    May 11 '21 at 23:33
0

You could try to implement the State pattern as described here.

In short, you would still have a State class that is extended by your wanted states (as in your 2nd example), but you would also have a Context class to delegate state-specific behavior to different objects.

0

Short answer

There's many ways to skin this particular cat, and they depend on both personal preference, size/scope of the project, and possible considerations external to this class.


Considerations

For example, is this state reused in other contexts, or is this specifically a WalkState?

  • If it is reused, it's not a great idea to go tying Walk-specific behavior into the State class itself.
  • If it isn't, then you're much more free to shape WalkState in function of how Walk expects to interact with it.

Are you going to have different walks (or derived walks) with different behavior?

  • If so, then the behavior should be tied to the specific walk type, rather than to the state.
  • If not, then defining the behavior as part of the state may be a way to enforce consistent usage...
    • ...though I would still first consider implementing this enforced consistency using a common ancestor (base class) that Walk derives from.

How many external dependencies are depended on as part of this behavior, i.e. the combined behavior of all possible states?

  • If many, you might not want to be passing them all from Walk to State just to trigger the behavior
  • If little to none, there's no real objection here.

Does your codebase intend to reuse this pattern for different domain objects, i.e. for SportsActivity and SportsActivityState?

  • If so, generalizing the "trigger behavior on state change" pattern may yield increased efficiency in development. For sufficiently large codebases, I would even consider a custom toolkit library to easily configure this mapping.
  • If not, you're much more free to couple this behavior tightly to Walk and not overly abstract it.
    • Not that tight coupling is good, per se. Loose coupling is always desirable, but there's always an upper limit based on size/scope/realism, and this coupling might not make the cut if it just doesn't render enough benefits vs the effort needed.

How much of the (sub) behaviors themselves are reused across different states? E.g. are there many states which would all individually decide to check the cell phone connection?

  • If so, you may need to further separate your sub-behaviors from your states themselves, to ensure that e.g. all states perform the same "check connection" sub behavior as part of their (larger) behavior algorithm.
  • If not, then you are able to more tightly couple a state and its behavior.
    • Same loose coupling argument like before applies here as well.

Implementations

Based on these considerations, and probably many more that I didn't even think of, you have a range of options:

  • Implement specific SetStateHome (etc) methods in Walk
  • Create specific HomeState : BaseWalkState classes and let Walk work with BaseWalkState
  • Stick to a very simple Dictionary<WalkStateEnum, Action> mapping in your Walk class
  • Build an event-driven system where you can write specific handlers to deal with any state changes (including observing the prior state)
  • Develop a more abstract (and likely generic) "property value change leads to configured behavior based on new property value" implementation, which can be reused in many different implementations such as Walk, SportsActivity, ...
  • ...

The world is your oyster.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.