Easy Tutorial
❮ Linux Tar Gz Android Tutorial Service 1 ❯

Strategy Pattern vs State Pattern

Classification

Among behavioral design patterns, the State Pattern and the Strategy Pattern are close relatives, both being very similar. Let's first look at their generic class diagrams and compare them side by side, as shown in the figure:

Both patterns look quite similar. Just by looking at this UML diagram, we can't discern much. Next, let's combine examples to compare the differences between the two. The following example is from "Head First Design Patterns."


Strategy Pattern

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to vary independently from clients that use it.

A company develops a duck game featuring various types of ducks: mallard ducks, redhead ducks, rubber ducks... How can we implement these ducks in code? Seeing this scenario, it's easy to think of defining a superclass for all ducks, Duck, encapsulating common methods and attributes. For example, all ducks can swim and have their appearance:

Example

public abstract class Duck {
    public void swim(){
        System.out.println("All ducks can swim!");
    }
    public abstract void display();
    public void fly(){
        System.out.println("Flying~~~");
    }
    public void quack(){
        System.out.println("Quack quack~");
    }
}

However, we soon realize there's a problem with this superclass definition. Not all ducks can fly, and not all ducks quack (rubber ducks might squeak). By inheriting this superclass, all ducks would fly and quack.

What to do?

Solution 1

The first solution that comes to mind is method overriding in subclasses. Simple, right? Subclasses that can't fly would override the fly method.

But the drawback is obvious: all subclasses that can't fly would need to override this method. If there are 50 types of ducks that can't fly, the amount of rewriting and maintenance would be enormous.

Solution 2

Alright, so we think of a second method: designing more abstract subclasses, such as FlyNoQuackDuck, QuackNoFlyDuck, NoQuackNoFlyDuck, FlyAndQuackDuck...

As we write, we realize this method is also not feasible. It's too inflexible, and the more parts that change, the more abstract subclasses we need to define.

Solution 3

What's a good method then? We realize that the problem arises because we habitually try to use inheritance to implement changes. Instead, we can extract the changing parts and use composition.

This approach follows three design principles:

By applying the first principle, we isolate the fly() and quack() methods into two behavior interface classes. Then, we design different behavior classes that implement these interfaces based on different requirements.

By applying the second and third principles, we compose these interfaces as member variables in the Duck class and dynamically assign them in the subclasses.

The overall "class diagram" is as follows:

Summary

This solution perfectly addresses our problem. Using the "Strategy Pattern" concept, we extract the varying parts, compose them into the class, and dynamically set different behavior subclasses, enabling dynamic behavior changes.

Code Implementation

Two behavior interface classes:

Example

public interface FlyBehavior {
    public void fly();
}

public interface QuackBehavior {
    public void quack();
}

Different behavior classes implementing the flying interface:

Example

public class FlyNoWay implements FlyBehavior{
    public void fly(){
        System.out.println("I can't fly...");
    }
}

public class FlyWithWings implements FlyBehavior{
    public void fly(){
        System.out.println("Flying~~~");
    }
}

public class FlyWithRocket implements FlyBehavior{
    public void fly(){
        System.out.println("Flying with a rocket~~~");
    }
}

Different behavior classes implementing the quacking interface:

Example

public class Quack implements QuackBehavior{
    public void quack(){
        System.out.println("Quack quack~");
    }
}

public class Squeak implements QuackBehavior{
    public void quack(){
        System.out.println("Squeak squeak~");
    }
}

public class MuteQuack implements QuackBehavior{
    public void quack(){
        System.out.println("I can't quack...");
    }
}

The superclass composed of implemented interfaces:

Example

public abstract class Duck {
    protected FlyBehavior flyBehavior;
    protected QuackBehavior quackBehavior;

    public void swim(){
        System.out.println("All ducks can swim!");
    }

    public abstract void display();

    public void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    public void setQuackBehavior(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }

    public void performFly(){
        flyBehavior.fly();
    }

    public void performQuack(){
        quackBehavior.quack();
    }
}

Different duck classes:

Example

public class MallardDuck extends Duck{
    public MallardDuck() {
        flyBehavior = new FlyWithWings();
        quackBehavior = new Quack();
    }

    @Override
    public void display() {
        System.out.println("Looks like a mallard duck");
    }
}

public class RedHeadDuck extends Duck{
    public RedHeadDuck() {
        flyBehavior = new FlyWithWings();
        quackBehavior = new Quack();
    }

    @Override
    public void display() {
        System.out.println("Looks like a redhead duck");
    }
}

public class RubberDuck extends Duck{
    public RubberDuck() {
        flyBehavior = new FlyNoWay();
        quackBehavior = new Squeak();
    }

    @Override
    public void display() {
        System.out.println("Looks like a rubber duck");
    }
}

State Pattern

The State Pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

The State Pattern is very similar to the Strategy Pattern; it also encapsulates the "state" of the class and automatically transitions when actions are performed, thus achieving different results for the same action in different states. The difference lies in the fact that this transition is "automatic" and "unconscious."

The class diagram for the State Pattern is as follows:

The class diagram for the State Pattern is identical to that of the Strategy Pattern, but their intents differ. The Strategy Pattern controls which strategy the object uses, while the State Pattern automatically changes the state. This should become clear after looking at the following case.

You are faced with a requirement to implement a candy machine in Java.

Let's analyze it: the candy machine's functionality can be divided into four actions and four states as shown in the diagram:

In different states, the same action yields different results. For example, "turning the crank" in the "has 25 cents" state will dispense candy, while in the "no 25 cents" state, it will prompt to insert coins first.

After some thought, we write the following implementation code for the candy machine:

Example

public class NoPatternGumballMachine{
    private final static int NO_QUARTER = 0;
    private final static int HAS_QUARTER = 1;
    private final static int SOLD = 2;
    private final static int SOLD_OUT = 3;

    private int state = SOLD_OUT;
    private int candyCount = 0;

    public NoPatternGumballMachine(int count) {
        this.candyCount = count;
        if(candyCount > 0)
            state = NO_QUARTER;
    }

    public void insertQuarter() {
        if(NO_QUARTER == state){
            System.out.println("Inserted a quarter");
            state = HAS_QUARTER;
        }
        else if(HAS_QUARTER == state){
            System.out.println("Do not insert another quarter!");
            returnQuarter();
        }
        else if(SOLD == state){
            System.out.println("Quarter already inserted, please wait for the candy");
        }
        else if(SOLD_OUT == state){
            System.out.println("Cannot insert a quarter, the machine is sold out");
            returnQuarter();
        }
    }

    public void ejectQuarter() {
        if(HAS_QUARTER == state){
            System.out.println("Quarter returned");
            state = NO_QUARTER;
        }
        else if(NO_QUARTER == state){
            System.out.println("You haven't inserted a quarter");
        }
        else if(SOLD == state){
            System.out.println("Sorry, you already turned the crank");
        }
        else if(SOLD_OUT == state){
            System.out.println("You can't eject, you haven't inserted a quarter yet");
        }
    }

    public void turnCrank() {
        if(SOLD == state){
            System.out.println("Turning twice doesn't get you another candy!");
        }
        else if(NO_QUARTER == state){
            System.out.println("You turned but there's no quarter");
        }
        else if(SOLD_OUT == state){
            System.out.println("You turned, but there are no candies");
        }
        else if(HAS_QUARTER == state){
            System.out.println("You turned...");
            state = SOLD;
            dispense();
        }
    }

    public void dispense() {
        if(SOLD == state){
            System.out.println("A candy comes rolling out the slot");
            candyCount = candyCount - 1;
            if(candyCount == 0){
                System.out.println("Oops, out of candies!");
                state = SOLD_OUT;
            }
            else{
                state = NO_QUARTER;
            }
        }
        else if(NO_QUARTER == state){
            System.out.println("You need to pay first");
        }
        else if(SOLD_OUT == state){
            System.out.println("No candy dispensed");
        }
        else if(HAS_QUARTER == state){
            System.out.println("No candy dispensed");
        }
    }

    public void returnQuarter() {
        System.out.println("Quarter returned");
    }
}
public void ejectQuarter() {
    if(NO_QUARTER == state){
        System.out.println("No quarter inserted, cannot eject.");
    }
    else if(HAS_QUARTER == state){
        returnQuarter();
        state = NO_QUARTER;
    }
    else if(SOLD == state){
        System.out.println("Cannot eject quarter, candy is being dispensed, please wait.");
    }else if(SOLD_OUT == state){
        System.out.println("No quarter inserted, cannot eject quarter.");
    }
}

/**
 * Turn the candy dispensing crank
 */
public void turnCrank() {
    if(NO_QUARTER == state){
        System.out.println("Please insert a quarter first.");
    }
    else if(HAS_QUARTER == state){
        System.out.println("Crank turned, preparing to dispense candy.");
        state = SOLD;
    }
    else if(SOLD == state){
        System.out.println("Crank already turned, please wait.");
    }else if(SOLD_OUT == state){
        System.out.println("Candy is sold out.");
    }
}

/**
 * Dispense candy
 */
public void dispense() {
    if(NO_QUARTER == state){
        System.out.println("No quarter inserted, cannot dispense candy.");
    }
    else if(HAS_QUARTER == state){
        System.out.println("This method is not supported.");
    }
    else if(SOLD == state){
        if(candyCount > 0){
            System.out.println("Dispensing a candy.");
            candyCount--;
            state = NO_QUARTER;
        }
        else{
            System.out.println("Sorry, candy is sold out.");
            state = SOLD_OUT;
        }
    }else if(SOLD_OUT == state){
        System.out.println("Sorry, candy is sold out.");
    }
}

/**
 * Return the quarter
 */
protected void returnQuarter() {
    System.out.println("Returning quarter...");
}

}

From the code, it can be seen that the candy machine exhibits different actions based on its current state. This code already meets our basic requirements, but upon closer inspection, it appears overly complex, with poor scalability and lacking object-oriented design.

Suppose a new state needs to be added due to new requirements. In that case, we would need to modify each action method and add another else statement. If the action in a particular state needs to be changed due to a requirement change, we would also need to modify four methods. This would be tedious and cumbersome.

What can we do? One of the six design principles is:

Identify the aspects of your application that vary and separate them from what stays the same.

In the candy machine, the state is the part that constantly changes, with different states resulting in different actions. We can abstract this out.

New design idea:

First, we define a State interface, where each action of the candy machine has a corresponding method.

Then, we implement state classes for each machine state, which will be responsible for the machine's behavior in that state.

Finally, we replace the old conditional code with delegation to the state classes.

Define a State interface:

public abstract class State {
    /**
     * Insert a quarter
     */
    public abstract void insertQuarter();

    /**
     * Eject a quarter
     */
    public abstract void ejectQuarter();

    /**
     * Turn the candy dispensing crank
     */
    public abstract void turnCrank();

    /**
     * Dispense candy
     */
    public abstract void dispense();

    /**
     * Return the quarter
     */
    protected void returnQuarter() {
        System.out.println("Returning quarter...");
    }
}

Implement state classes for each machine state:

/**
 * State when no quarter is inserted
 */
public class NoQuarterState extends State{
    GumballMachine gumballMachine;

    public NoQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    @Override
    public void insertQuarter() {
        System.out.println("You inserted a quarter.");
        // Switch to has quarter state
        gumballMachine.setState(gumballMachine.hasQuarterState);
    }

    @Override
    public void ejectQuarter() {
        System.out.println("No quarter inserted, cannot eject.");
    }

    @Override
    public void turnCrank() {
        System.out.println("Please insert a quarter first.");
    }

    @Override
    public void dispense() {
        System.out.println("No quarter inserted, cannot dispense candy.");
    }

}

/**
 * State when a quarter is inserted
 */
public class HasQuarterState extends State{
    GumballMachine gumballMachine;

    public HasQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    @Override
    public void insertQuarter() {
        System.out.println("Please do not insert another quarter!");
        returnQuarter();
    }

    @Override
    public void ejectQuarter() {
        returnQuarter();
        gumballMachine.setState(gumballMachine.noQuarterState);
    }

    @Override
    public void turnCrank() {
        System.out.println("Crank turned, preparing to dispense candy.");
        gumballMachine.setState(gumballMachine.soldState);
    }

    @Override
    public void dispense() {
        System.out.println("This method is not supported.");
    }

}

/**
 * State when candy is being sold
 */
public class SoldState extends State{
    GumballMachine gumballMachine;

    public SoldState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("Quarter already inserted, please wait for the candy.");
        returnQuarter();
    }

    @Override
    public void ejectQuarter() {
        System.out.println("Cannot eject quarter, candy is being dispensed, please wait.");
    }

    @Override
    public void turnCrank() {
        System.out.println("Crank already turned, please wait.");
    }

    @Override
    public void dispense() {
        int candyCount = gumballMachine.getCandyCount();
        if(candyCount > 0){
            System.out.println("Dispensing a candy.");
            candyCount--;
            gumballMachine.setCandyCount(candyCount);
            if(candyCount > 0){
                gumballMachine.setState(gumballMachine.noQuarterState);
                return;
            }
        }

        System.out.println("Sorry, candy is sold out.");
        gumballMachine.setState(gumballMachine.soldOutState);
    }

}

/**
 * State when candy is sold out
 */
public class SoldOutState extends State{
    GumballMachine gumballMachine;
public SoldOutState(GumballMachine gumballMachine) {
    this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
    System.out.println("All gumballs are sold out");
    returnQuarter();
}

@Override
public void ejectQuarter() {
    System.out.println("No quarter inserted, cannot return quarter");
}

@Override
public void turnCrank() {
    System.out.println("All gumballs are sold out");
}

@Override
public void dispense() {
    System.out.println("All gumballs are sold out");
}

}

Delegating gumball machine actions to state classes

Example

public class GumballMachine extends State{
    public State noQuarterState = new NoQuarterState(this);
    public State hasQuarterState = new HasQuarterState(this);
    public State soldState = new SoldState(this);
    public State soldOutState = new SoldOutState(this);

    private State state = soldOutState;
    private int candyCount = 0;

    public GumballMachine(int count) {
        this.candyCount = count;
        if(count > 0)
            setState(noQuarterState);
    }

    @Override
    public void insertQuarter() {
        state.insertQuarter();
    }
    @Override
    public void ejectQuarter() {
        state.ejectQuarter();
    }
    @Override
    public void turnCrank() {
        state.turnCrank();
    }
    @Override
    public void dispense() {
        state.dispense();
    }

    public void setState(State state) {
        this.state = state;
    }

    public State getState() {
        return state;
    }

    public void setCandyCount(int candyCount) {
        this.candyCount = candyCount;
    }

    public int getCandyCount() {
        return candyCount;
    }

}

It can be observed that under this design, the gumball machine does not need to be aware of state changes; it simply calls the state methods. The state transitions occur internally within the states. This is the essence of the "State Pattern."

If a new state is added at this point, the gumball machine does not need to be altered at all. We simply need to add a new state class and then add the transition process within the relevant state class methods.

Not understanding? Here's an article about the State Pattern: https://dzone.com/articles/state-pattern-simplified

Original link: https://blog.csdn.net/z55887/article/details/73198039

Original link: https://blog.csdn.net/z55887/article/details/60608898 ```

❮ Linux Tar Gz Android Tutorial Service 1 ❯