Decoupling States using the State Pattern

906 Views Asked by At

I am unsure as to what the best OO design approach should be regarding a particular State pattern I am implementing. Please consider the following:

public class World {
    private Animal dog_;
    private Animals cats_;
    …..
    public void sendDogRequest(DogRequest request) {
        dog_.sendRequest(request);
    }
    …
    public Cat getCat(String catName) {
        …
        return cat;
    }
    ...
}

public class Animal<RequestType extends Request, StateType extends State> {
    private State<StateType> currentState_;
    ….
    public void sendRequest(RequestType request) {
        request.sendToState(currentState_);
    }
    public void setState(StateType state) {
        currentState_ = state;
    }
}

public class Dog extends Animal<DogState> {
    …
}

public class DogState extends State {
    public DogState(Dog dog) {
    …
    }
    public void seeCat(Cat cat) {   }
}

public class OnLeashState extends DogState {
    public void seeCat(Cat cat) {
        dog.setState(new BarkingState());
    }
}

public class OffLeashState extends DogState {
    public void seeCat(Cat cat) {
        dog.setState(new ChasingAfterAnimalState(cat));
        cat.sendRequest(new RunAwayRequest(cat));
    }
}

public interface Request<StateType extends State> {
    public void sendToState(StateType state);
}

public class DogRequest extends Request<DogState> { }

public class SeeCatRequest extends DogRequest {
    private Cat cat_;   
    public SeeCatRequest(Cat cat) {
        cat_ = cat;
    }
    public void sendToState(DogState state) {
        state.seeCat(state);
    }
}

public class Controller() {
    public Controller(World model, View view) {
        …
    }
    ...
    public void catSelected(String catName) {
        Cat cat = world.getCat(catName);
        Dog dog = world.getDog();
        world.sendDogRequest(new SeeCatRequest(cat));
    }
    …
}

My area of hesitation is with the usages of the word new here, ie. instantiating a new SomeState() with another State, or new SomeRequest() within the Controller or another State. It seems to me that this would produce high coupling between the States and their siblings, as well as the Controller and States.

The requirements are as follows:

  1. It MUST be possible to add new States, for example adding a SniffingState.
  2. It also MUST be possible to replace existing States with new ones. For example, I should be able to replace OffLeachState with a different OffLeashState that performs a different action. For example (for some reason the code won't format):

    public class OffLeachState2 extends DogState {
    public void seeCat(Cat cat) {
    if (dog.knows(cat)) {
    // dog changes to "PlayWithCatState"
    // cat gets a "PlayWithDog" request
    } else {
    // dog changes to "ChaseAnimalState"
    }
    }
    }

  3. Finally, all changes within the World class MUST be logged. That means that the World class has a logger which is keeping track of everything that is going on. This is also because the World class is a model, and has to fire off a notifyObservers() so that the view knows to do something.

My question is, where should the states, requests etc be stored? For example:

  1. Should there be state "getters" in Dog? for example, dog.getBarkingState(), dog.getOnLeashState(), etc? This seems to make sense, but it doesn't make the Dog class resistant to change. Ie, every time I add a new DogState class, I also have to make sure that Dog has a getter for it. Also, the World doesn't know about these changes, so it doesn't log them nor notify observers.

  2. Should there be a class called DogStates and I can run DogStates.getBarkingState()? Again, similar problems to the one above.

  3. Should they be a part of the World class? For example, world.setDogState(dog, world.getDogBarkingState()? This would solve the logging/updating problem, but puts too much responsibility on the World class.

  4. Should it be some combination thereof, for example world.setState(dog, dog.getBarkingState()? This COULD be good, but doesn't assure type safety. For example, I could pass in a Dog object with a CatState, and it wouldn't know the difference.

Solution #4 seems the best to me, but I would like some other opinions about this issue.

The same question applies to the Request object. I originally wanted to send Requests by Strings which were associated with an object, for example world.sendRequest(dog, DogRequests.SEE_CAT), but then I couldn't pass the cat object as an argument.

Thank you very much for your time!

1

There are 1 best solutions below

9
On BEST ANSWER

1.) This looks like a programming exam question. In such scenarios, if unsure what to do, use a Pattern! So every State should be generated by a StateFactory and give the Factory instance some information about the World so it can decide which specific State instance to create.

Here's the logging stuff:

public class World implements StateChangeListener {
  private Animal dog_;
  private Animals cats_;

  private final List<StateChangeListener> listeners = new ArrayList<StateChangeListener>();

  public World() {
    listeners.add(this);
  }

  // Instead of sending DogRequests to Dogs via the sendDogRequest method:
  public <RequestType extends Request> void sendRequest(
      Animal<RequestType, ?> animal, Request<RequestType> request) {
    animal.sendRequest(request);
    for(StateChangeListener listener : listeners) {
      listener.stateChanged(animal, request);
    }
  }

  public void stateChanged(Animal<?, ?> animal, State<?> state) {
    // ... log here ...
  }
...

And that Factory stuff (probably a bit scatterbrained, Generics might not work correctly ;o).

public enum LocationEnum {
  HOME, PARK, POND, FOREST
}

public interface StateFactory<StateType extends State> {
  State<StateType> create(Animal<StateType, ?> animal, Context context);
}

// Do stuff Dogs do.
public class DogStateFactory<DogState> {
  public State<DogState> create(Animal<DogState, ?>, Context context) {
    if(context.currentAnimalLocation==LocationEnum.POND) {
      return new IgnoreEverythingState();
    }else if(context.currentAnimalLocation==LocationEnum.HOME){
      return new PerpetualBarkState();
    }else {
      return new FollowEveryCatState();
    }
  }
}

public class Animal<RequestType extends Request, StateType extends State> {
  private StateFactory<StateType> stateFactory;
  private State<StateType> currentState_;

  public void sendRequest(Request<RequestType> request) {
    request.sendToState(currentState_);
  }

  // A specific animal knows what it wants to do, depending on it's current
  // state and it's situational context. We don't want other animals
  // to set the state for us.
  public void determineState() {
    currentState_ = stateFactory.create(this, new Context(...));
    // One might want to extend the messaging stuff in a way that
    // the World instance can log this state change.
  }
}

public class Dog extends Animal<DogRequest, DogState> {
  public Dog() {
    this.stateFactory = new DogStateFactory<DogState>();
  }
}

2.) If you want the World to know everything happening in it, you could substitute the state setters whith messages and let the World instance listen to everybody's state changes.