Намір

Дозволяє об’єкту змінювати свою поведінку під час зміни внутрішнього стану. Це виглядає так, ніби об’єкт змінює свій клас.

Також відомий як

Об’єкти для стану

Мотивація

Розглянемо клас TCPConnection, який предаставляє підключення по мережі. Об’єкт TCPConnection може бути у одному з декількох різних станів: встановлено (Established), прослуховування (Listening), закрито (Closed). Коли об’єкт TCPConnection отримує запити від інших об’єктів він відповідає по різному в залежності від поточного стану. Наприклад, ефект запиту Open залажить від того чи стан підключення встановлено у закрито (Closed), чи він у стані підключення (Established). Шаблон проектування Стан описує як клас TCPConnection показує різну поведінку при кожному стані. Ключовою ідеєю у цьому шаблоні проектування є введення абстрактного класу під назвою TCPState, для представлення станів підключення по мережі. Клас TCPState оголошує загальний інтерфейс до усіх класів, які представляють різні операційні стани. Потомки класу TCPState реалізовують залежну від стану поведінку. Наприклад, класи TCPEstablished i TCPClosed реалізовують поведінку у відповідності до станів встановленого (Established) i закритого (Closed) станів об’єкта класу TCPConnection. Клас TCPConnection утримує об’єкт стану (примірник потомку TCPState), який представляє поточний стан TCPConnection. Клас TCPConnection делегує усі залежні від стану запити до його об’єкта стану. TCPConnection використовує власний примірник потомка класу TCPState для виконання запитів специфічних для даного стану підключення. Щоразу, коли стан з’єднання змінюється, об’єкт TCPConnection змінює власний використовуваний об’єкт стану. Коли з’єднання змінюється від встановленого на закрите, наприклад, TCPConnection заміняє його примірник класу TCPEstablished на примірник TCPClosed.

Застосовуваність

Використовуйте шаблон у будь-якому з наведених випадків:
  • поведінка об’єкта залежить від його стану, і вона повинна змінюватися під час виконання програми в залежності від стану.
  • Операції мають великі, часті вирази з операторами умов, які залежать від стану об’єкта. Даний стан зазвичай представляється однією або більшою кількістю нумерованих констант. Часто, декілька операцій будуть містити такі ж структури умовних операторів. Шаблон проектування Стан дозволяє помістити кожні підвирази умовного оператора у окремий клас. Це дозволяє вам трактувати стан об’єкта у якості об’єкта, і який може варіюватися незалежно від інших об’єктів.

Структура

Учасники

  • Context (TCPConnection)
    • визначає інтерфейс цікавий для клієнтів.
    • Утримує примірники потомка ConcreteState, який визначає поточний стан.
  • State (TCPState)
    • визначає інтерфейс для інкапсулювання поведінки асоційованої з певним станом об’єкта Context.
  • Потомки ConcreteState (TCPEstablished, TCPListen, TCPClosed)
    • кожен потомок реалізовує поведінку асоційовану з станом Context.

Співпрацювання

  • Context делегує певні для стану запити до поточного об’єкту ConcreteState.
  • Контекст може передавати себе у якості аргументу до об’єкта State, обробляючи запити. Це дозволяє об’єкту State отримувати доступ до контексту у разі необхідності.
  • Context являється головним інтерфейсом для клієнтів. Клієнти можуть конфігурувати контекст за допомогою об’єктів State. Як тільки контекст сконфігуровано, його клієнти безпосередньо не мають справу з об’єктами State.
  • Або потомки Context або ConcreteState можуть вирішувати який стан замінює інший і під якими обставинами.

Наслідки

Шаблон проектування State має наступні наслідки:
  1. Він локалізує станово-залежну поведінку і часткову поведінку для різних станів. Шаблон проектування State зберігає усю поведінку асоційовану з певним станом у об’єкт. Через те що станово-залежний код проживає у потомках класу State, нові стани і перетворення можуть легко додаватися за допомогою визначення нових дочірніх класів. Альтернативою являється використання значень даних для визначення станів і їх перевірка класом Context безпосередньо. Але в даному випадку ми б отримували множину виразів умовного або switch-case операторів розкиданих крізь реалізацію Context. Додавання нового стану буде вимагати зміну декількох операцій, які ускладнюють підтримку. Шаблон проектування State запобігає цю проблему, але може впровадити іншу, через те що шаблон поширює поведінку на різні стани між потомками класу State. Це збільшує кількість дочірніх класів і являється менш компактним ніж один клас. Але така дистрибуція являється позитивною, якщо існує певна кількість станів, які в іншому випадку вимагають велику кількість умовних операторів. На подобі великих процедур, великі вирази з умовними операторами є не бажаними. Вони є монолітними і роблять код менш зрозумілим, що в свою чергу робить їх важкими для модифікування і розширення. Шаблон проектування State пропонує кращий шлях для структурування станово-залежного коду. Логіка яка визначає зміну стану не розміщується у монолітних виразах if або switch-case, але замість цього розбивається між потомками класу State. Інкапсулювання кожної зміни стану і дії у класах піднімає ідею від стану виконання до повного статуса об’єкта. Це нав’язує структурування коду і робить його чистішим.
  2. Шаблон проектування робить явною зміну стану. Коли об’єкт визначає його поточний стан тільки у тірмінах значень внутрішнії даних, його зміна стану не має прямого представлення; вони виглядають тільки як присвоєння значень для деяких змінних. Впровадження окремих об’єктів для різних станів робить змін станів більш явними. Також, об’єкти State можуть захищати Context від непослідовних внутрішніх станів, тому що зміна станів являється атомарною з точки зору класу Context — вони виконуються через зміну однієї змінної (змінна State об’єкта Context), а не декількох [dCLF93].
  3. Об’єкти State можуть бути розподіленими. Якщо об’єкти State не мають примірника змінної — тобто, стан, який вони представляють закодований повністю у їхній тип — тоді контексти можуть розподіляти об’єкти State. Коли стани є розподіленим даним способом, вони поведінкою являються легкими вагами (перегляньте шаблон проектування Легка Вага (000)) без внутрішнього стану.

Реалізація

Шаблон проектування State піднімає декілька питань реалізації:
  1. Хто визначає зміну стану? Шаблон проектування Стан не вказує, які учасники визначають критерії для зміну стану. Якщо критерії являються фіксованими, тоді вони можуть бути реалізованими повністю у Context. Однак, в загальному більш гнучко і правильно дозволити потомкам самостійно визначати їхній поточний стан і тоді виконувати зміну. Це вимагає додавання інтерфейсу до Context, який дозволяє об’єктам State більш явно встановлювати поточний стан об’єкта Context. Децентралізація логіки зміни даним способом полегшує модифікування або розширення логіки, за допомогою визначення нового потомка класу State. Недоліком децентралізації являється те, що один потомок класу State повинен знати про хоча-б одного іншого, що впроваджує реалізаційні залежності між потомками.
  2. Таблична альтернатива. У книзі Стилі Програмування C++ (С++ Programming Style) [Car92], Карґіл (Cargill) описує інший спосіб нав’язування структури станово-залежного коду: він використовує таблиця для визначення перетворення даних введення до стану. Для кожного стану, таблиця перетворює кожне можливе введення до відповідного стану. Як ефект, даний підхід перетворює умовний код (і віртуальні функції, у випадку шаблону проектування Стан) у пошук по таблиці. Головна перевага таблиць полягає у їхній розповсюдженості: ви можете змінювати критерії зміни за допомогою зміни даних замість редагування програмного коду. Однак, існує декілька недоліків:
    • пошук по таблиці часто являється менш ефективним ніж виклик (віртуальної) функції.
    • Вкладання логіки зміни в уніфікований, табличний формат робить критерії зміни менш явними і отже важчими для розуміння.
    • Зазвичай важко додавати дії для супроводу змін станів. Табличний підхід охоплює стани і їхні зміни, але він повинен бути розширеним для виконання обчислень на кожній зміні.
    Ключової різницею між табличною машиною станів і шаблоном проектування Стан може бути просумований наступним чином: шаблон проектування Стан моделює станово-залежні поведінки, коли табличний підхід фокусується на визначенні станів переходу.
  3. Створення і знищення об’єктів State. Поширений компроміс реалізації вартий розглядання коли (1) створення об’єктів State тільки коли вони потрібні і наступне їх знищення, на противагу (2) одноразового створення і утримування в пам’яті. Перший випадок є бажаним коли стани, в які буде виконуватися входження не відомі під час виконання, і контексти змінюють стани часто. Даний підхід запобігає створення об’єктів, які не будуть використаними, що являється важливим, якщо об’єкти State зберігають велику кількість інформації. Другий підхід являється кращим, коли стани змінюється часто і швидко, в якому випадку ви бажаєте запобігти знищенню станів, через те що вони можуть бути невдовзі потрібними. За створення примірникиів класів ви розплачуєтеся однаразово і після чого немає втрат на знищення. Даний підхід може бути неправильним, через те що Context повинен утримувати посилання до усіх станів, в які він може входити.
  4. Використання динамічної успадкованості. Зміна поведінки для певного запиту може бути виконаною зміно класу об’єкта під час виконання, але це не можливо у більшості об’єктно-орієнтованих мовах програмування. Виключеннями являються Self [US87] і інші делегаційно-засновані мови, які забезпечують такий механізм і отже безпосередньо підтримують шаблон проектування State. Об’єкти у Self можуть делегувати операції до інших об’єктів для отримуння форми динамічної успадковуваності. Зміна цілі делегації під час виконання програми ефективно змінює структуру успадкованості. Даний механізм дозволяє об’єктам змінювати їхню поведінку і досягає зміни їхнього класу.

Приклад коду

Наступний приклад демонструє С++ код для прикладу TCP з’єднань описаного у секції Мотивація. Даний приклад являється спрощеною версією протоколу TCP; він не описує протокол повністю або усі стани підключень TCP. Спершу ми визначимо клас TCPConnection, який забезпечує інтерфейс для передавання даних і обробки запитів для зміни стану.
class TCPOctetStream ;
class TCPState ;

class TCPConnection 
{
public:

	TCPConnection () ;
	void ActiveOpen () ;
	void PassiveOpen () ;
	void Close () ;
	void Send () ;
	void Acknowledge () ;
	void Synchronize () ;
	void ProcessOctet (TCPOctetStream*) ;

private:

	friend class TCPState ;
	void ChangeState (TCPState*) ;

private:

	TCPState* _state;
} ;
TCPConnection утримує примірник класу TCPState у своєму полі _state. Клас TCPState дублікує залежний від стану інтерфейс класу TCPConnection. Кожна операція TCPState приймає примірник TCPConnection у якості параметру, дозволяючи TCPState отримувати доступ до даних з TCPConnection і змінювати стан підключення.
class TCPState 
{
public:

	virtual void Transmit (TCPConnection*, TCPOctetStream*) ;
	virtual void ActiveOpen (TCPConnection*) ;
	virtual void PassiveOpen (TCPConnection*) ;
	virtual void Close (TCPConnection*) ;
	virtual void Synchronize (TCPConnection*) ;
	virtual void Acknowledge (TCPConnection*) ;
	virtual void Send (TCPConnection*) ;

protected:

	void ChangeState (TCPConnection*, TCPState*) ;

} ;
TCPConnection делегує усі станово-залежні запити до його примірника класу TCPState _state. TCPConnection також забезпечує операції для зміни значення даної змінної до нового примірника TCPState. Конструктор класу TCPConnection ініціалізує об’єкт у стан TCPClosed (який визначається пізніше).
TCPConnection :: TCPConnection ()
{
	_state = TCPClosed :: Instance () ;
}
void TCPConnection :: ChangeState (TCPState* s) 
{
	_state = s;
}
void TCPConnection :: ActiveOpen () 
{
	_state->ActiveOpen (this) ;
}
void TCPConnection :: PassiveOpen () 
{
	_state->PassiveOpen (this) ;
}

void TCPConnection :: Close () 
{
	_state->Close (this) ;
}
void TCPConnection :: Acknowledge () 
{
	_state->Acknowledge(this);
}

void TCPConnection :: Synchronize () 
{
	_state->Synchronize (this) ;
}
TCPState реалізовує поведінку за умовчанням для усіх запитів, які делегуються до нього. Він також змінює стан об’єкту TCPConnection за допомогою операції ChangeState. TCPState оголошується другом (friend) класу TCPConnection для надавання привілегій до доступу до його операцій.
void TCPState::Transmit (TCPConnection*, TCPOctetStream*) { }
void TCPState::ActiveOpen (TCPConnection*) { }
void TCPState::PassiveOpen (TCPConnection*) { }
void TCPState::Close (TCPConnection*) { }
void TCPState::Synchronize (TCPConnection*) { }
void TCPState::ChangeState (TCPConnection* t, TCPState* s) 
{
	t->ChangeState(s);
}
Потомки класу TCPState реалізовують станово-залежну поведінку. Клас TCPConnection може бути у багатьох станах: Established (встановлено), Listening (прослуховування) i Closed (закрито), або-що, і існує потомок класу TCPState Для кожного стану. Ми обговоримо три потомки у деталях: TCPEstablished, TCPListen, i TCPClosed.
class TCPEstablished : public TCPState {
public:

	static TCPState* Instance () ;
	virtual void Transmit (TCPConnection*, TCPOctetStream*) ;
	virtual void Close (TCPConnection*) ;
} ;

class TCPListen : 
	public TCPState 
{
public:

	static TCPState* Instance () ;
	virtual void Send (TCPConnection*) ;
	// ...

} ;

class TCPClosed : 
	public TCPState 
{
public:

	static TCPState* Instance () ;
	virtual void ActiveOpen (TCPConnection*) ;
	virtual void PassiveOpen (TCPConnection*) ;
	// ...

} ;
Потомки TCPState не утримують власний стан, отож вони можуть бути розподіленими, і вимагається тільки один примірник кожного класу. Унікальний примірник кожного потомку TCPState отримується за допомогою статичної операції Instance. Кожен потомок TCPState реалізовує станово-залежну поведінку для відповідних запитів у стані:
void TCPClosed :: ActiveOpen (TCPConnection* t) 
{
	// send SYN, receive SYN, ACK, etc.
	ChangeState (t, TCPEstablished::Instance()) ;
}

void TCPClosed :: PassiveOpen (TCPConnection* t) 
{
	ChangeState (t, TCPListen::Instance()) ;
}

void TCPEstablished :: Close (TCPConnection* t) 
{
	// send FIN, receive ACK of FIN
	ChangeState (t, TCPListen::Instance()) ;
}

void TCPEstablished :: Transmit ( TCPConnection* t, TCPOctetStream* o ) 
{
	t->ProcessOctet (o) ;
}

void TCPListen :: Send (TCPConnection* t) 
{
	// send SYN, receive SYN, ACK, etc.
	ChangeState (t, TCPEstablished::Instance()) ;
}
Після виконання станово-залежної роботи, дані методи викликають операцію ChangeState для зміни стану примірника класу TCPConnection. Об’єкт TCPConnection самостійно не знає що-небудь про протокол підключення TCP; це потомки TCPState визначають кожну зміну стану і дію в TCP.

Відомі використання

Джонсон (Johnson) і Цвейґ (Zweig) [JZ91] описують шаблон проектування Стан і його застосування до протоколу підключення TCP. Більшість популярних інтерактивних програм малювання забезпечують “інструментарій” для виконання операцій за допомогою прямої маніпуляції. Наприклад, інструмент для малювання ліній дозволяє користувачу натискати мишею і перетягувати курсор для створення нової лінії. Інструмент виділення дозволяє користувачу виділяти форми. Зазвичай присутня палітра таких інструментів. Користувач думає про ці дії, як про обирання інструменту і його використання, але в реальності поведінка редактора змінюється відносно поточного інструменту: коли інструмент для малювання є активним ми створюємо фігури; коли інструмент для виділення є активним ми виділяємо форми; і так далі. Ми можемо використовувати шаблон проектування State для зміни поведінки редактора в залежності від поточного інструменту. Ми можемо визначити абстрактний клас Tool з якого будемо визначати класи потомки, які реалізовують залежну від інструменту поведінку. Редактор малюнків утримує поточний примірник інструменту Tool і делегує запити до нього. Він змінює даний об’єкт, коли користувач обирає новий інструмент, спричиняючи відповідну зміну поведінки редактора зображень. Дана техніка використовується у каркасах (framework) редакторів малюнків HotDraw [Joh92] і Unidraw [VL90]. Вона дозволяє клієнтам легко визначати нові типи інструментів. У HotDraw клас DrawingController передає запити до поточного об’єкта Tool. У Unidraw, відповідними класами являються інтерфейси DrawingController i Tool: Ідіома Конверт-Лист (Envelope-Letter) Копліна (Coplien) [Cop92] відноситься до шаблону проектування Стан. Конверт-Лист являється технікою для зміни класу об’єкта під час виконання програми. Шаблон проектування Стан являється більш специфічним, фокусуючись на тому, як обробляти об’єкти, поведінка яких залежить від їхнього стану.

Споріднені шаблони

Шаблон проектування Легка вага пояснює коли і як об’єкти State можуть бути розподіленими. Об’єкти State часто являються Синглтонами.