Gang of Four Behavioural pattern: State
Behavioural Pattern
Alter an object's behavior when its state changes. Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
State objects that extends an abstract state class can be switched from one to the other at runtime. A StateContext delegates methods calls to the current state object. The StateContext object has the same method names as the State objects.
During a method call the State object can decide to switch it's self for another State object.
In other words some or all of the methods in the State object have the ability to switch the State object for another state object.
The new state object also extends the same abstract state object and has the same method signatures, however the implementation of the methods are likely to be different. This pattern uses polymorphism to modifiy the behaviour at runtime.
To get the code for this example:
git clone https://github.com/spotadev/gangoffour.git
In src/main/java navigate to this package:
com.javaspeak.designpatterns.go4.behavioural.state
You can run the code from the main method of:
StateApplication
In this example the ApplicationState class is the StateContext and holds a reference to the current State Object extending AbstractAccountState. There are 3 classes that extend AbstractAccountState: StarterAccountState, StandardAccountState and PremiereAccountState.
This example models a Bank Account that can be upgraded or downgraded according to the current annual salary of the account holder. The setSalary( int salary ) method in ApplicationState delegates to the current State Object by calling its setSalary( StateContext stateContext, int salary ) method.
Notice that ApplicationState passes a reference of itself ( StateContext stateContext ) in the setSalary(..) method. The reason it does this is so that the State Object, e.g. StandardAccountState can then call the stateContext to switch the current State object for another one:
if ( salary >= SalaryGrade.PREMIERE_ACCOUNT.getSalary() ){
stateContext.changeState(
new PremiereAccountState( salary, balance ) );
}
else if ( ! ( salary >= SalaryGrade.STANDARD_ACCOUNT.getSalary() ) ){
stateContext.changeState(
new StarterAccountState( salary, balance ) );
}
else {
this.salary = salary;
}
In fact changing the salary via the setSalary(..) method can consequently result in the account being upgraded or downgraded. Notice that not all method calls of ApplicationState delegated to a state object result in the State object being swapped for another State Object. For example the setBalance(..) method does not result in the state object switching, the reason for this is that the current bank account type depends on the annual salary and not the current bank balance.
package com.javaspeak.designpatterns.go4.behavioural.state;
/**
* Text book description:
* <ul>
* State: Alter an object's behaviour when its state changes. Allow an object to alter its
* behaviour when its internal state changes. The object will appear to change its class.
* </ul>
* This example uses the state pattern. State objects that extends an abstract state class can be
* switched from one to the other at runtime.
* <p>
* A StateContext delegates methods calls to the current state object. The StateContext object
* has the same method names as the State objects.
* <p>
* During a method call the State object can decide to switch it's self for another State object.
* <p>
* In other words some or all of the methods in the State object have the ability to switch the
* State object for another state object.
* <p>
* The new state object also extends the same abstract state object and has the same method
* signatures, however the implementation of the methods are likely to be different. This pattern
* uses polymorphism to modify the behaviour at runtime.
* <p>
* In this example the ApplicationState class is the StateContext and holds a reference to the
* current State Object extending AbstractAccountState. There are 3 classes that extend
* AbstractAccountState: StarterAccountState, StandardAccountState and PremiereAccountState.
* <p>
* This example models a Bank Account that can be upgraded or down graded according to the current
* annual salary of the account holder. The setSalary( int salary ) method in ApplicationState
* delegates to the current State Object by calling its setSalary( StateContext stateContext,
* int salary ) method.
* <p>
* Notice that ApplicationState passes a reference of itself ( StateContext stateContext ) in the
* setSalary(..) method. The reason it does this is so that the State Object, e.g.
* StandardAccountState can then call the stateContext to switch the current State object for
* another one:
* <pre>
* {@code
* if ( salary >= SalaryGrade.PREMIERE_ACCOUNT.getSalary() ){
*
* stateContext.changeState( new PremiereAccountState( salary, balance ) );
* }
* else if ( ! ( salary >= SalaryGrade.STANDARD_ACCOUNT.getSalary() ) ){
*
* stateContext.changeState( new StarterAccountState( salary, balance ) );
* }
* else {
* this.salary = salary;
* }
* }
* </pre>
* In fact changing the salary via the setSalary(..) method can consequently result in the account
* being upgraded or down graded. Notice that not all method calls of ApplicationState delegated
* to a state object result in the State object being swapped for another State Object. For example
* the setBalance(..) method does not result in the state object switching, the reason for this is
* that the current bank account type depends on the annual salary and not the current bank balance.
* <p>
* @author John Dickerson - 22 Feb 2020
*/
public class StateApplication implements StateContext {
private AbstractAccountState accountState;
/**
* Initialise state with the Starter Bank Account and a balance of 200 GBP
*/
public StateApplication() {
accountState = new StarterAccountState( 0, 200 );
}
@Override
public void changeState( AbstractAccountState newState ) {
accountState = newState;
}
@Override
public void setSalary( int salary ) {
accountState.setSalary( this, salary );
}
@Override
public int getSalary() {
return accountState.getSalary();
}
@Override
public float getBalance() {
return accountState.getBalance();
}
@Override
public void setBalance( float balance ) {
accountState.balance = balance;
}
@Override
public int getOverdraft() {
return accountState.getOverdraft();
}
@Override
public String getAccountName() {
return accountState.getAccountName();
}
/**
* Run the example. Note the accountState.toString() method is in the AbstractAccountState
* which the state objects extend. The toString() method prints out the state of the object.
* <p>
* Notice that the setSalary(..) method in this class delegates to the current state object and
* calls the state object's setSalary(..) method. Notice that the setSalary(..) method of the
* state object has the ability to switch the current bank account state object to another one.
*/
private void runExample() {
System.out.println( "===============================================" );
setBalance( 500 );
System.out.println( accountState.toString() );
System.out.println( "===============================================" );
setSalary( 4000 );
setBalance( 1000 );
System.out.println( accountState.toString() );
System.out.println( "===============================================" );
setSalary( 7200 );
setBalance( 3000 );
System.out.println( accountState.toString() );
System.out.println( "===============================================" );
setSalary( 82000 );
setBalance( 7000 );
System.out.println( accountState.toString() );
System.out.println( "===============================================" );
}
public static void main( String[] args ) {
StateApplication application = new StateApplication();
application.runExample();
}
}
package com.javaspeak.designpatterns.go4.behavioural.state;
/**
* Provides common functionality between all the State implementations
*
* @author John Dickerson - 22 Feb 2020
*/
public abstract class AbstractAccountState implements State {
int salary;
float balance;
public AbstractAccountState( int salary, float balance ){
this.salary = salary;
this.balance = balance;
}
@Override
public float getBalance() {
return balance;
}
@Override
public int getSalary() {
return salary;
}
public String toString(){
StringBuffer sb = new StringBuffer();
sb.append( "Account Name : " );
sb.append( getAccountName() );
sb.append( ", Overdraft : " );
sb.append( getOverdraft() );
sb.append( ", balance : " );
sb.append( balance );
sb.append( ", salary : " );
sb.append( salary );
return sb.toString();
}
}
package com.javaspeak.designpatterns.go4.behavioural.state;
/**
* This is the state object for the Premiere Account Type where the criteria for having one is
* determined by the current annual Salary. This state object will be automatically downgraded to
* a StandardAccountState if the salary raises below the salary defined in the enum:
* <p>
* SalaryGrade.PREMIERE_ACCOUNT.getSalary().
* <p>
* Similarly the account will be downloaded further to a StarterAccountState if the salary drops
* below
* <p>
* SalaryGrade.STANDARD_ACCOUNT.getSalary()
* <p>
* The other method calls do not result in the State Object being switched for another one. However
* the other methods have different method implementations amongst the state objects. For example
* calling getOverdraft() on a PremierAccountState gives an overdraft of 5000 while calling
* getOverdraft() on StandardAccountState gives an overdraft of 700.
*
* @author John Dickerson - 22 Feb 2020
*/
public class PremiereAccountState extends AbstractAccountState {
public PremiereAccountState( int salary, float balance ) {
super( salary, balance );
}
@Override
public void setSalary( StateContext stateContext, int salary ) {
if ( salary < SalaryGrade.STANDARD_ACCOUNT.getSalary() ) {
stateContext.changeState(
new StarterAccountState( salary, this.balance ) );
}
else if ( salary < SalaryGrade.PREMIERE_ACCOUNT.getSalary() ) {
stateContext.changeState(
new StandardAccountState( salary, this.balance ) );
}
else {
this.salary = salary;
}
}
@Override
public int getOverdraft() {
return 5000;
}
@Override
public String getAccountName() {
return "Premiere Account";
}
}
package com.javaspeak.designpatterns.go4.behavioural.state;
/**
* This is the state object for the Standard Bank Account Type where the criteria for having one
* is determined by the current annual Salary. This state object will be automatically upgraded to
* a PremiereAccountState if the salary raises above the salary defined in the enum:
* <p>
* SalaryGrade.PREMIERE_ACCOUNT.getSalary().
* <p>
* Similarly the account will be automatically downgraded to a StarterAccountState if the salary
* drops below
* <p>
* SalaryGrade.STANDARD_ACCOUNT.getSalary()
* <p>
* The other method calls do not result in the State Object being switched for another one. However
* the other methods have different method implementations amongst the state objects. For example
* calling getOverdraft() on a PremierAccountState gives an overdraft of 5000 while calling
* getOverdraft() on StandardAccountState gives an overdraft of 700.
*
* @author John Dickerson - 22 Feb 2020
*/
public class StandardAccountState extends AbstractAccountState {
public StandardAccountState( int salary, float balance ) {
super( salary, balance );
}
@Override
public void setSalary( StateContext stateContext, int salary ) {
if ( salary >= SalaryGrade.PREMIERE_ACCOUNT.getSalary() ) {
stateContext.changeState( new PremiereAccountState( salary, balance ) );
}
else if ( !( salary >= SalaryGrade.STANDARD_ACCOUNT.getSalary() ) ) {
stateContext.changeState( new StarterAccountState( salary, balance ) );
}
else {
this.salary = salary;
}
}
@Override
public int getOverdraft() {
return 700;
}
@Override
public String getAccountName() {
return "Standard Account";
}
}
package com.javaspeak.designpatterns.go4.behavioural.state;
/**
* This is the state object for the Starter Bank Account Type where the criteria for having one is
* determined by the current annual Salary. This state object will be automatically upgraded to a
* StandardAccountState if the salary raises above the salary defined in the enum:
* <p>
* SalaryGrade.STANDARD_ACCOUNT.getSalary().
* <p>
* Similarly the account will be automatically upgraded to a PremiereAccountState if the salary
* raises to:
* <p>
* SalaryGrade.PREMIERE_ACCOUNT.getSalary()
* <p>
* The other method calls do not result in the State Object being switched for another one. However
* the other methods have different method implementations amongst the state objects. For example
* calling getOverdraft() on a PremierAccountState gives an overdraft of 5000 while calling
* getOverdraft() on StandardAccountState gives an overdraft of 700.
*
* @author John Dickerson - 22 Feb 2020
*/
public class StarterAccountState extends AbstractAccountState {
public StarterAccountState( int salary, float balance ) {
super( salary, balance );
}
@Override
public void setSalary( StateContext stateContext, int salary ) {
if ( salary >= SalaryGrade.PREMIERE_ACCOUNT.getSalary() ) {
stateContext.changeState( new PremiereAccountState( salary, this.balance ) );
}
else if ( salary >= SalaryGrade.STANDARD_ACCOUNT.getSalary() ) {
stateContext.changeState( new StandardAccountState( salary, this.balance ) );
}
else {
this.salary = salary;
}
}
@Override
public int getOverdraft() {
return 0;
}
@Override
public String getAccountName() {
return "Starter Account";
}
}
package com.javaspeak.designpatterns.go4.behavioural.state;
/**
* Shows what the minimum annual salary is for the different account types.
*
* @author John Dickerson - 22 Feb 2020
*/
public enum SalaryGrade {
STARTER_ACCOUNT( 0 ),
STANDARD_ACCOUNT( 7000 ),
PREMIERE_ACCOUNT( 80000 );
private int salary;
private SalaryGrade( int salary ) {
this.salary = salary;
}
public int getSalary() {
return this.salary;
}
}
package com.javaspeak.designpatterns.go4.behavioural.state;
/**
* The StateContext object ApplicationState implements this method.
*
* @author John Dickerson - 22 Feb 2020
*/
public interface StateContext {
void changeState( AbstractAccountState newState );
int getSalary();
void setSalary( int salary );
float getBalance();
void setBalance( float balance );
int getOverdraft();
String getAccountName();
}
Back: Gang of Four
Page Author: JD