Стан
Суть патерна
Стан — це поведінковий патерн проектування, що дає змогу об’єктам змінювати поведінку в залежності від їхнього стану. Ззовні створюється враження, ніби змінився клас об’єкта.
Проблема
Патерн Стан неможливо розглядати у відриві від концепції машини станів, також відомої як стейт-машина або скінченний автомат .
Основна ідея в тому, що програма може знаходитися в одному з кількох станів, які увесь час змінюють один одного. Набір цих станів, а також переходів між ними, визначений наперед та скінченний. Перебуваючи в різних станах, програма може по-різному реагувати на одні і ті самі події, що відбуваються з нею.
Такий підхід можна застосувати і до окремих об’єктів. Наприклад, об’єкт Документ
може приймати три стани: Чернетка
, Модерація
або Опублікований
. У кожному з цих станів метод опублікувати
працюватиме по-різному:
- З чернетки він надішле документ на модерацію.
- З модерації — в публікацію, але за умови, що це зробив адміністратор.
- В опублікованому стані метод не буде робити нічого.
Машину станів найчастіше реалізують за допомогою множини умовних операторів, if
або switch
, які перевіряють поточний стан об’єкта та виконують відповідну поведінку. Ймовірніше за все, ви вже реалізували у своєму житті хоча б одну машину станів, навіть не знаючи про це. Не вірите? Як щодо такого коду, виглядає знайомо?
Побудована таким чином машина станів має критичну ваду, яка покаже себе, якщо до Документа
додати ще з десяток станів. Кожен метод буде складатися з об’ємного умовного оператора, який перебирає доступні стани.
Такий код дуже складно підтримувати. Навіть найменша зміна логіки переходів змусить вас перевіряти роботу всіх методів, які містять умовні оператори машини станів.
Плутанина та нагромадження умов особливо сильно проявляється в старих проектах. Набір можливих станів буває важко визначити заздалегідь, тому вони увесь час додаються в процесі еволюції програми. Через це рішення, що здавалося простим і ефективним на початку розробки проекту, може згодом стати проекцією величезного макаронного монстра.
Рішення
Патерн Стан пропонує створити окремі класи для кожного стану, в якому може перебувати контекстний об’єкт, а потім винести туди поведінки, що відповідають цим станам.
Замість того, щоб зберігати код всіх станів, початковий об’єкт, який зветься контекстом, міститиме посилання на один з об’єктів-станів і делегуватиме йому роботу в залежності від стану.
Завдяки тому, що об’єкти станів матимуть спільний інтерфейс, контекст зможе делегувати роботу стану, не прив’язуючись до його класу. Поведінку контексту можна буде змінити в будь-який момент, підключивши до нього інший об’єкт-стан.
Дуже важливим нюансом, який відрізняє цей патерн від Стратегії, є те, що і контекст, і конкретні стани можуть знати один про одного та ініціювати переходи від одного стану до іншого.
Аналогія з життя
Ваш смартфон поводиться по-різному в залежності від поточного стану:
- Якщо телефон розблоковано, натискання кнопок телефону призведе до якихось дій.
- Якщо телефон заблоковано, натискання кнопок призведе до появи екрану розблокування.
- Якщо телефон розряджено, натискання кнопок призведе до появи екрану зарядки.
Структура
-
Контекст зберігає посилання на об’єкт стану та делегує йому частину роботи, яка залежить від станів. Контекст працює з цим об’єктом через загальний інтерфейс станів. Контекст повинен мати метод для присвоєння йому нового об’єкта-стану.
-
Стан описує спільний для всіх конкретних станів інтерфейс.
-
Конкретні стани реалізують поведінки, пов’язані з певним станом контексту. Іноді доводиться створювати цілі ієрархії класів станів, щоб узагальнити дублюючий код.
Стан може мати зворотнє посилання на об’єкт контексту. Через нього не тільки зручно отримувати з контексту потрібну інформацію, але й здійснювати зміну стану.
-
І контекст, і об’єкти конкретних станів можуть вирішувати, коли і який стан буде обрано наступним. Щоб перемкнути стан, потрібно подати інший об’єкт-стан до контексту.
Псевдокод
У цьому прикладі патерн Стан змінює функціональність одних і тих самих елементів керування музичним програвачем, залежно від стану, в якому зараз знаходиться програвач.
Об’єкт програвача містить об’єкт-стан, якому й делегує головну роботу. Змінюючи стан, можна впливати на те, як поводяться елементи керування програвача.
Застосування
Якщо у вас є об’єкт, поведінка якого кардинально змінюється в залежності від внутрішнього стану, причому типів станів багато, а їхній код часто змінюється.
Патерн пропонує виділити в окремі класи всі поля й методи, пов’язані з визначеним станом. Початковий об’єкт буде постійно посилатися на один з об’єктів-станів, делегуючи йому частину своєї роботи. Для зміни стану до контексту достатньо буде підставляти інший об’єкт-стан.
Якщо код класу містить безліч великих, схожих один на одного умовних операторів, які вибирають поведінки в залежності від поточних значень полів класу.
Патерн пропонує перемістити кожну гілку такого умовного оператора до власного класу. Сюди ж можна поселити й усі поля, пов’язані з цим станом.
Якщо ви свідомо використовуєте табличну машину станів, побудовану на умовних операторах, але змушені миритися з дублюванням коду для схожих станів та переходів.
Патерн Стан дозволяє реалізувати ієрархічну машину станів, що базується на наслідуванні. Ви можете успадкувати схожі стани від одного батьківського класу та винести туди весь дублюючий код.
Кроки реалізації
-
Визначтеся з класом, який відіграватиме роль контексту. Це може бути як існуючий клас, який вже має залежність від стану, так і новий клас, якщо код станів «розмазаний» по кількох класах.
-
Створіть загальний інтерфейс станів. Він повинен описувати методи, спільні для всіх станів, виявлених у контексті. Зверніть увагу, що не всю поведінку контексту потрібно переносити до стану, а тільки ту, яка залежить від станів.
-
Для кожного фактичного стану створіть клас, який реалізує інтерфейс стану. Перемістіть код, пов’язаний з конкретними станами, до потрібних класів. Зрештою, всі методи інтерфейсу стану повинні бути реалізовані в усіх класах станів.
При перенесенні поведінки з контексту ви можете зіткнутися з тим, що ця поведінка залежить від приватних полів або методів контексту, до яких немає доступу з об’єкта стану. Є кілька способів, щоб обійти цю проблему.
Найпростіший — залишити поведінку всередині контексту, викликаючи його з об’єкта стану. З іншого боку, ви можете зробити класи станів вкладеними до класу контексту, і тоді вони отримають доступ до всіх приватних частин контексту. Останній спосіб, щоправда, доступний лише в деяких мовах програмування (наприклад, Java, C#).
-
Створіть в контексті поле для зберігання об’єктів-станів, а також публічний метод для зміни значення цього поля.
-
Старі методи контексту, в яких перебував залежний від стану код, замініть на виклики відповідних методів об’єкта-стану.
-
В залежності від бізнес-логіки, розмістіть код, який перемикає стан контексту, або всередині контексту, або всередині класів конкретних станів.
Переваги та недоліки
- Позбавляє від безлічі великих умовних операторів машини станів.
- Концентрує в одному місці код, пов’язаний з певним станом.
- Спрощує код контексту.
- Може невиправдано ускладнити код, якщо станів мало, і вони рідко змінюються.
Відносини з іншими патернами
-
Міст, Стратегія та Стан (а також трохи і Адаптер) мають схожі структури класів — усі вони побудовані за принципом «композиції», тобто делегування роботи іншим об’єктам. Проте вони відрізняються тим, що вирішують різні проблеми. Пам’ятайте, що патерни — це не тільки рецепт побудови коду певним чином, але й описування проблем, які призвели до такого рішення.
-
Стан можна розглядати як надбудову над Стратегією. Обидва патерни використовують композицію, щоб змінювати поведінку головного об’єкта, делегуючи роботу вкладеним об’єктам-помічникам. Проте в Стратегії ці об’єкти не знають один про одного і жодним чином не пов’язані. У Стані конкретні стани самостійно можуть перемикати контекст.