Данный паттерн по диаграмме эквивалентен паттерну Стратегия, но отличие заключается в цели: вместо алгоритмов в нашем случае варианты поведения инкапсулируются в объектах состояния. Поэтому данный паттерн может рассматриваться как замена многочисленных условных конструкций в коде контекста: если поведение инкапсулировано в объектах состояния, для изменения поведения контекста достаточно выбрать другой объект состояния.
Рассмотрим сначала пример реализации работы автомата, выдающего шарики за монетку без использования паттерна Состояние. Итак, наш автомат может находиться в определенных состояниях, например - ожидание, пуст и прочее, что зависит от ряда условий. Между состояниями существуют переходы, например - выдыча шарика или прием монетки. И вот здесь нужно использовать различные условные конструкции. Для начала рассмотрим модель нашего автомата на следующем рисунке.
Как видно, кружки - состояния автомата, а стрелки - переходы между состояниями. Вот реализация без паттерна.
/* GumballMachine */
var GumballMachine = function (count) {
var SOLD_OUT = 0,
NO_QUARTER = 1,
HAS_QUARTER = 2,
SOLD = 3;
var state = SOLD_OUT,
count = count || 0;
if (count > 0) {
state = NO_QUARTER;
}
this.insertQuarter = function () {
if (state == HAS_QUARTER) {
console.log("You can't insert another quarter");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
console.log("You inserted a quarter");
} else if (state == SOLD_OUT) {
console.log("You can't insert a quarter, the machine is sold out");
} else if (state == SOLD) {
console.log("Please wait, we're already giving you a gumball");
}
};
this.ejectQuarter = function () {
if (state == HAS_QUARTER) {
console.log("Quarter returned");
state = NO_QUARTER;
} else if (state == NO_QUARTER) {
console.log("You haven't inserted a quarter");
} else if (state == SOLD) {
console.log("Sorry, you already turned the crank");
} else if (state == SOLD_OUT) {
console.log("You can't eject, you haven't inserted a quarter yet");
}
};
this.turnCrank = function () {
if (state == SOLD) {
console.log("Turning twice doesn't get you another gumball!");
} else if (state == NO_QUARTER) {
console.log("You turned but there's no quarter");
} else if (state == SOLD_OUT) {
console.log("You turned, but there are no gumballs");
} else if (state == HAS_QUARTER) {
console.log("You turned...");
state = SOLD;
this.dispense();
}
};
this.dispense = function () {
if (state == SOLD) {
console.log("A gumball comes rolling out the slot");
count = count - 1;
if (count == 0) {
console.log("Oops, out of gumballs!");
state = SOLD_OUT;
} else {
state = NO_QUARTER;
}
} else if (state == NO_QUARTER) {
console.log("You need to pay first");
} else if (state == SOLD_OUT) {
console.log("No gumball dispensed");
} else if (state == HAS_QUARTER) {
console.log("No gumball dispensed");
}
};
this.refill = function (numGumBalls) {
count = numGumBalls;
state = NO_QUARTER;
};
this.toString = function () {
var result = "";
result += "\nMighty Gumball, Inc.";
result += "\nJavascript-enabled Standing Gumball Model #2004\n";
result += "Inventory: " + count + " gumball";
if (count != 1) {
result += "s";
}
result += "\nMachine is ";
if (state == SOLD_OUT) {
result += "sold out";
} else if (state == NO_QUARTER) {
result += "waiting for quarter";
} else if (state == HAS_QUARTER) {
result += "waiting for turn of crank";
} else if (state == SOLD) {
result += "delivering a gumball";
}
result += "\n";
return result;
};
};
/* testing... */
var Application = function () {
this.run = function () {
var gumballMachine = new GumballMachine(5);
console.log(gumballMachine.toString());
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
console.log(gumballMachine.toString());
gumballMachine.insertQuarter();
gumballMachine.ejectQuarter();
gumballMachine.turnCrank();
console.log(gumballMachine.toString());
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.ejectQuarter();
console.log(gumballMachine.toString());
gumballMachine.insertQuarter();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
console.log(gumballMachine.toString());
};
};
var application = new Application();
application.run();
На выходе получаем:
Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 5 gumballs
Machine is waiting for quarter
You inserted a quarter
You turned...
A gumball comes rolling out the slot
Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 4 gumballs
Machine is waiting for quarter
You inserted a quarter
Quarter returned
You turned but there's no quarter
Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 4 gumballs
Machine is waiting for quarter
You inserted a quarter
You turned...
A gumball comes rolling out the slot
You inserted a quarter
You turned...
A gumball comes rolling out the slot
You haven't inserted a quarter
Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 2 gumballs
Machine is waiting for quarter
You inserted a quarter
You can't insert another quarter
You turned...
A gumball comes rolling out the slot
You inserted a quarter
You turned...
A gumball comes rolling out the slot
Oops, out of gumballs!
You can't insert a quarter, the machine is sold out
You turned, but there are no gumballs
Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 0 gumballs
Machine is sold out
А теперь реализуем данный автомат с помощью паттерна Состояние.
/* interface State */
var State = function () {
this.insertQuarter = function () {};
this.ejectQuarter = function () {};
this.turnCrank = function () {};
this.dispense = function () {};
};
/* ConcreteState - HasQuarterState */
var HasQuarterState = function (gumballMachine) {
var gumballMachine = gumballMachine;
this.insertQuarter = function () {
console.log("You can't insert another quarter");
};
this.ejectQuarter = function () {
console.log("Quarter returned");
gumballMachine.setState(gumballMachine.getNoQuarterState());
};
this.turnCrank = function () {
console.log("You turned...");
gumballMachine.setState(gumballMachine.getSoldState());
};
this.dispense = function () {
console.log("No gumball dispensed");
};
this.toString = function () {
return "waiting for turn of crank";
};
};
HasQuarterState.prototype = new State();
HasQuarterState.prototype.constructor = HasQuarterState;
/* ConcreteState - NoQuarterState */
var NoQuarterState = function (gumballMachine) {
var gumballMachine = gumballMachine;
this.insertQuarter = function () {
console.log("You inserted a quarter");
gumballMachine.setState(gumballMachine.getHasQuarterState());
};
this.ejectQuarter = function () {
console.log("You haven't inserted a quarter");
};
this.turnCrank = function () {
console.log("You turned, but there's no quarter");
};
this.dispense = function () {
console.log("You need to pay first");
};
this.toString = function () {
return "waiting for quarter";
};
}
NoQuarterState.prototype = new State();
NoQuarterState.prototype.constructor = NoQuarterState;
/* ConcreteState - SoldOutState */
var SoldOutState = function (gumballMachine) {
var gumballMachine = gumballMachine;
this.insertQuarter = function () {
console.log("You can't insert a quarter, the machine is sold out");
};
this.ejectQuarter = function () {
console.log("You can't eject, you haven't inserted a quarter yet");
};
this.turnCrank = function () {
console.log("You turned, but there are no gumballs");
};
this.dispense = function () {
console.log("No gumball dispensed");
};
this.toString = function () {
return "sold out";
};
};
SoldOutState.prototype = new State();
SoldOutState.prototype.constructor = SoldOutState;
/* ConcreteState - SoldState */
var SoldState = function (gumballMachine) {
var gumballMachine = gumballMachine;
this.insertQuarter = function () {
console.log("Please wait, we're already giving you a gumball");
};
this.ejectQuarter = function () {
console.log("Sorry, you already turned the crank");
};
this.turnCrank = function () {
console.log("Turning twice doesn't get you another gumball!");
};
this.dispense = function () {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
console.log("Oops, out of gumballs!");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
};
this.toString = function () {
return "dispensing a gumball";
};
};
SoldState.prototype = new State();
SoldState.prototype.constructor = SoldState;
/* Context */
var GumballMachine = function (numberGumballs) {
var soldOutState = new SoldOutState(this),
noQuarterState = new NoQuarterState(this),
hasQuarterState = new HasQuarterState(this),
soldState = new SoldState(this);
var state = soldOutState,
count = numberGumballs;
if (numberGumballs > 0) {
state = noQuarterState;
}
this.insertQuarter = function () {
state.insertQuarter();
};
this.ejectQuarter = function () {
state.ejectQuarter();
};
this.turnCrank = function () {
state.turnCrank();
state.dispense();
};
this.setState = function (_state) {
state = _state;
};
this.releaseBall = function () {
console.log("A gumball comes rolling out the slot...");
if (count != 0) {
count = count - 1;
}
};
this.getCount = function () {
return count;
};
this.refill = function (_count) {
count = _count;
state = noQuarterState;
};
this.getState = function () {
return state;
};
this.getSoldOutState = function () {
return soldOutState;
};
this.getNoQuarterState = function () {
return noQuarterState;
};
this.getHasQuarterState = function () {
return hasQuarterState;
};
this.getSoldState = function () {
return soldState;
};
this.toString = function () {
var result = "";
result += "\nMighty Gumball, Inc.";
result += "\nJavascript-enabled Standing Gumball Model #2004";
result += "\nInventory: " + count + " gumball";
if (count != 1) {
result += "s";
}
result += "\n";
result += "Machine is " + state + "\n";
return result;
};
};
/* testing... */
var GumballMachineTestDrive = function () {
this.run = function () {
var gumballMachine = new GumballMachine(5);
console.log(gumballMachine.toString());
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
console.log(gumballMachine.toString());
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
console.log(gumballMachine.toString());
};
};
var machine = new GumballMachineTestDrive();
machine.run();
На выходе получаем следующий результат:
Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 5 gumballs
Machine is waiting for quarter
You inserted a quarter
You turned...
A gumball comes rolling out the slot...
Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 4 gumballs
Machine is waiting for quarter
You inserted a quarter
You turned...
A gumball comes rolling out the slot...
You inserted a quarter
You turned...
A gumball comes rolling out the slot...
Mighty Gumball, Inc.
Javascript-enabled Standing Gumball Model #2004
Inventory: 2 gumballs
Machine is waiting for quarter
Всем удачи в бою :)