image

Today, let’s make a candy machine. Users only need to invest 25 cents to buy candy. The specific structure is shown in the figure below:

image

Each circle represents a state, and each arrow represents an action, and these states can be switched between different actions. As you can see from the diagram, we have four states and four actions, so without further ado, let’s look at the specific code implementation.

#import "gumabllMachines.h"

typedef enum : NSUInteger {
    sold_out,          // The candy is sold out
    no_quarter,        // There are no coins
    has_quarter,       / / a coin
    sold               // Sell candy
}gumabllMachineState;

@interface gumabllMachines(a)
@property(assign.nonatomic)gumabllMachineState state;
@property(assign.nonatomic)NSInteger gumabllCount;
@end


@implementation gumabllMachines

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.gumabllCount = 10;
    }
    return self;
}


// Put in a coin- (void)insertQuarter{
    if(self.state == has_quarter){
        NSLog("Do not double coin");

    }else if (self.state == no_quarter){
        self.state = has_quarter;
        NSLog("You put in a coin");

    }else if (self.state == sold_out){
        NSLog("You can't put coins in. Candy is sold out.");

    }else if (self.state == sold){
        NSLog("Please wait, we are selling candy"); }}// Exit the coin- (void)ejectQuarter{
    if(self.state == has_quarter){
        NSLog(@" Exiting coins for you");
        self.state = no_quarter;

    }else if (self.state == no_quarter){
        NSLog("You can't refund if you didn't put coins in");

    }else if (self.state == sold_out){
        NSLog("No refunds, you haven't put the coin in yet");

    }else if (self.state == sold){
        NSLog("Coin cannot be refunded. You have already turned the crank and bought the candy."); }}// Exit the coin- (void)turnCrank{
    if(self.state == has_quarter){
        NSLog(@" Do not turn the crank repeatedly");

    }else if (self.state == no_quarter){
        NSLog("Please put a coin in");

    }else if (self.state == sold_out){
        NSLog("No more candy.");

    }else if (self.state == sold){
        NSLog("Selling candy, please hold...");
        self.state = sold;
        [selfturnCrank]; }}// Sell candy- (void)dispense{
    if(self.state == has_quarter){
        self.gumabllCount --;
        if (self.gumabllCount > 0) {
            NSLog(@" Candy is being sold");
            self.state = no_quarter;
        }else{
            self.state = sold_out;
            NSLog("Sorry, we're out of candy."); }}else if (self.state == no_quarter){
        NSLog(@" No candy sold");

    }else if (self.state == sold_out){
        NSLog(@" No candy sold");

    }else if (self.state == sold){
        NSLog(@" No candy sold"); }}@endCopy the code

The code above should solve our current problem, but it’s coming: the requirements have changed. We’re going to add a state where when we turn the crank, there’s a 10% chance that two candies will fall. The candy machine is as follows:

image

Now there is one more state – winner, then must add this state judgment in the above four methods, if one day to modify a state, then must change in the four methods one by one, it is not too much trouble.

So what’s the solution?

After careful analysis of the above code and diagram, we find that every time we modify the state, we have to modify the original code, because the original code and the state are mixed together, so it is ok to separate these states into a class. In this way, you only need to modify the individual state class to add or modify the state later, and the original logic code does not need to make any changes.

The above code uses actions to classify. An action method is divided into four states, but the states need to be modified frequently, so each state modification needs to modify each action method. Therefore, in order to avoid such a situation, we need to separate the states into a class. If the state is fixed and the action is constantly changing, you might consider separating the action into a class. In fact, the ultimate goal is to separate the changing from the unchanging, and to encapsulate the changing part so that it changes independently without affecting the unchanging part.

Let’s take a look at the code implementation


Code implementation

1. Define the interface to the status class

We need to define an interface that contains the above four actions, and each state class needs to implement these four methods

#import <Foundation/Foundation.h>

@protocol stateInterface <NSObject>
@required- (void)insertQuarter; - (void)ejectQuarter; - (void)trunCrank; - (void)dispense;

@endCopy the code

2. Define four states

Now we are going to implement the four states of the candy machine. We will extract these states and separate them into classes. Each state will implement the four actions of the above interface

There is no 25 cent state

#import <Foundation/Foundation.h>
#import "stateInterface.h"
#import "gumabllMachine.h"

@interface noQuarterState : NSObject<stateInterface>
@property(strong.nonatomic)gumabllMachine *machine;
- (instancetype)initWithMachine:(gumabllMachine *)machine;
@end= = = = = = = = = = = = = = =#import "noQuarterState.h"

@implementation noQuarterState
- (instancetype)initWithMachine:(gumabllMachine *)machine
{
    self = [super init];
    if (self) {
        self.machine = machine;
    }
    return self; } - (void)insertQuarter{
    NSLog("You stuffed a coin.");
    self.machine.state = self.machine.hasQuarterState; } - (void)ejectQuarter{
    NSLog("You can't get a refund if you didn't put a coin in."); } - (void)trunCrank{
    NSLog(@" You pressed the buy button, but you didn't insert coins, please insert coins"); } - (void)dispense{
    NSLog("You want a candy, but you didn't put any coins in it, please pay in advance");

}


@endCopy the code

There’s a 25 cent state

#import <Foundation/Foundation.h>
#import "stateInterface.h"
#import "gumabllMachine.h"

@interface hasQUarterState : NSObject<stateInterface>
@property(strong.nonatomic)gumabllMachine *machine;
- (instancetype)initWithMachine:(gumabllMachine *)machine;

@end= = = = = = = = = = =#import "hasQUarterState.h"

@implementation hasQUarterState
- (instancetype)initWithMachine:(gumabllMachine *)machine
{
    self = [super init];
    if (self) {
        self.machine = machine;
    }
    return self; } - (void)insertQuarter{
    NSLog("You have already inserted a coin, do not insert it twice"); } - (void)ejectQuarter{
    NSLog(@" Coins coming soon");
    self.machine.state = self.machine.noQuarterState; } - (void)trunCrank{
    NSLog(@" You choose to buy candy, processing....");
    self.machine.state = self.machine.soldingState; } - (void)dispense{
    NSLog("Please choose to buy candy first");
}

@endCopy the code

State of sale

#import <Foundation/Foundation.h>
#import "stateInterface.h"
#import "gumabllMachine.h"

@interface soldingState : NSObject<stateInterface>
@property(strong.nonatomic)gumabllMachine *machine;
- (instancetype)initWithMachine:(gumabllMachine *)machine;

@end= = = = = = = = = = = = = = = = = = = = =#import "soldingState.h"

@implementation soldingState
- (instancetype)initWithMachine:(gumabllMachine *)machine
{
    self = [super init];
    if (self) {
        self.machine = machine;
    }
    return self; } - (void)insertQuarter{
    NSLog(@" please wait for us to ship, do not repeat coin..."); } - (void)ejectQuarter{
    NSLog(@" Sorry, you already bought candy, no refunds"); } - (void)trunCrank{
    NSLog("Click the button repeatedly and you won't get more candy."); } - (void)dispense{
    if (self.machine.count > 0) {
        self.machine.count --;
        self.machine.state = self.machine.noQuarterState;
        NSLog(@" Candy has been sold");
        NSLog("Candy left: %zd".self.machine.count);
    }else{
        NSLog(Sorry, we are out of candy. If you need a refund, please click the repel coin button.);
        self.machine.state = self.machine.soldOutState; }}@endCopy the code

The state of being sold out of candy

#import <Foundation/Foundation.h>
#import "stateInterface.h"
#import "gumabllMachine.h"

@interface soldOutState : NSObject<stateInterface>
@property(strong.nonatomic)gumabllMachine *machine;
- (instancetype)initWithMachine:(gumabllMachine *)machine;

@end= = = = = = = = = = = = = = = = = = = = = = = = = = =#import "soldOutState.h"

@implementation soldOutState
- (instancetype)initWithMachine:(gumabllMachine *)machine
{
    self = [super init];
    if (self) {
        self.machine = machine;
    }
    return self; } - (void)insertQuarter{
    NSLog("No candy, no coins, please come again."); } - (void)ejectQuarter{
    NSLog(@" Your money will be refunded soon..."); } - (void)trunCrank{
    NSLog(@" No candy"); } - (void)dispense{
    NSLog("No more candy.");
}

@endCopy the code

3. Realize candy machine

Candy machines mainly do two things:

  • Open method, give the client operation, open four methods corresponding to four actions
  • Four states are exposed and initialized for state classes to switch states
#import <Foundation/Foundation.h>
#import "stateInterface.h"

@interface gumabllMachine : NSObject- (void)setState:(id<stateInterface>)state;
@property(strong.nonatomic)id<stateInterface> state;
@property(strong.nonatomic)id<stateInterface> soldOutState;
@property(strong.nonatomic)id<stateInterface> noQuarterState;
@property(strong.nonatomic)id<stateInterface> hasQuarterState;
@property(strong.nonatomic)id<stateInterface> soldingState;
@property(assign.nonatomic)NSInteger count;

- (instancetype)initWithGumabllCount:(NSInteger)count; - (void)machineInsertQuarter; - (void)machineEjectQuarter; - (void)machinetrunCrank; - (void)machineDispense;


@end= = = = = = = = = = = = = = = = = = = = = = = = =#import "gumabllMachine.h"
#import "noQuarterState.h"
#import "hasQUarterState.h"
#import "soldingState.h"
#import "soldOutState.h"


@implementation gumabllMachine
- (instancetype)initWithGumabllCount:(NSInteger)count
{
    self = [super init];
    if (self) {
        self.count =count;
        self.noQuarterState = [[noQuarterState alloc]initWithMachine:self];
        self.hasQuarterState = [[hasQUarterState alloc]initWithMachine:self];
        self.soldingState = [[soldingState alloc]initWithMachine:self];
        self.soldOutState = [[soldOutState alloc]initWithMachine:self];
        // Initialize the state to no coins
        if (self.count > 0) self.state = self.noQuarterState;
    }
    return self;
}

// We can see that all four action methods are delegated to the state class- (void)machineInsertQuarter{
    [self.state insertQuarter]; } - (void)machineEjectQuarter{
    [self.state ejectQuarter]; } - (void)machinetrunCrank{
    [self.state trunCrank]; } - (void)machineDispense{
    [self.state dispense];
}


@endCopy the code

4. Client testing

        1GumabllMachine *machine = [[gumabllMachine alloc]initWithGumabllCount:2];
        2, [machine machineInsertQuarter];3, [machine machinetrunCrank];4, [machine machineDispense]; [machine machineEjectQuarter];Copy the code

The output is as follows

2016- 12- 13 10:42:24.218State mode [62936:1497982You put a coin in2016- 12- 13 10:42:24.218State mode [62936:1497982You choose to buy candy, deal with....2016- 12- 13 10:42:24.218State mode [62936:1497982The candy has been sold out2016- 12- 13 10:42:24.219State mode [62936:1497982[The candy is left:1
2016- 12- 13 10:42:24.219State mode [62936:1497982You can't refund money without putting a coin in itwith exit code: 0Copy the code

Let’s use a diagram to illustrate the above process

image

The four steps of the above flowchart correspond exactly to the four sentences of the client test code. Let’s take a look at them one by one

  1. Initialize the candy machine, at this time the candy machine is noquarter (no 25 cents)
  2. Executive candy machinegumabllMachineThe action method ofmachineInsertQuarter, this method delegates the action to the current state class (noQuarterState), and jumps tonoQuarterStateClass, the implementation ofinsertQuarterMethod, output displayed, and by referencegumabllMachineClass to change the current state tohasQuarterState
  3. Executive candy machinegumabllMachineThe action method ofmachinetrunCrank, this method delegates the action to the current state class (hasQuarterState), jumping tohasQuarterStateClass, the implementation oftrunCrankMethod, output displayed, and by referencegumabllMachineClass to change the current state tosoldingState
  4. Executive candy machinegumabllMachineThe action method ofmachineDispense, this method delegates the action to the current state class to execute, jumping tosoldingStateClass, the implementation ofdispenseMethod, output displayed, and by referencegumabllMachineClass to change the current state tonoQuarterStateGo back to the initial state of step 1

From the above analysis, we can see two points:

  1. All the action methods of candy machine are delegated to specific state classes
  2. The state class itself can switch states

These are the two functions of the state pattern that we are going to talk about today. Let’s look at them in detail


define

Allows an object to change its behavior when its internal state changes. Object appears to have modified its class.

First of all, the state mode divides the state into classes separately, and then delegates the action to the current state class for execution. Then, when the state changes, the execution result of the same action will be different. Compare that to the example above, where we put 25 cents into different candy machine states, it could be accepted or rejected. This means that when the state changes, the behavior also changes.

What about the second sentence? The same object gumabllMachine at different times, the same action will be different because of the different states and different execution results. It looks like the gumabllMachine has been changed (because in general, the same method of a class should have the same result every time if the external conditions don’t change). The gumabllMachine class is actually changed by switching to a different state object.

GumabllMachine (gumabllMachine, gumabllMachine, gumabllMachine, gumabllMachine, gumabllMachine, gumabllMachine, gumabllMachine, gumabllMachine, gumabllMachine, gumabllMachine) The function of state mode is to separate the behavior corresponding to the state. Each state corresponds to the same behavior, but the same behavior of each state has different forms of expression. Corresponding to the above example, each candy machine state has four behaviors, but the behavior of each behavior is different. In this way, by switching to a different state, the specific behavior corresponding to that state can be realized. Therefore, the state determines the behavior.

From the above analysis, we can know that the premise of state mode realization is that the behavior is unchanged while the state is constantly changing.


applicability

Consider using state mode when:

  • An object’s behavior depends on its state, and it must change its behavior at runtime based on its state.

  • An operation contains a large number of conditional statements with multiple branches that depend on the state of the object. This state is typically represented by one or more enumerated constants. Often, there are more than one operation that contains the same conditional structure. The State pattern puts each conditional branch into a separate class. This allows you to treat the object’s state as an object, depending on the object itself, which can change independently of other objects.


UML structure diagram and description

image

The state mode localizes the rows that are associated with a particular state and splits the rows that are not, so that the state mode puts all the behavior associated with a particular state into one object. Because all state-related code exists in a subclass, new subclasses can easily add new states and transforms.

Another approach is to use data values to define the internal state and have C O N T E x T operations explicitly check the data. But this would leave the entire implementation of C o n T e x T littered with similar-looking conditional statements or C A s e statements. Adding a new state may require several operations to change, which complicates maintenance.

The S T a T E pattern avoids this problem, but may introduce another because it distributes the rows of dissimilar states among multiple S t A T e subclasses. This increases the number of subclasses, which is not close enough to the reality of a single class. But this distribution is actually better if you have a lot of states, otherwise you need to use huge conditional statements. As with long procedures, large conditional statements are undesirable. They form large chunks and make the code unclear, which makes them hard to modify and extend.

The S T A T E mode provides a better way to organize codes associated with a particular state. The logical logic of the deterministic state transition is not in a single block of I f or S W I T c H statements, but is distributed between subclasses s T A T e. Encapsulating each state transition and action into a class raises the focus from the execution state to the state of the entire object. This will structure the code and make its intent clearer.


The advantages and disadvantages

  • Simplify application logic control

    The state pattern uses separate classes to encapsulate the processing of a state. So we can put a big program control divided into many small individual status in the class to be realized, so that the original focus on through behavior classification to focus on through state classification, for that state often change program, after the changes, code logic clearer, can also eliminate big if – else statement

  • Better separation of state and behavior

    By setting all state class’s public interface, define their joint behavior, each state classes implement the behavior but the performance is different, this program only need to set the appropriate state class can perform behavior, so that the program only need to be concerned with state of the switch, and don’t need to be concerned with state corresponding behavior, to handle more simple and clear.

  • Better scalability

    In the future, if you add a state class, you just implement the behavior defined by the public interface, and then add interfaces where necessary, following the open closed principle.

  • Clearer state switching

    State transitions occur in only one place (context or state class), and state transitions are recorded by a variable so that there is no confusion over state transitions.


State maintenance and transition

There are two ways to maintain and transition states

  1. In the context of
  2. In each concrete state class

The example above uses the second method, how do you implement it in context? Simply extract the state transition code from each specific state class into the context, modify gumabllMachine as shown below, and delete the state transition code for each state class


#import "gumabllMachine.h"
#import "noQuarterState.h"
#import "hasQUarterState.h"
#import "soldingState.h"
#import "soldOutState.h"


@implementation gumabllMachine
- (instancetype)initWithGumabllCount:(NSInteger)count
{
    self = [super init];
    if (self) {
        self.count =count;
        self.noQuarterState = [[noQuarterState alloc]initWithMachine:self];
        self.hasQuarterState = [[hasQUarterState alloc]initWithMachine:self];
        self.soldingState = [[soldingState alloc]initWithMachine:self];
        self.soldOutState = [[soldOutState alloc]initWithMachine:self];
        // Initialize the state to no coins
        if (self.count > 0) self.state = self.noQuarterState;
    }
    return self; } - (void)machineInsertQuarter{
    [self.state insertQuarter];
    self.state = self.hasQuarterState; } - (void)machineEjectQuarter{
    [self.state ejectQuarter];
    self.state = self.noQuarterState; } - (void)machinetrunCrank{
    [self.state trunCrank];
    self.state = self.soldingState; } - (void)machineDispense{
    [self.state dispense];
    if (self.count > 0) {
        self.state = self.noQuarterState;
    }else{
        self.state = self.soldOutState; }}@endCopy the code

Download the Demo

State Mode Demo