Стан (State). Стан на Java Стан state gof приклад на с

Прийшов час висповідатися: я трохи перестарався з цієї головної. Передбачалося, що вона присвячена шаблоном проектування Стан (State) GoF. Але я не можу говорити про його застосування в іграх, не зачіпаючи концепцію кінцевих автоматів (finite state machines) (Або "FSM"). Але як тільки я в неї заглибився, я зрозумів, що мені доведеться згадати ієрархічну машину станів (hierarchical state machine) або ієрархічний автомат і автомат з магазинної пам'яттю (pushdown automata).

Тематика виходить занадто великою, тому щоб скоротити главу до мінімуму, я буду опускати деякі очевидні фрагменти прикладів коду і вам доведеться заповнити деякі пропуски самостійно. Я сподіваюся, це не зробить їх менш зрозумілими.

Не потрібно турбуватися, якщо ви ніколи не чули про кінцеві автомати. Вони добре відомі розробникам ІІ і комп'ютерним хакерам, але маловідомі в інших областях. На мій погляд вони заслуговують більшої популярності, так що я хочу продемонструвати вам кілька проблем, які вони вирішують.

Все це відгомони старих ранніх часів штучного інтелекту. У 50-е і 60-е штучний інтелект в основному фокусувався на обробці мовних конструкцій. Багато використовувані в сучасних компіляторах технології були винайдені для парсинга людських мов.

Всі ми там були

Припустимо ми працюємо над невеликим платформер сайд-скроллером. Наше завдання полягає в моделюванні героїні, яка буде аватаром гравця в ігровому світі. Це означає, що вона повинна реагувати на призначений для користувача введення. Натисніть B і вона стрибне. Досить просто:

void Heroine :: handleInput (Input input) (if (input \u003d\u003d PRESS_B) (yVelocity_ \u003d JUMP_VELOCITY; setGraphics (IMAGE_JUMP);))

Помітили баг?

Тут немає ніякого коду, що запобігає "стрибок у повітрі"; продовжуйте натискати B поки вона в повітрі і вона буде підлітати знову і знову. Найпростіше вирішити це додаванням булевского прапора isJumping_ в Heroine, який буде стежити за тим коли героїня стрибнула:

void Heroine :: handleInput (Input input) (if (input \u003d\u003d PRESS_B) (if (! isJumping_) (isJumping_ \u003d true; // Стрибок ...)))

Нам потрібен ще й код, який буде встановлювати isJumping_ назад в false, коли героїня знову торкнеться землі. Для простоти я опускаю цей код.

void Heroine :: handleInput (Input input) (if (input \u003d\u003d PRESS_B) ( // Стрибаємо якщо вже не стрибнули ... ) Else if (input \u003d\u003d PRESS_DOWN) (if (! IsJumping_) (setGraphics (IMAGE_DUCK);)) else if (input \u003d\u003d RELEASE_DOWN) (setGraphics (IMAGE_STAND);))

А тут баг помітили?

За допомогою цього коду гравець може:

  1. Натиснути вниз для присідання.
  2. Натиснути B для стрибка з сидячою позиції.
  3. Відпустити вниз, перебуваючи в повітрі.

При цьому героїня переключиться на графіку стояння прямо в повітрі. Доведеться додати ще один прапор ...

void Heroine :: handleInput (Input input) (if (input \u003d\u003d PRESS_B) (if (! isJumping_ &&! isDucking_) (// Стрибок ...)) else if (input \u003d\u003d PRESS_DOWN) (if (! isJumping_) ( isDucking_ \u003d true; setGraphics (IMAGE_DUCK);)) else if (input \u003d\u003d RELEASE_DOWN) (if (isDucking_) (isDucking_ \u003d false; setGraphics (IMAGE_STAND);)))

Тепер буде здорово додати героїні здатність атакувати підкатом, коли гравець натискає вниз, перебуваючи в повітрі:

void Heroine :: handleInput (Input input) (if (input \u003d\u003d PRESS_B) (if (! isJumping_ &&! isDucking_) (// Стрибок ...)) else if (input \u003d\u003d PRESS_DOWN) (if (! isJumping_) ( isDucking_ \u003d true; setGraphics (IMAGE_DUCK);) else (isJumping_ \u003d false; setGraphics (IMAGE_DIVE);)) else if (input \u003d\u003d RELEASE_DOWN) (if (isDucking_) (// Стояння ...)))

Знову шукаємо баги. Знайшли?

У нас є перевірка на те, щоб було неможливо стрибнути в повітрі, але не під час підкату. Додаємо ще один прапор ...

Є в цьому підході щось неправильне. Кожен раз, коли ми торкаємося коду, у нас щось ламається. Нам знадобиться додати ще купу руху, у нас адже ще навіть ходьби немає, але при такому підході нам доведеться подолати ще купу багів.

Програмісти яких ми всі ідеалізуємо і які створюють відмінний код на насправді зовсім не супермени. Вони просто розвинули в собі чуття на загрозливий появою помилок код і намагаються по можливості його уникати.

Складне розгалуження і змінюються стану - це якраз і є ті типи коду, яких варто уникати.

Кінцеві автомати - наше спасіння

У пориві розчарування, ви забираєте зі столу все, крім олівця і паперу і починаєте креслити блок-схему. Малюємо прямокутник для кожної дії, що може зробити героїня: стояння, стрибок, присідання і підкат. Щоб вона могла реагувати на натискання клавіш в будь-якому з станів, малюємо стрілки між цими прямокутниками, підписуємо над ними кнопки і з'єднуємо між собою стану.

Вітаю, ви тільки що створили кінцевий автомат (finite state machine). Вони прийшли з області комп'ютерних наук, званої теорія автоматів (automata theory), В сімейство структур якої також входить знаменита машина Тьюринга. FSM - найпростіший член цього сімейства.

Суть полягає в наступному:

    У нас є фіксований набір станів, В яких може перебувати автомат. У нашому прикладі це стояння, стрибок, присідання і підкат.

    Автомат може перебувати тільки в одному стані в кожен момент часу. Наша героїня не може стрибати і стояти одночасно. Власне для того щоб цьому запобігти FSM в першу чергу і використовується.

    послідовність введення або подій, Переданих автомату. У нашому прикладі це натиснення і відпуск кнопок.

    Кожне стан має набір переходів, Кожен з яких пов'язаний з введенням і вказує на стан. Коли відбувається користувача введення, якщо він відповідає поточному стану, автомат змінює свій стан на те куди вказує стрілка.

    Наприклад, якщо натиснути вниз в стані стояння, відбудеться перехід в стан присідання. Натискання вниз під час стрибка змінює стан на підкат. Якщо в поточному стані ніякої перехід для введення не передбачений - нічого не відбувається.

У чистій формі це і є цілий банан: стану, введення і переходи. Можна зобразити їх у вигляді блок-схеми. На жаль, компілятор таких каракулей не зрозуміє. Так як же в такому випадку реалізувати кінцевий автомат? Банда Чотирьох пропонує свій варіант, але почнемо ми з ще більш простого.

Моя улюблена аналогія FSM - це старий текстовий квест Zork. У вас є світ, що складається з кімнат, які з'єднані між собою переходами. І ви можете досліджувати їх, вводячи команди типу "йти на північ".

Така карта повністю відповідає визначенню кінцевого автомата. Кімната, в якій ви перебуваєте - це поточний стан. Кожен вихід з кімнати - перехід. Навігаційні команди - введення.

Перерахування і перемикачі

Одна з проблем нашого старого класу Heroine полягає в тому, що він допускає некоректну комбінацію булевских ключів: isJumping_ і isDucking_, вони не можуть бути істинними одночасно. А якщо у вас є кілька булевских прапорів, тільки один з яких може бути true, чи не краще замінити їх все на enum.

У нашому випадку за допомогою enum можна повністю описати всі стани нашої FSM таким чином:

enum State (STATE_STANDING, STATE_JUMPING, STATE_DUCKING, STATE_DIVING);

Замість купи прапорів, у Heroine є тільки одне поле state_. Також нам доведеться змінити порядок розгалуження. У попередньому прикладі коду, ми робили розгалуження спочатку в залежності від введення, а потім вже від стану. При цьому ми групували код по кнопці, але розмивали код, пов'язаний з станами. Тепер ми зробимо навпаки і будемо перемикати введення в залежності від стану. Отримаємо ми ось що:

void Heroine :: handleInput (Input input) (switch (state_) (case STATE_STANDING: if (input \u003d\u003d PRESS_B) (state_ \u003d STATE_JUMPING; yVelocity_ \u003d JUMP_VELOCITY; setGraphics (IMAGE_JUMP);) else if (input \u003d\u003d PRESS_DOWN) (state_ \u003d STATE_DUCKING; setGraphics (IMAGE_DUCK);) break; case STATE_JUMPING: if (input \u003d\u003d PRESS_DOWN) (state_ \u003d STATE_DIVING; setGraphics (IMAGE_DIVE);) break; case STATE_DUCKING: if (input \u003d\u003d RELEASE_DOWN) (state_ \u003d STATE_STANDING; setGraphics (IMAGE_STAND);) break;))

Виглядає досить тривіально, але тим не менше цей код вже набагато краще, ніж попередній. У нас залишилися деякі умовні розгалуження, але зате ми спростили змінюване стан до єдиного поля. Весь код, керуючий єдиним станом зібраний в одному місці. Це найпростіший спосіб реалізації кінцевого автомата і іноді його цілком достатньо.

Тепер героїня вже не зможе бути в невизначеному стані. При використанні булевих прапорів деякі комбінації були можливі, але не мали сенсу. При використанні enum все значення коректні.

На жаль, ваша проблема може перерости таке рішення. Припустимо, ми захотіли додати нашої героїні спеціальну атаку, для проведення якої героїні потрібно присісти для підзарядки і потім розрядити накопичену енергію. І поки ми сидимо, нам потрібно стежити за часом зарядки.

Додаємо в Heroine поле chargeTime_ для зберігання часу зарядки. Припустимо у нас вже є метод update (), що викликається на кожному кадрі. Додамо в нього наступний код:

void Heroine :: update () (if (state_ \u003d\u003d STATE_DUCKING) (chargeTime _ ++; if (chargeTime_\u003e MAX_CHARGE) (superBomb ();)))

Якщо ви вгадали, що це шаблон Метод відновлення (Update Method), ви виграли приз!

Кожен раз, коли ми присідаємо заново, нам потрібно обнуляти цей таймер. Для цього нам потрібно змінити handleInput ():

void Heroine :: handleInput (Input input) (switch (state_) (case STATE_STANDING: if (input \u003d\u003d PRESS_DOWN) (state_ \u003d STATE_DUCKING; chargeTime_ \u003d 0; setGraphics (IMAGE_DUCK);) // Обробка залишився введення ... break; // Інші стану ... } }

Зрештою, для додавання цієї атаки з підзарядкою, нам довелося змінити два методу і додати поле chargeTime_ в Heroine, навіть якщо воно використовується тільки в стані присідання. Хотілося б мати весь цей код і дані в одному місці. Банда Чотирьох може нам в цьому допомогти.

шаблон стан

Для людей, що добре розбираються в об'єктно-орієнтованої парадигми, кожне умовне розгалуження - це можливість для використання динамічної диспетчеризації (іншими словами, виклику віртуального методу в C ++). Думаю нам потрібно спуститися в цю кролячу нору ще глибше. Іноді if - це все що нам потрібно.

Цьому є історичне обгрунтування. Багато зі старих апостолів об'єктно-орієнтованої парадигми, такі як Банда Чотирьох зі своїми патернами програмування і Мартін Фулер з його рефакторингом прийшли з Smalltalk. А там ifThen - це всього лише метод, яким ви обробляєте умова і який реалізується по різному для об'єктів true і false.

У нашому прикладі ми вже дісталися до тієї критичної точки, коли нам варто звернути увагу на щось об'єктно-орієнтоване. Це підводить нас до шаблону Стан. Цитую Банду Чотирьох:

Дозволяє об'єктам міняти свою поведінку відповідно до зміни внутрішнього стану. При цьому об'єкт буде вести себе як інший клас.

Не дуже то і зрозуміло. Зрештою і switch з цим справляється. Стосовно нашого прикладу з героїнею шаблон буде виглядати наступним чином:

інтерфейс стану

Для початку визначимо інтерфейс для стану. Кожен біт поведінки, що залежить від стану - тобто все що ми раніше реалізовували за допомогою switch - перетворюється у віртуальний метод цього інтерфейсу. У нашому випадку це handleInput () і update ().

class HeroineState (public: virtual ~ HeroineState () () virtual void handleInput{} {} };

Класи для кожного з станів

Для кожного стану ми визначаємо клас, який реалізує інтерфейс. Його методи визначають поведінку героїні в даному стані. Іншими словами беремо всі варіанти з switch в попередньому прикладі перетворюємо їх в клас стану. наприклад:

class DuckingState: public HeroineState (public: DuckingState (): chargeTime_ (0) () virtual void handleInput (Heroine & heroine, Input input)(If (input \u003d\u003d RELEASE_DOWN) ( // Перехід в стан стояння ... heroine.setGraphics (IMAGE_STAND); )) virtual void update (Heroine & heroine)(ChargeTime _ ++; if (chargeTime_\u003e MAX_CHARGE) (heroine.superBomb ();)) private: int chargeTime_; );

Зверніть увагу, що ми перенесли chargeTime_ з класу самої героїні в клас DuckingState. І це дуже добре, тому що цей шматок даних має значення тільки в цьому стані і наша модель даних явно про це свідчить.

Делегування до стану

class Heroine (public: virtual void handleInput (Input input)(State _-\u003e handleInput (* this, input);) virtual void update ()(State _-\u003e update (* this);) // Інші методи ... private: HeroineState * state_; );

Щоб "змінити стан" нам потрібно просто зробити так, щоб state_ вказував на інший об'єкт HeroineState. В цьому власне і полягає шаблон Стан.

Виглядає досить схоже на шаблони Стратегія (Strategy) GoF і Об'єкт тип (Type Object). У всіх трьох у нас є головний об'єкт, що делегує до підлеглого. різниця в призначення.

  • Мета Стратегії полягає в зменшенні зв'язності (Decouple) між головним класом і його поведінкою.
  • Метою Об'єкт тип (Type Object) є створення певної кількості об'єктів, які поводяться однаково за допомогою поділу між собою спільної об'єкта типу.
  • Метою Стани є зміна поведінки головного об'єкта через зміну об'єкта до якого він делегує.

А де ж ці об'єкти стану?

Я вам дещо не сказав. Щоб змінити стан, нам потрібно присвоїти state_ нове значення, яке вказує на новий стан, але звідки цей об'єкт візьметься? У нашому прикладі з enum думати нема про що: значення enum - це просто примітиви на зразок чисел. Але тепер наші стану представлені класами і це значить, що нам потрібні покажчики на реальні екземпляри. Існує два найпоширеніших відповіді:

статичні стану

Якщо об'єкт стану не має ніяких інших полів, єдине, що він зберігає - це покажчик на внутрішню віртуальну таблицю методів, для того щоб ці методи можна було викликати. В такому випадку, немає ніякої необхідності мати більше одного екземпляра класу: кожен з примірників все одно буде однаковим.

Якщо у вашого стану немає полів і тільки один віртуальний метод, можна ще сильніше спростити шаблон. замінимо кожен клас стану функцією стану - звичайною функцією верхнього рівня. І відповідно поле state_ в нашому головному класі перетвориться в простий покажчик на функцію.

Цілком можна обійтися єдиним статичним екземпляром. Навіть якщо у вас ціла купа FSM, що перебувають одночасно в одному і тому ж стані, вони можуть вказувати на один і той же статичний примірник, бо нічого специфічного для конкретного кінцевого автомата в ньому немає.

Куди ви помістіть статичний примірник - це вже ваша справа. Знайдіть таке місце, де це буде доречно. Давайте помістимо наш екземпляр в базовий клас. Без будь-якої причини.

class HeroineState (public: static StandingState standing; static DuckingState ducking; static JumpingState jumping; static DivingState diving; // Решта код ... };

Кожне з цих статичних полів - екземпляр стану, використовуваного грою. Щоб змусити героїню підстрибнути, стан стояння зробить щось на кшталт:

if (input \u003d\u003d PRESS_B) (heroine.state_ \u003d & HeroineState :: jumping; heroine.setGraphics (IMAGE_JUMP);)

примірники станів

Іноді попередній варіант не злітає. Статичний стан не підійде для стану присядки. У нього є поле chargeTime_ і воно специфічно для героїні, яка буде присідати. Це ще зле бідно спрацює в нашому випадку, тому що у нас всього одна героїня, але якщо ми захочемо додати кооператив для двох гравців, у нас будуть великі проблеми.

В такому випадку, нам слід створювати об'єкт стану, коли ми переходимо в нього. Це дозволить кожному FSM мати власний примірник стану. Звичайно, якщо ми виділяємо пам'ять під нове стан, це означає нам слід звільнити займану пам'ять поточного. Ми повинні бути обережні, тому що код, який викликає зміни знаходиться в методах поточного стані. Ми не хочемо, щоб видалити this з-під себе.

Замість цього, ми дозволимо handleInput () в HeroineState опціонально повертати новий стан. Коли це станеться, Heroine видалить старе стан і поміняє його на нове, наприклад, так:

void Heroine :: handleInput (Input input) (HeroineState * state \u003d state _-\u003e handleInput (* this, input); if (state! \u003d NULL) (delete state_; state_ \u003d state;))

Таким чином, ми не видаляємо попередній стан, поки ми не повернулися зі свого методу. Тепер, стан стояння може перейти до стану нирок шляхом створення нового екземпляра:

HeroineState * StandingState :: handleInput (Heroine & heroine, Input input) (if (input \u003d\u003d PRESS_DOWN) (// Other code ... return new DuckingState ();) // Stay in this state. Return NULL;)

Коли у мене виходить, я вважаю за краще використовувати статичні стану, потім що вони не займають пам'ять і такти процесора, виділяючи об'єкти при кожній зміні стану. Для станів, які не уявляють з себе щось більше, ніж просто стану - це якраз те що потрібно.

Звичайно, коли ви виділяєте пам'ять під стан динамічно, вам варто подумати про можливу фрагментації пам'яті. Допомогти може шаблон Пул об'єктів (Object Pool).

Дії для входу і виходу

Шаблон Стан призначений для інкапсуляції всього поведінки і пов'язаних з ним даних усередині одного класу. У нас досить непогано виходить, але залишилися деякі нез'ясовані деталі.

Коли героїня змінює стан, ми також перемикаємо її спрайт. Прямо зараз, цей код належить стану, з якого вона перемикається. Коли стан переходить від нирка в стан стояння, то нирок встановлює її образ:

HeroineState * DuckingState :: handleInput (Heroine & heroine, Input input) (if (input \u003d\u003d RELEASE_DOWN) (heroine.setGraphics (IMAGE_STAND); return new StandingState ();) // Other code ...)

Те, що ми дійсно хочемо, кожне стан контролювало свою власну графіку. Ми можемо добитися цього, додавши в стан вхідний дію (entry action):

class StandingState: public HeroineState (public: virtual void enter (Heroine & heroine)(Heroine.setGraphics (IMAGE_STAND);) // Other code ...);

Повертаючись до Heroine, ми модифікуємо код, домагаючись, щоб зміна стану супроводжувалося викликом функції вхідного дії нового стану:

void Heroine :: handleInput (Input input) (HeroineState * state \u003d state _-\u003e handleInput (* this, input); if (state! \u003d NULL) (delete state_; state_ \u003d state; // Виклик вхідного дії нового стану. state _-\u003e enter (* this); ))

Це дозволить спростити код стану DuckingState:

HeroineState * DuckingState :: handleInput (Heroine & heroine, Input input) (if (input \u003d\u003d RELEASE_DOWN) (return new StandingState ();) // Other code ...)

Все це робить перемикання в стояння і стан стояння піклується про графік. Тепер наші стану дійсно вміщені. Ще однією приємною особливістю такого вхідного дії є те, що воно запускається при вході в стан незалежно від стану, в якому ми перебували.

На більшості графів станів з реального життя присутні кілька переходів в один і той же стан. Наприклад, наша героїня може стріляти зі зброї стоячи, сидячи або в стрибку. А це означає, що у нас може з'явитися дублювання коду всюди, де це відбувається. Вхідний дію дозволяє зібрати його в одному місці.

Можна по аналогії зробити і вихідна дія (exit action). Це буде просто метод, який ми будемо викликати для стану, перед тим, як залишаємо його і перемикається на новий стан.

І чого ж ми досягли?

Я стільки часу витратив, щоб продати вам FSM, а тепер збираюся вирвати килимок з під ніг. Все, що я до сих пір говорив - правда і відмінно вирішує проблеми. Але, так вже вийшло, що найголовніші достоїнства кінцевих автоматів, одночасно є і їх найбільшими недоліками.

Стан автомата допомагає вам серйозно розплутати код, організувавши його у вкрай сувору структуру. Все, що у нас є - це фіксований набір станів, єдине поточний стан і жорстко запрограмовані переходи.

Кінцевий автомат не володіє повнотою по Тьюрингу (Turing complete). Теорія автоматів описує повноту через серію абстрактних моделей, кожна з яких складніше попередньої. Машина Тьюринга - одна з найвиразніших.

"Повнота по Тьюрігну" означає систему (зазвичай мова програмування), що володіє достатньою виразністю для реалізації машини Тьюринга. У свою чергу це означає що всі повні по Тьюрингу мови приблизно однаково виразні. FSM недостатньо виразні щоб увійти в цей клуб.

Якщо ж ви спробуєте використовувати машину станів для чого-небудь більш складного, як наприклад ігровий AI, ви відразу уткнется в обмеження цієї моделі. На щастя, наші попередники навчилися обходити деякі перешкоди. Я закінчу цей розділ декількома такими прикладами.

Машина конкурентних станів

Ми вирішили додати нашої героїні можливість носити зброю. Хоча вона тепер озброєна, вона як і раніше може робити все, що робила раніше: бігати, стрибати, присідати і т.д. Але тепер, роблячи все це, вона ще може і стріляти зі зброї.

Якщо ми захочемо вмістити таку поведінку в рамки FSM, нам доведеться подвоїти кількість станів. Для кожного з станів нам доведеться завести ще одне таке ж, але вже для героїні зі зброєю: стояння, стояння зі зброєю, стрибок, стрибок зі зброєю .... Ну ви зрозуміли.

Якщо додати ще кілька видів зброї, кількість станів збільшиться комбинаторно. І це не просто купа станів, а ще й купа повторів: збройне і беззбройна стану практично ідентичні за винятком частини коду, що відповідає за стрілянину.

Проблема тут в тому, що ми змішуємо дві частини стану - що вона робить і що тримає в руках - в один автомат. Щоб змоделювати всі можливі комбінації, нам потрібно завести стан для кожної пари. Рішення очевидно: треба завести два окремих кінцевих автомата.

Якщо ми хочемо об'єднати n станів дії і m станів того, що тримаємо в руках у один кінцевий автомат - нам потрібно n × m станів. Якщо у нас буде два автомата - нам знадобиться n + m станів.

Наш перший кінцевий автомат з діями ми залишимо без змін. А на додаток до нього створимо ще один автомат для опису того, що героїня тримає. Тепер у Heroine буде два посилання на "стан", по одній для кожного автомата.

class Heroine ( // Решта код ... private: HeroineState * state_; HeroineState * equipment_; );

Для ілюстрації ми використовуємо повну реалізацію шаблону Стан для другого кінцевого автомата, хоча на практиці в даному випадку вистачило б простого булевского прапора.

Коли героїня делегує введення станів, вона передає переклад обом кінцевим автоматам:

void Heroine :: handleInput (Input input) (state _-\u003e handleInput (* this, input); equipment _-\u003e handleInput (* this, input);)

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

Кожен кінцевий автомат може реагувати на введення, породжувати поведінку і змінювати свій стан незалежно від інших автоматів. І коли обидва стану практично не пов'язані між собою це відмінно працює.

На практиці ви можете зустріти ситуацію, коли стану взаємодіють один з одним. Наприклад, вона не може вистрілити в стрибку або наприклад виконати атаку з підкатом коли озброєна. Щоб забезпечити таку поведінку і координацію автоматів в коді, вам доведеться повернутися до тієї ж самої грубої перевірці через if іншого кінцевого автомата. Чи не саме елегантне рішення, але принаймні працює.

Ієрархічна машина станів

Після подальшого пожвавлення поведінки героїні, у неї напевно з'явиться цілий букет схожих станів. Наприклад, у не можуть бути стану стояння, ходьби, бігу та скочування зі схилів. У будь-якому з цих станів натискання на B змушує її підстрибнути, а натискання вниз - присісти.

У простій реалізації кінцевого автомата ми дублювали цей код для всіх станів. Але звичайно було б набагато краще, якби нам потрібно було написати код всього один раз і після цього ми могли б використовувати його повторно для всіх станів.

Якби це був просто об'єктно-орієнтована код, а не кінцевий автомат, можна було б використовувати такий прийом поділу коду між станами, як успадкування. Можна визначити клас для стану "на землі", який буде обробляти підстрибування і присідання. Стояння, ходьба, біг і скочування для нього успадковується і додає своє додаткове поведінку.

Таке рішення має як хороші, так і погані наслідки. Спадкування - це потужний інструмент для повторного використання коду, але в той же час воно дає дуже сильну зв'язність між двома шматками коду. Молот занадто важкий, щоб бити їм бездумно.

У такому вигляді вийшла структура буде називатися ієрархічна машина станів (або ієрархічний автомат). А у кожного стану може бути своє суперсостояніе (Сам стан при цьому називається підстанів). Коли настає подія і підстан його і не виконує жодних, воно передається по ланцюжку суперсостояній вгору. Іншими словами, виходить подоба перевизначення успадкованого методу.

Насправді, якщо ми використовуємо оригінальний шаблон Стан для реалізації FSM, ми вже можемо використовувати успадкування класів для реалізації ієрархії. Визначимо базовий клас для суперкласу:

class OnGroundState: public HeroineState (public: virtual void handleInput (Heroine & heroine, Input input)(If (input \u003d\u003d PRESS_B) (// Підстрибнути ...) else if (input \u003d\u003d PRESS_DOWN) (// Присісти ...)));

А тепер кожен підклас буде його наслідувати:

class DuckingState: public OnGroundState (public: virtual void handleInput (Heroine & heroine, Input input)(If (input \u003d\u003d RELEASE_DOWN) (// Встаємо ...) else ( // Введення не був оброблений. Тому передаємо його вище за ієрархією. OnGroundState :: handleInput (heroine, input); )));

Звичайно, це не єдиний спосіб реалізації ієрархії. Але, якщо ви не використовуєте шаблон Стан Банди Чотирьох, це не спрацює. Замість цього ви можете змоделювати чітку ієрархію поточних станів і суперсостояній за допомогою стека станів замість єдиного стану в головному класі.

Поточний стан буде знайти у верхній частині стека, під ним його суперсостояніе, далі суперсостояніе для цього суперсостоянія і т.д. І коли вам потрібно буде реалізувати специфічне для стану поведінку, ви почнете з верху стека спускатися по ньому вниз, поки стан його не оброблені. (А якщо не обробить - значить ви його просто ігноруєте).

Автомат з магазинної пам'яттю

Є ще одне звичайне розширення кінцевих автоматів, також використовує стек стану. Тільки тут стекявляє зовсім іншу концепцію і використовується для вирішення інших проблем.

Проблема в тому, що у кінцевого автомата немає концепції історії. Ви знаєте в якому стані ви перебуваєте, Але у вас немає ніякої інформації про те, в якому стані ви були. І відповідно немає простої можливості повернутися в попередній стан.

Ось простий приклад: Раніше ми дозволили нашій безстрашної героїні озброїтися до зубів. Коли вона стріляє зі своєї зброї, нам потрібно нове стан для програвання анімації пострілу, породження кулі і супутніх візуальних ефектів. Для цього ми створюємо нове FiringState і робимо в нього переходи з усіх станів, в яких героїня може стріляти після натискання кнопки стрільби.

Так як це поведінка дублюється між декількома станами, тут як раз можна застосувати ієрархічну машину станів для повторного використання коду.

Складність тут в тому, що потрібно якимось чином зрозуміти в який стан потрібно перейти після стрільби. Героїня може вистрілити всю обойму, поки вона стоїть на місці, біжить, стрибає або присідає. Коли послідовність стрільби закінчена, їй потрібно повернутися в стан, в якому вона була до стрільби.

Якщо ми прив'язуємося до чистого FSM, ми відразу забуваємо в якому стані ми були. Щоб за цим стежити, нам потрібно визначити безліч практично однакових станів - стрілянина стоячи, стрілянина в бігу, стрілянина в стрибку і т.д. Таким чином, у нас утворюються жорстко закодовані переходи, перехідні в нормальний стан за своїм закінчення.

Що нам насправді потрібно - так це можливість зберігати стан, в якому ми перебували до стрільби і після стрілянини згадувати його знову. Тут нам знову може допомогти теорія автоматів. Відповідна структура даних називається Автомат з магазинної пам'яттю (Pushdown Automaton).

Там, де в кінцевому автоматі у нас знаходиться єдиний покажчик на стан, в автоматі з магазинної пам'яттю знаходиться їх стек. У FSM перехід до нового стану замінює собою попередній. Автомат з магазинної пам'яттю теж дозволяє це робити, але додає сюди ще дві операції:

    Ви можете помістити (push) Новий стан в стек. Поточний стан завжди буде знаходитися зверху стека, так що це і є операція переходу в новий стан. Але при цьому старе стан залишається прямо під поточним в стеці, а не зникає безслідно.

    Ви можете вилучити (pop) Верхнє стан з стека. Стан пропадає і поточним стає то що знаходилося під ним.

Це все що нам потрібно для стрільби. ми створюємо єдине стан стрільби. Коли ми натискаємо кнопку стрільби, знаходячись в іншому стані, ми поміщаємо (push) Стан стрільби в стек. Коли анімація стрілянини закінчується, ми витягаємо (pop) Стан і автомат з магазинної пам'яттю автоматично повертає нас в попередній стан.

Наскільки вони реально корисні?

Навіть з цим розширенням кінцевих автоматів, їх можливості все одно досить обмежені. В AI сьогодні переважає тренд використання речей типу дерев поведінки (Behavior trees) і систем планування (Planning systems). І якщо вам цікава саме область AI, вся ця глава повинна просто роздратувати ваш апетит. Щоб його задовольнити, вам доведеться звернутися до інших книг.

Це зовсім не означає, що кінцеві автомати, автомати з магазинною пам'яттю і інші подібні системи повністю марні. Для деяких речей це хороші інструменти для моделювання. Кінцеві автомати корисні коли:

  • У вас є сутність, поведінка якої змінюється в залежності від її внутрішнього стану.
  • Цей стан жорстко ділиться на відносно невелику кількість конкретних варіантів.
  • Сутність постійно відповідає на серії команд введення або подій.

В іграх кінцеві автомати зазвичай використовуються для моделювання AI, але їх можна застосовувати і для реалізації призначеного для користувача введення, навігації в меню, парсинга тексту, мережевих протоколів і іншого асинхронного поведінки.

Поведінковий шаблон проектування. Використовується в тих випадках, коли під час виконання програми об'єкт повинен міняти свою поведінку в залежності від свого стану. Класична реалізація передбачає створення базового абстрактного класу або інтерфейсу, що містить всі методи і по одному класу на кожне можливе стан. Шаблон являє собою окремий випадок рекомендації «замінюйте умовні оператори поліморфізмом».

Здавалося б, все по книжці, але є нюанс. Як правильно реалізувати методи не релевантні для даного стану? Наприклад, як видалити товар з порожньою кошика або оплатити порожню корзину? Зазвичай кожен state-клас реалізує тільки релевантні методи, а в решті випадків викидає InvalidOperationException.

Порушення принципу підстановки Лісков на обличчя. Yaron Minsky запропонував альтернативний підхід: зробіть неприпустимі стану непредставімо (make illegal states unrepresentable). Це дає можливість перенести перевірку помилок з часу виконання на час компіляції. Однак control flow в цьому випадку буде організований на основі зіставлення зі зразком, а не за допомогою поліморфізму. На щастя, .

Більш детально на прикладі F # тема make illegal states unrepresentable розкрита на сайті Скотта Влашин.

Розглянемо реалізацію «стану» на прикладі кошика. У C # немає вбудованого типу union. Розділимо дані і поведінку. Сам стан будемо кодувати за допомогою enum, а поведінка окремим класом. Для зручності оголосимо атрибут, що зв'язує enum і відповідний клас поведінки, базовий клас «стану» і допишемо метод розширення для переходу від enum до класу поведінки.

інфраструктура

public class StateAttribute: Attribute (public Type StateType (get;) public StateAttribute (Type stateType) (StateType \u003d stateType ?? throw new ArgumentNullException (nameof (stateType));)) public abstract class State where T: class (protected State (T entity) (Entity \u003d entity ?? throw new ArgumentNullException (nameof (entity));) protected T Entity (get;)) public static class StateCodeExtensions (public static State ToState (This Enum stateCode, object entity) where T: class // так, так reflection повільний. Замініть компільовані expression tree // або IL Emit і буде швидко \u003d\u003e (State ) Activator.CreateInstance (stateCode .GetType () .GetCustomAttribute () .StateType, entity); )

Предметна область

Оголосимо сутність «кошик»:

Public interface IHasState where TEntity: class (TStateCode StateCode (get;) State State (get;)) public partial class Cart: IHasState (Public User User (get; protected set;) public CartStateCode StateCode (get; protected set;) public State State \u003d\u003e StateCode.ToState (This); public decimal Total (get; protected set;) protected virtual ICollection Products (get; set;) \u003d new List (); // ORM Only protected Cart () () public Cart (User user) (User \u003d user ?? throw new ArgumentNullException (nameof (user)); StateCode \u003d StateCode \u003d CartStateCode.Empty;) public Cart (User user, IEnumerable Products): this (user) (StateCode \u003d StateCode \u003d CartStateCode.Empty; foreach (var product in products) (Products.Add (product);)) public Cart (User user, IEnumerable Products, decimal total): this (user, products) (if (total<= 0) { throw new ArgumentException(nameof(total)); } Total = total; } }
Реалізуємо по одному класу на кожне стан кошика: порожню, активну і оплачену, але не будемо оголошувати загальний інтерфейс. Нехай кожне стан реалізує тільки релевантне поведінка. Це не означає, що класи EmptyCartState, ActiveCartState і PaidCartState не можуть реалізувати один інтерфейс. Вони можуть, але такий інтерфейс повинен містити тільки методи, доступні в кожному стані. У нашому випадку метод Add доступний в EmptyCartState і ActiveCartState, тому можна успадкувати їх від абстрактного AddableCartStateBase. Однак, додавати товари можна тільки в неоплачену кошик, тому загального інтерфейсу для всіх станів не буде. Таким чином ми гарантуємо відсутність InvalidOperationException в нашому коді на етапі компіляції.

Public partial class Cart (public enum CartStateCode: byte (Empty, Active, Paid) public interface IAddableCartState (ActiveCartState Add (Product product); IEnumerable Products (get;)) public interface INotEmptyCartState (IEnumerable Products (get;) decimal Total (get;)) public abstract class AddableCartState: State , IAddableCartState (protected AddableCartState (Cart entity): base (entity) () public ActiveCartState Add (Product product) (Entity.Products.Add (product); Entity.StateCode \u003d CartStateCode.Active; return (ActiveCartState) Entity.State;) public IEnumerable Products \u003d\u003e Entity.Products; ) Public class EmptyCartState: AddableCartState (public EmptyCartState (Cart entity): base (entity) ()) public class ActiveCartState: AddableCartState, INotEmptyCartState (public ActiveCartState (Cart entity): base (entity) () public PaidCartState Pay (decimal total) ( Entity.Total \u003d total; Entity.StateCode \u003d CartStateCode.Paid; return (PaidCartState) Entity.State;) public State Remove (Product product) (Entity.Products.Remove (product); if (! Entity.Products.Any ()) (Entity.StateCode \u003d CartStateCode.Empty;) return Entity.State;) public EmptyCartState Clear () (Entity. Products.Clear (); Entity.StateCode \u003d CartStateCode.Empty; return (EmptyCartState) Entity.State;) public decimal Total \u003d\u003e Products.Sum (x \u003d\u003e x.Price); ) Public class PaidCartState: State , INotEmptyCartState (public IEnumerable Products \u003d\u003e Entity.Products; public decimal Total \u003d\u003e Entity.Total; public PaidCartState (Cart entity): base (entity) ()))
Стану оголошені вкладеними ( nested) Класами не випадково. Вкладені класи мають доступ до захищених членів класу Cart, а значить нам не доведеться жертвувати инкапсуляцией суті для реалізації поведінки. Щоб не смітити в файлі класу суті я розділив оголошення на два: Cart.cs і CartStates.cs за допомогою ключового слова partial.

Public ActionResult GetViewResult (State cartState) (switch (cartState) (case Cart.ActiveCartState activeState: return View ( "Active", activeState); case Cart.EmptyCartState emptyState: return View ( "Empty", emptyState); case Cart.PaidCartState paidCartState: return View ( " Paid ", paidCartState); default: throw new InvalidOperationException ();))
Залежно від стану кошика будемо використовувати різні уявлення. Для порожній кошика виведемо повідомлення «ваша корзина порожня». В активній кошику буде список товарів, можливість змінити кількість товарів і видалити частину з них, кнопка «оформити замовлення» і загальна сума покупки.

Сплачена кошик буде виглядати так само, як і активна, але без можливості щось відредагувати. Цей факт можна відзначити виділенням інтерфейсу INotEmptyCartState. Таким чином ми не тільки позбулися порушення принципу підстановки Лісков, а й застосували принцип поділу інтерфейсу.

висновок

У прикладному коді ми можемо працювати по інтерфейсним посиланнях IAddableCartState і INotEmptyCartState, щоб повторно використовувати код, який відповідає за додавання товарів в корзину і висновок товарів в кошику. Я вважаю, що pattern matching підходить для control flow в C # тільки коли між типами немає нічого спільного. В інших випадках робота по базовій посиланням зручніше. Аналогічний прийом можна застосувати не тільки для кодування поведінки суті, але і для.

Для того, щоб правильно використовувати патерни стан і стратегія в ядрі Java додатків, важливо для Java-програмістів чітко розуміти різницю між ними. Хоча обидва шаблону, Стан і стратегія, мають схожу структуру, і обидва засновані на принципі відкритості / закритості, що представляють "O" в SOLID принципах, вони абсолютно різні за намірам. патерн стратегія в Java використовується для інкапсуляції пов'язаних наборів алгоритмів для забезпечення гнучкості виконання для клієнта. Клієнт може вибрати будь-який алгоритм під час виконання без зміни контексту класу, який використовує об'єкт Strategy. Деякі популярні приклади паттерна стратегія - це написання коду, який використовує алгоритми, наприклад, шифрування, стиснення або сортування. З іншого боку, патерн Стан дозволяє об'єкту поводитися по-різному в різному стані. Оскільки в реальному світі об'єкт часто має стану, і він веде себе по-різному в різних станах, наприклад, торговий автомат продає товари тільки якщо він в змозі hasCoin, він не продає до тих пір поки ви не покладете в нього монету. Зараз ви можете ясно бачити різницю між паттернами Стратегія і Стан, це різні наміри. Патерн Стан допомагає об'єкту керувати станом, тоді як патерн Стратегія дозволяє вибрати клієнту іншу поведінку. Ще одна відмінність, яка не так легко побачити, це хто управляє зміною в поведінці. У разі паттерна Стратегія, це клієнт, який надає різні стратегії до контексту, в паттерне Стан переходом управляє контекст або стан об'єкта самостійно. Крім того, якщо ви керуєте змінами станів в об'єкті Стан самостійно, повинна бути посилання на контекст, наприклад, в торговому автоматі повинна бути можливість викликати метод setState () для зміни поточного стану контексту. З іншого боку, об'єкт Стратегія ніколи не містить посилання на контекст, сам клієнт передає Стратегію свого вибору в контекст. Різниця між паттернами Стан і Стратегія один з популярних питань про патернах Java на інтерв'ю, в цій статті про патернах Java ми детальніше розглянемо це. Ми будемо досліджувати деякі подібності та відмінності між паттернами Стратегія і Стан в Java, які допоможуть вам поліпшити ваше розуміння цих патернів.

Схожість між паттернами Стан і Стратегія

Якщо ви подивіться на UML-діаграму патернів Стан і стратегія, можна помітити, що обидва виглядають схоже один на одного. Об'єкт, який використовує Стан для зміни своєї поведінки відомий як Context-об'єкт, аналогічно об'єкт, який використовує Стратегію щоб змінити свою поведінку згадується як Context-об'єкт. Запам'ятайте, що клієнт взаємодіє з Context-об'єкт. У разі патерну Стан контекст делегує методи виклику об'єкту Стан, який утримується у вигляді поточного об'єкта, а в разі паттерна Стратегія контекст використовує об'єкт Стратегії як параметр або надається під час створення контексту об'єкта. UML діаграма патерну Стан в Java
Ця UML діаграма для патерну Стан, зображує класичну проблему створення об'єктно-орієнтованого дизайну торгового апарату в Java. Ви можете бачити, що стан торговельного апарату представлено з використанням інтерфейсу, який далі має реалізацію для подання конкретного стану. Кожне стан також має посилання на контекст об'єкта, щоб зробити перехід в інший стан в результаті дій викликаних в контексті.
Ця UML діаграма для патерну Стратегія містить функціональні реалізації угруповань. Оскільки є багато алгоритмів сортування, цей шаблон проектування дозволяє клієнтові вибрати алгоритм при сортуванні об'єктів. Насправді Java Collection framework використовує цей патерн реалізуючи метод Collections.sort (), який використовується для сортування об'єктів в Java. Єдина різниця в тому, що замість дозволу клієнта вибирати алгоритм сортування він дозволяє йому вказати стратегію порівняння передаючи екземпляр інтерфейсу Comparator або Comparable в Java. Давайте подивимося на кілька подібностей між цими двома основними шаблонами проектування в Java:
  1. Обидва патерну, Стан і стратегія, роблять нескладним додавання нового стану і стратегії не зачіпаючи контекст об'єкта, який використовує їх.

  2. Обидва з них підтримують ваш код відповідно до принципу відкритості / закритості, тобто дизайн буде відкритий для розширень, але закритий для модифікації. У разі патернів Стан і стратегія, контекст об'єкта закритий для модифікацій, введень нових Станів або нових стратегій, або ви не маєте потребу в модифікації контексту іншого стану, або мінімальних змінах.

  3. Також як контекст об'єкта починається з стану ініціалізації об'єкта в паттерне Стан, контекст об'єкта також має стратегію за замовчуванням в разі паттерна Стратегія в Java.

  4. Патерн Стан представляє різні поведінки в формі різних станів об'єкта, в той час як патерн Стратегія являє різну поведінку в вигляді різних стратегій об'єкта.

  5. Обидва патерну, Стратегія і Стан, залежать від підкласів реалізації поведінки. Кожна конкретна стратегія розширює Абстрактну Стратегію, кожне стан є підклас інтерфейсу або абстрактного класу, який використовується для кончини Стану.

Відмінності між патернами Стратегія і Стан в Java

Отже, тепер ми знаємо, що патерни Стан і Стратегія схожі за структурою, а їх наміри різні. Давайте розглянемо деякі ключові відмінності між цими шаблонами проектування.
  1. Патерн Стратегія инкапсулирует набір пов'язаних алгоритмів, і дозволяє клієнтові використовувати взаємозамінні поведінки незважаючи на склад і делегування під час виконання, з іншого боку патерн Стан допомагає класу демонструвати різні поведінки в різних станах.

  2. Наступна різниця між паттернами Стан і Стратегія полягає в тому, що Стан инкапсулирует стан об'єкта, тоді як патерн Стратегія инкапсулирует алгоритм або стратегію. Оскільки стан пов'язаний з об'єктом воно не може бути повторно використано, але відокремлюючи стратегію або алгоритм з контексту ми можемо використовувати його повторно.

  3. У паттерне Стан особистий статок може містити посилання на контекст для реалізації переходів між станами, але Стратегія не містить посилання на контекст де вона використовується.

  4. Реалізація Стратегії може бути передана як параметр об'єкту, який буде використовувати її, наприклад, Collection.sort () приймає Comparator, який є стратегією. З іншого боку, стан є частиною самого контексту об'єкта, і протягом довгого часу контекст об'єкта переходить з одного стану в інший.

  5. Хоча і Стратегія і Стан дотримуються принципу відкритості / закритості, Стратегія також слід Принципу Єдиною Обов'язки так як кожна Стратегія містить індивідуальний алгоритм, різні стратегії незалежні один від одного. Зміна однієї стратегії не вимагає зміни іншої стратегії.

  6. Ще одне теоретичне відмінність між паттернами Стратегія і Стан полягає в тому, що творець визначає частину об'єкта "Як", наприклад, "Як" об'єкт сортування сортує дані, з іншого боку патерн Стан визначає частини "що" і "коли" в об'єкті, наприклад , що може об'єкт коли він знаходиться в певному стані.

  7. Порядок переходу стану добре визначений в паттерне Стан, такої вимоги немає до паттерну Стратегія. Клієнт вільний у виборі будь-якої реалізації Стратегії на його вибір.

  8. Деякі з загальних прикладів паттерна Стратегія - це інкапсуляція алгоритмів, наприклад, алгоритми сортування, шифрування або алгоритм стиснення. Якщо ви бачите, що ваш код повинен використовувати різні види пов'язаних алгоритмів, слід подумати про використання патерну Стратегія. З іншого боку, розпізнати можливість використання патерну Стан досить легко, якщо вам потрібно керувати станом і переходами між станами без великої кількості вкладених умовних операторів патерн Стан - потрібний патерн для використання.

  9. Останнє, але одне з найбільш важливих відмінностей між паттернами Стан і Стратегія полягає в тому, що зміна Стратегії виконується Клієнтом, а зміна Стану може бути виконано контекстом або станом об'єкта самостійно.

Це все про різницю між паттернами Стан і Стратегія в Java. Як я сказав, обидва виглядають схоже в своїх класах і UML діаграмах, обидва забезпечують відкрито / закритий принцип і інкапсулюють поведінку. Використовуйте патерн Стратегія для інкапсулювання алгоритму або стратегії, який надається контексту під час виконання, можливо як параметр або складений об'єкт і використовуйте патерн Стан для управління переходами між станами в Java. оригінал

стан - це поведінковий патерн, що дозволяє динамічно змінювати поведінку об'єкта при зміні його стану.

Поведінки, які залежать від стану, переїжджають в окремі класи. Початковий клас зберігає посилання на один з таких об'єктів-станів і делегує йому роботу.

Особливості патерну на Java

складність:

Популярність:

Застосування: Патерн Стан часто використовують в Java для перетворення в об'єкти громіздких стейт-машин, побудованих на операторах switch.

Приклади Стану в стандартних бібліотеках Java:

  • javax.faces.lifecycle.LifeCycle # execute () (контрольований з FacesServlet: поведінка залежить від поточної фази (стану) JSF)

Ознаки застосування патерну: Методи класу делегують роботу одному вкладеному об'єкту.

аудіоплеєр

Основний клас плеєра змінює свою поведінку в залежності від того, в якому стані знаходиться програвання.

states

states / State.java: Загальний інтерфейс станів

package сайт.state.example..state.example.ui.Player; / ** * Загальний інтерфейс всіх станів. * / Public abstract class State (Player player; / ** * Контекст передає себе в конструктор стану, щоб стан могло * звертатися до його даними і методам в майбутньому, якщо буде потрібно. * / State (Player player) (this.player \u003d player ;) public abstract String onLock (); public abstract String onPlay (); public abstract String onNext (); public abstract String onPrevious ();)

states / LockedState.java: Стан "заблокований"

package сайт.state.example..state.example.ui.Player; / ** * Конкретні стану реалізують методи абстрактного стану по-своєму. * / Public class LockedState extends State (LockedState (Player player) (super (player); player.setPlaying (false);) @Override public String onLock () (if (player.isPlaying ()) (player.changeState (new ReadyState (player)); return "Stop playing";) else (return "Locked ...";)) \u200b\u200b@Override public String onPlay () (player.changeState (new ReadyState (player)); return "Ready";) @ Override public String onNext () (return "Locked ...";) @Override public String onPrevious () (return "Locked ...";))

states / ReadyState.java: Стан "готовий"

package сайт.state.example..state.example.ui.Player; / ** * Вони також можуть переводити контекст в інші стани. * / Public class ReadyState extends State (public ReadyState (Player player) (super (player);) @Override public String onLock () (player.changeState (new LockedState (player)); return "Locked ...";) @ Override public String onPlay () (String action \u003d player.startPlayback (); player.changeState (new PlayingState (player)); return action;) @Override public String onNext () (return "Locked ...";) @Override public String onPrevious () (return "Locked ...";))

states / PlayingState.java: Стан "програвання"

package сайт.state.example..state.example.ui.Player; public class PlayingState extends State (PlayingState (Player player) (super (player);) @Override public String onLock () (player.changeState (new LockedState (player)); player.setCurrentTrackAfterStop (); return "Stop playing";) @Override public String onPlay () (player.changeState (new ReadyState (player)); return "Paused ...";) @Override public String onNext () (return player.nextTrack ();) @Override public String onPrevious ( ) (return player.previousTrack ();))

ui

ui / Player.java: програвач

package сайт.state.example..state.example.states..state.example.states.State; import java.util.ArrayList; import java.util.List; public class Player (private State state; private boolean playing \u003d false; private List playlist \u003d new ArrayList<>(); private int currentTrack \u003d 0; public Player () (this.state \u003d new ReadyState (this); setPlaying (true); for (int i \u003d 1; i<= 12; i++) { playlist.add("Track " + i); } } public void changeState(State state) { this.state = state; } public State getState() { return state; } public void setPlaying(boolean playing) { this.playing = playing; } public boolean isPlaying() { return playing; } public String startPlayback() { return "Playing " + playlist.get(currentTrack); } public String nextTrack() { currentTrack++; if (currentTrack > playlist.size () - 1) (currentTrack \u003d 0;) return "Playing" + playlist.get (currentTrack); ) Public String previousTrack () (currentTrack--; if (currentTrack< 0) { currentTrack = playlist.size() - 1; } return "Playing " + playlist.get(currentTrack); } public void setCurrentTrackAfterStop() { this.currentTrack = 0; } }

ui / UI.java: GUI програвача

package сайт.state.example.ui; import javax.swing. *; import java.awt. *; public class UI (private Player player; private static JTextField textField \u003d new JTextField (); public UI (Player player) (this.player \u003d player;) public void init () (JFrame frame \u003d new JFrame ( "Test player"); frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE); JPanel context \u003d new JPanel (); context.setLayout (new BoxLayout (context, BoxLayout.Y_AXIS)); frame.getContentPane (). add (context); JPanel buttons \u003d new JPanel (new FlowLayout (FlowLayout.CENTER)); context.add (textField); context.add (buttons); // Контекст змушує стан реагувати на призначений для користувача введення // замість себе. Реакція може бути різною в залежності від того, яке // стан зараз активно. JButton play \u003d new JButton ( "Play"); play.addActionListener (e -\u003e textField.setText (player.getState (). onPlay ())); JButton stop \u003d new JButton ( "Stop"); stop.addActionListener (e -\u003e textField.setText (player.getState (). onLock ())); JButton next \u003d new JButton ( "Next"); next.addActionListener (e -\u003e textField.setTe xt (player.getState (). onNext ())); JButton prev \u003d new JButton ( "Prev"); prev.addActionListener (e -\u003e textField.setText (player.getState (). onPrevious ())); frame.setVisible (true); frame.setSize (300, 100); buttons.add (play); buttons.add (stop); buttons.add (next); buttons.add (prev); ))

Demo.java: клієнтський код

package refactoring_guru.state..state.example.ui..state.example.ui.UI; / ** * Демо-клас. Тут все зводиться воєдино. * / Public class Demo (public static void main (String args) (Player player \u003d new Player (); UI ui \u003d new UI (player); ui.init ();))