In the state pattern, a class behavior changes based on its state. This type of design pattern comes under the behavior pattern.
In the state pattern, we create objects that represent various states and a context object whose behavior varies as its state object changes.
The role of this pattern is to adapt its behavior depending on the internal state of an object. It can be used when implementing the dependency of the state object if the condition statement becomes complex.
The participants in the state pattern are as follows:
StateMachine
: This is a concrete class that describes a state machine's objects, which means that they have a set of states that can be described in a state transition diagram. This class has a reference to an instance of a sub class that implements the state abstract class and defines the current state.IState
: This is an abstract class that introduces you to the methods signature of a state behavior.ConcreteStateA
and ConcreteStateB
: These are concrete subclasses that implement the behavioral methods depending on the state.The
StateMachine
object delegates the call of the method to a ConcreteState
object depending on the current state.
The StateMachine
object can send a reference to itself and to the ConcreteState
object if needed. This reference can then be sent through the initialization of the concreteState
object or each time calls are delegated.
Your company needs to sell a new device that has only two buttons that can play the radio or some music. You need to have the following functionalities, depending on the current state of the device:
State |
Action button |
Source button |
---|---|---|
Radio |
This changes to the next station and plays it |
This changes to music playing mode |
Playing music |
This pauses the music |
This changes to standby mode |
Pausing music |
This plays the music |
This changes to standby mode |
Standby |
This changes to radio mode |
This does nothing |
The preceding table shows us that we have four states to implement. Depending on the state, the button's behaviors will not be the same.
First, open the StatePattern.xcodeproj
file to see the structure of the project.
Our audio player device is represented by the Player.swift
class in the ConcreteClassWithState
folder.
Our common interface that defines the method signature of a state behavior is defined in the IPlayerState.swift
file. Each state that implements the IPlayerState
interfaces are grouped in the ConcreteState
folder.
The main.swift
file contains our demo client:
As always, we will first define our interface. Each state will implement a behavior for each of the two buttons visible on our audio player and pass the device object as an argument. This will allow the current state object to manipulate the current state of the audio player object:
protocol IAudioPlayerState{ func buttonAction(player:AudioPlayer) func buttonSource(player:AudioPlayer) }
Then, we can implement our audio player. The init
method is waiting to receive a concreteState
instance that we will keep in mind in the state variable.
We define our two buttons. Each of them will delegate the request to the state object by invoking the appropriate button.
We then add a computed property called CurrentState
that allows us to return the current state of the audio player or to change it through the state objects.
The final code of the AudioPlayer
class is as follows:
import Foundation class AudioPlayer { private var state:IAudioPlayerState! required init(state:IAudioPlayerState){ self.state = state } //Press the Action Button func ActionButton(){ state.buttonAction(self) } //Press the Source Button func SourceButton(){ state.buttonSource(self) } var CurrentState:IAudioPlayerState{ get{ return state } set{ state = newValue } } }
Our player is now ready and the interface implemented by the state's objects is defined. We can now start with coding our first state: the RadioState
class.
This class represents the state where the audio player plays the radio:
import Foundation class RadioState: IAudioPlayerState { init(){ print("RADIO MODE") } func buttonSource(player: AudioPlayer) { print("Changing to MUSIC Mode") player.CurrentState = MusicPlayingState() } func buttonAction(player: AudioPlayer) { print("Choosing next Station & playing it") } }
The implementation is quite simple; we inform the init()
method that we are in the radio mode. We implement the IAudioPlayerState
protocol and the buttonSource
and buttonAction
methods.
As we are in the radio mode, pressing buttonAction
will change it to the next station, and clicking on the source button will move it to the MusicPlaying
state.
To change the state of the audio player, we only need to make a call to the CurrentState
property of the player object:
player.CurrentState = MusicPlayingState()
Using the same logical implementation and based on the table in the preceding example, we can complete our code. The following code is the implementation of the MusicPlayingState
class:
class MusicPlayingState: IAudioPlayerState { init(){ print("MUSIC PLAY MODE") } func buttonSource(player: AudioPlayer) { print("Changing source to Standby Mode") player.CurrentState = StandByState() } func buttonAction(player: AudioPlayer) { print("Changing to Pausing Mode") player.CurrentState = MusicPausedState() } }
The following code is the implementation of the MusicPausedState
class:
class MusicPausedState: IAudioPlayerState { init(){ print("MUSIC PAUSED MODE") } func buttonSource(player: AudioPlayer) { print("Changing source to Standby Mode") player.CurrentState = StandByState() } func buttonAction(player: AudioPlayer) { print("Changing to playing Mode") player.CurrentState = MusicPlayingState() } }
The following code is the implementation of the StandBySTate
class:
class StandByState: IAudioPlayerState { init(){ print("STANDBY MODE") } func buttonSource(player: AudioPlayer) { print("Changing to Radio Mode") player.CurrentState = RadioState() } func buttonAction(player: AudioPlayer) { print("cannot launch an action in standby mode") } }
Our player is now ready to work. We will code our demo case to test whether the functionalities implemented are working as described in the table given in the introduction of the sample.
Open the main.swift
file and write the following code:
let standbyMode = StandByState() let player = AudioPlayer(state: standbyMode) player.ActionButton() player.SourceButton() player.ActionButton() player.SourceButton() player.ActionButton() player.ActionButton() player.SourceButton()
First, we instantiate the first state where our audio player will be. We decide to put it in the StandBy
mode.
Then, we instantiate our audio player and pass the standbymode
state as an argument. Finally, we will simulate an action on by clicking the action or source button. Let's run the code, and you will see the result, as shown in the following example:
We start in the standby mode. The action button tells us that we cannot use it in the standby mode. So, we click on the source button and enter in the radioMode
. We push the action button again; this changes to the next station and plays it.
We push the source button again and change to the music mode by playing the music. We push the action button and the music is paused. We then push the action button again and the music is played again.
Finally, we push the source button and the audio player comes back in the audio mode.