Компютри Windows интернет

състояние. Държавни класове за всеки щат

Модел на поведенчески дизайн. Използва се в случаите, когато по време на изпълнение на програмата обектът трябва да промени поведението си в зависимост от състоянието си. Класическата реализация включва създаване на базов абстрактен клас или интерфейс, съдържащ всички методи и един клас за всяко възможно състояние. Моделът е специален случай на препоръката „замяна на условни изрази с полиморфизъм“.

Изглежда, че всичко е според книгата, но има нюанс. Как правилно да се прилагат методи, които не са подходящи за дадено състояние? Например, как да извадите артикул от празна количка или да платите за празна количка? Обикновено всеки клас на състояние прилага само подходящи методи и хвърля InvalidOperationException в други случаи.

Нарушаване на принципа за заместване на Лисков с човек. Ярон Мински предложи алтернативен подход: правят незаконните държави непредставими. Това прави възможно преместването на проверката за грешки от време на изпълнение във време на компилиране. Въпреки това, контролният поток в този случай ще бъде организиран въз основа на съвпадение на шаблони, а не чрез използване на полиморфизъм. За щастие,.

Повече подробности за примера на F# тема правят незаконните държави непредставимиразкри на уебсайта на Скот Влашин.

Нека разгледаме прилагането на „състояние“, използвайки примера на кошница. C# няма вграден тип обединение. Нека разделим данните и поведението. Ще кодираме самото състояние с помощта на enum и поведението като отделен клас. За удобство нека декларираме атрибут, свързващ enum и съответния клас на поведение, основния клас „състояние“, и да добавим метод на разширение, за да преминем от enum към класа на поведение.

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

публичен клас StateAttribute: Атрибут ( public Type StateType ( get; ) public StateAttribute(Type stateType) ( StateType = stateType ?? throw new ArgumentNullException(nameof(stateType)); ) ) public abstract class State където T: клас ( protected State(T entity) ( Entity = entity ?? throw new ArgumentNullException(nameof(entity)); ) protected T Entity ( get; ) ) public static class StateCodeExtensions ( public static State Да заявя (този Enum stateCode, обект обект) където T: клас // да, да отражението е бавно. Заменете с компилирано дърво на изрази // или IL Emit и ще бъде бързо => (State ) Activator.CreateInstance(stateCode .GetType() .GetCustomAttribute ().StateType, обект); )

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

Нека декларираме обекта "количка":

Публичен интерфейс IHasState където TEntity: клас ( TStateCode StateCode ( get; ) Състояние State ( get; ) ) публичен частичен клас Cart: IHasState ( public User User ( get; protected set; ) public CartStateCode StateCode ( get; protected set; ) public State State => StateCode.ToState (това); публичен десетичен Общ (вземи; защитен набор;) защитен виртуален ICollection Продукти (get; set; ) = нов списък (); // Защитен само ORM Cart() ( ) public Cart(User user) ( User = user ?? throw new ArgumentNullException(nameof(user)); StateCode = StateCode = CartStateCode.Empty; ) public Cart(User user, IEnumerable Продукти) : this(user) ( StateCode = StateCode = CartStateCode.Empty; foreach (var product in products) ( Products.Add(product); ) ) public Cart(User user, IEnumerable Продукти, десетичен сбор) : това(потребител, продукти) ( ако (общо<= 0) { throw new ArgumentException(nameof(total)); } Total = total; } }
Ще внедрим един клас за всяко състояние на количката: празна, активна и платена, но няма да декларираме общ интерфейс. Нека всяка държава прилага само съответното поведение. Това не означава, че класовете EmptyCartState, ActiveCartState и PaidCartState не могат да реализират един и същ интерфейс. Те могат, но такъв интерфейс трябва да съдържа само методи, които са налични във всяко състояние. В нашия случай методът Add е наличен в EmptyCartState и ActiveCartState, така че можем да ги наследим от абстрактния AddableCartStateBase. Можете обаче да добавяте артикули само към неплатена количка, така че няма да има общ интерфейс за всички състояния. По този начин ние гарантираме, че няма InvalidOperationException в нашия код по време на компилация.

Публичен частичен клас Cart ( public enum CartStateCode: byte ( Empty, Active, Paid ) public interface IAddableCartState ( ActiveCartState Add(Product product); IEnumerable Продукти ( get; ) ) публичен интерфейс INotEmptyCartState ( IEnumerable Продукти ( 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 = CartStateCode.Active; return (ActiveCartState)Entity.State; ) публичен IEnumerable Продукти => Entity.Products; ) публичен клас EmptyCartState: AddableCartState ( публичен EmptyCartState(Cart entity): base(entity) ( ) ) public class ActiveCartState: AddableCartState, INotEmptyCartState ( public ActiveCartState(Cart entity): base(entity) ( ) public PaidCartState Pay(decimal total) ( Entity.Total = общо; Entity.StateCode = CartStateCode.Paid; return (PaidCartState)Entity.State; ) public State Remove(Product product) ( Entity.Products.Remove(product); if(!Entity.Products.Any()) ( Entity.StateCode = CartStateCode.Empty; ) return Entity.State; ) public EmptyCartState Clear() ( Entity. Products.Clear(); Entity.StateCode = CartStateCode.Empty; return (EmptyCartState)Entity.State; ) public decimal Total => Products.Sum(x => x.Price); ) публичен клас PaidCartState: Състояние , INotEmptyCartState ( public IEnumerable Продукти => Entity.Products; public decimal Total => Entity.Total; public PaidCartState(Cart entity) : base(entity) ( ) ) )
Състоянията се обявяват за вложени ( вложени) класове не са случайни. Вложените класове имат достъп до защитени членове на класа 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); по подразбиране: хвърля ново InvalidOperationException(); ) )
В зависимост от състоянието на количката ще използваме различни изгледи. За празна количка ще покажем съобщението „вашата количка е празна“. Активната количка ще съдържа списък с продукти, възможност за промяна на броя на продуктите и премахване на някои от тях, бутон „поръчай“ и общата сума на покупката.

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

Заключение

В кода на приложението можем да работим с интерфейсните връзки IAddableCartState и INotEmptyCartState, за да използваме повторно кода, отговорен за добавяне на артикули в количката и показване на артикули в количката. Вярвам, че съпоставянето на шаблони е подходящо само за контролен поток в C#, когато няма нищо общо между типовете. В други случаи работата с базовата връзка е по-удобна. Подобна техника може да се използва не само за кодиране на поведението на обект, но и за .

Време е за признание: малко прекалих с това основно. Трябваше да става дума за модела на дизайн на GoF State. Но не мога да говоря за използването му в игри, без да засегна концепцията крайни автомати(или "FSM"). Но след като влязох в това, осъзнах, че ще трябва да си спомня йерархична държавна машинаили йерархичен автоматИ автоматична машина с памет на магазина (пушаунд автомати).

Това е много обширна тема, така че, за да бъда възможно най-кратка тази глава, ще пропусна някои очевидни примери за код и ще трябва сами да попълните някои от пропуските. Надявам се, че това не ги прави по-малко разбираеми.

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

Всичко това е ехо от старите ранни дни на изкуствения интелект. През 50-те и 60-те години изкуственият интелект се фокусира главно върху обработката на езикови структури. Много от технологиите, използвани в съвременните компилатори, са изобретени за анализиране на човешки езици.

Всички сме били там

Да приемем, че работим върху малък платформинг със странично превъртане. Нашата задача е да моделираме героиня, която ще бъде аватар на играча в света на играта. Това означава, че трябва да отговаря на въведените от потребителя данни. Натиснете B и тя ще скочи. Доста просто:

void Heroine::handleInput(Вход на вход) ( if (input == PRESS_B) ( yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); ) )

Забелязахте грешка?

Тук няма код за предотвратяване на "скачане във въздуха"; продължавайте да натискате B, докато тя е във въздуха и тя ще лети нагоре отново и отново. Най-лесният начин да разрешите това е да добавите булев флаг isJumping_ към Heroine, който ще следи кога героинята е скочила:

void Heroine::handleInput(Въвеждане на вход) ( if (input == PRESS_B) ( if (!isJumping_) ( isJumping_ = true ; // Jump... ) ) )

Също така се нуждаем от код, който ще върне isJumping_ обратно на false, когато героинята отново докосне земята. За простота пропускам този код.

void Heroine::handleInput(Въвеждане на вход) ( if (въвеждане == PRESS_B) ( // Да скочим, ако още не сме...) else if (вход == PRESS_DOWN) ( if (!isJumping_) ( setGraphics(IMAGE_DUCK); ) ) else if (input == RELEASE_DOWN) ( setGraphics(IMAGE_STAND); ) )

Забелязали ли сте грешка тук?

С този код играчът може:

  1. Натиснете надолу, за да клекнете.
  2. Натиснете B, за да скочите от седнало положение.
  3. Пуснете надолу, докато сте във въздуха.

В същото време героинята ще премине към графиката на стоене точно във въздуха. Ще трябва да добавим още един флаг...

void Heroine::handleInput(Въвеждане на вход) ( if (input == PRESS_B) ( if (!isJumping_ && !isDucking_) ( // Jump... ) ) else if (input == PRESS_DOWN) ( if (!isJumping_) ( isDucking_ = true; setGraphics(IMAGE_DUCK); ) ) else if (input == RELEASE_DOWN) ( if (isDucking_) ( isDucking_ = false; setGraphics(IMAGE_STAND); ) ) )

Сега би било чудесно да добавите способността на героинята да атакува с подхват, когато играчът натисне надолу, докато е във въздуха:

void Heroine::handleInput(Въвеждане на вход) ( if (input == PRESS_B) ( if (!isJumping_ && !isDucking_) ( // Jump... ) ) else if (input == PRESS_DOWN) ( if (!isJumping_) ( isDucking_ = true ; setGraphics(IMAGE_DUCK); ) else ( isJumping_ = false ; setGraphics(IMAGE_DIVE); ) ) else if (input == RELEASE_DOWN) ( if (isDucking_) ( // Standing... ) ) )

Отново търсене на грешки. Намерих го?

Имаме проверка, за да направим невъзможен скок във въздуха, но не и по време на борба. Добавяне на друг флаг...

Има нещо нередно в този подход. Всеки път, когато докоснем код, нещо се поврежда. Ще трябва да добавим още куп движения, които дори нямаме ходенене, но с този подход ще трябва да преодолеем куп грешки.

Програмистите, които всички ние идеализираме и които създават страхотен код, изобщо не са супермени. Те просто са развили инстинкт за код, който заплашва да въведе грешки и се опитват да го избегнат, когато е възможно.

Сложното разклоняване и променящите се състояния са точно типовете код, които трябва да избягвате.

Крайните автомати са нашето спасение

В пристъп на разочарование премахвате всичко от бюрото си, освен молив и хартия, и започвате да рисувате блок-схема. Начертаваме правоъгълник за всяко действие, което героинята може да извърши: стоене, скачане, приклекване и търкаляне. За да може да реагира на натискане на клавиши във всяко от състоянията, рисуваме стрелки между тези правоъгълници, надписваме бутоните над тях и свързваме състоянията заедно.

Поздравления, току-що създадохте държавна машина (краен автомат). Те идват от област на компютърните науки, наречена теория на автоматите (теория на автоматите), чието семейство от структури също включва известната машина на Тюринг. FSM е най-простият член на това семейство.

Изводът е следният:

    Имаме фиксиран комплект държави, който може да съдържа картечница.В нашия пример това са стоене, скачане, приклекване и търкаляне.

    Машината може да бъде само в единсъстояние във всеки един момент.Нашата героиня не може да скача и да стои едновременно. Всъщност FSM се използва на първо място, за да предотврати това.

    Последователност входили събития, предавани на машината.В нашия пример това е натискане и освобождаване на бутони.

    Всяка държава има преходен комплект, всеки от които е свързан с вход и показва състояние.При въвеждане от потребителя, ако то съвпада с текущото състояние, машината променя състоянието си до мястото, където сочи стрелката.

    Например, ако натиснете надолу, докато стоите, то ще премине в клекнало състояние. Натискането надолу, докато скачате, променя състоянието за справяне. Ако в текущото състояние не е предвиден преход за въвеждане, нищо не се случва.

В най-чистата си форма това е целият банан: състояния, въвеждане и преходи. Можете да ги изобразите под формата на блокова диаграма. За съжаление, компилаторът няма да разбере подобни драсканици. Е как тогава изпълнявамкрайна машина? The Gang of Four предлага своя собствена версия, но ние ще започнем с още по-проста.

Любимата ми аналогия с FSM е старата текстова мисия Zork. Имате свят, състоящ се от стаи, които са свързани с проходи. И можете да ги изследвате, като въведете команди като "отидете на север".

Такава карта напълно отговаря на определението за краен автомат. Стаята, в която се намирате, е текущото състояние. Всяко излизане от една стая е преход. Команди за навигация - въвеждане.

Изброявания и превключватели

Един от проблемите с нашия стар клас Heroine е, че позволява неправилна комбинация от булеви ключове: isJumping_ и isDucking_, те не могат да бъдат true едновременно. И ако имате няколко булеви флага, само един от които може да е true, не би ли било по-добре да ги замените всички с enum.

В нашия случай, използвайки enum, можем напълно да опишем всички състояния на нашия FSM по този начин:

enum състояние ( STATE_STANDING, STATE_JUMPING, STATE_DUCKING, STATE_DIVING );

Вместо куп знамена, Heroine има само едно поле state_. Ще трябва също да променим реда на разклоняване. В предишния пример с код се разклонихме първо в зависимост от входа, а след това от състоянието. Правейки това, ние групирахме кода по натиснатия бутон, но замъглихме кода, свързан със състоянията. Сега ще направим обратното и ще превключим входа в зависимост от състоянието. Ето какво получаваме:

void Heroine::handleInput(Input input) ( switch (state_) ( case STATE_STANDING: if (input == PRESS_B) ( state_ = STATE_JUMPING; yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); ) else if (input == PRESS_DOWN) ( state_ = STATE_DUCKING; setGraphics(IMAGE_DUCK); ) break; case STATE_JUMPING: if (input == PRESS_DOWN) ( state_ = STATE_DIVING; setGraphics(IMAGE_DIVE); ) break; case STATE_DUCKING: if (input == RELEASE_DOWN) ( state_ = STATE_STANDING; setGraphics (IMAGE_STAND); ) почивка ; ) )

Изглежда доста тривиално, но въпреки това този код вече е много по-добър от предишния. Все още имаме някакво условно разклоняване, но сме опростили променливото състояние до едно поле. Целият код, който управлява едно състояние, се събира на едно място. Това е най-простият начин за реализиране на краен автомат и понякога е напълно достатъчен.

Сега героинята вече няма да може да влезе несигуренсъстояние. При използване на булеви флагове някои комбинации бяха възможни, но нямаха смисъл. Когато използвате enum, всички стойности са правилни.

За съжаление вашият проблем може да надрасне това решение. Да кажем, че искаме да добавим специална атака към нашата героиня, за която героинята трябва да седне, за да презареди и след това да разтовари натрупаната енергия. И докато седим, трябва да следим времето за зареждане.

Добавете поле chargeTime_ към Heroine, за да съхранявате времето за зареждане. Да кажем, че вече имаме метод update(), извикан на всеки кадър. Нека добавим следния код към него:

void Heroine::update() ( if (state_ == STATE_DUCKING) (chargeTime_++; if (chargeTime_ > MAX_CHARGE) ( superBomb(); ) ) )

Ако познаете модела на метода за актуализиране, вие спечелихте награда!

Всеки път, когато клякаме отново, трябва да нулираме този таймер. За да направим това, трябва да променим handleInput():

void Heroine::handleInput(Input input) ( switch (state_) ( case STATE_STANDING: if (input == PRESS_DOWN) ( state_ = STATE_DUCKING; chargeTime_ = 0; setGraphics(IMAGE_DUCK); ) // Обработка на оставащия вход...прекъсване ; // Други щати... } }

В крайна сметка, за да добавим тази таксуваща атака, трябваше да променим два метода и да добавим поле chargeTime_ към Heroine, въпреки че то се използва само в свито състояние. Бих искал да имам целия този код и данни на едно място. Бандата на четиримата може да ни помогне с това.

Състояние на шаблона

За хора, добре запознати с обектно-ориентираната парадигма, всеки условен клон е възможност за използване на динамично изпращане (с други думи, извикване на виртуален метод в C++). Мисля, че трябва да отидем още по-дълбоко в тази заешка дупка. Понякога това е всичко, от което се нуждаем.

Има историческа основа за това. Много от старите апостоли на обектно-ориентираната парадигма, като бандата на четиримата и техните Модели на програмиранеи Мартин Фулър с неговия Рефакторингдойде от Smalltalk. И там ifThen е просто метод, който използвате за обработка на условието и който се прилага по различен начин за true и false обекти.

В нашия пример вече сме достигнали критичната точка, в която трябва да обърнем внимание на нещо обектно-ориентирано. Това ни води до модела на държавата. Да цитирам Бандата на четиримата:

Позволява на обектите да променят поведението си в съответствие с промените във вътрешното състояние. В този случай обектът ще се държи като друг клас.

Не е много ясно. В крайна сметка switch се справя и с това. Във връзка с нашия пример с героинята, шаблонът ще изглежда така:

Интерфейс за състоянието

Първо, нека дефинираме интерфейс за състоянието. Всеки бит зависимо от състоянието поведение - т.е. всичко, което преди това внедрихме с помощта на switch, се превръща във виртуален метод на този интерфейс. В нашия случай това са handleInput() и update().

клас HeroineState (публичен: виртуален ~HeroineState() () виртуална празнота handleInput{} {} };

Класове за всеки щат

За всяко състояние ние дефинираме клас, който имплементира интерфейса. Неговите методи определят поведението на героинята в това състояние. С други думи, вземаме всички опции от превключвателя в предишния пример и ги превръщаме в клас състояние. Например:

клас DuckingState: public HeroineState (public: DuckingState(): chargeTime_(0) () виртуална празнота handleInput (Героиня и героиня, входно въвеждане)( if (input == RELEASE_DOWN) ( // Преход към изправено състояние... heroine.setGraphics(IMAGE_STAND); ) ) актуализация на виртуална празнота (Heroine&heroine)(chargeTime_++; if (chargeTime_ > MAX_CHARGE) ( heroine.superBomb(); ) ) private : int chargeTime_; );

Моля, обърнете внимание, че преместихме chargeTime_ от собствения клас на героинята в класа DuckingState. И това е много добре, защото тази част от данните има значение само в това състояние и нашият модел на данни ясно показва това.

Делегация на държавата

клас героиня ( публичен : virtual void handleInput(Въвеждане на вход)(state_->handleInput(*this, input);) виртуална празна актуализация()(състояние_->актуализация(*това);) // Други методи... private : HeroineState* state_; );

За да „променим състоянието“, просто трябва да накараме state_ да сочи към различен HeroineState обект. Това е, от което всъщност се състои Държавният модел.

Изглежда доста подобно на шаблоните Strategy и Type Object на GoF. И в трите имаме главен обект, делегиращ на подчинен. Разликата е предназначение.

  • Целта на Стратегията е намаляваща свързаност(отделяне) между основния клас и неговото поведение.
  • Целта на тип обект е да създаде няколко обекта, които се държат еднакво, като споделят обект от общ тип помежду си.
  • Целта на държавата е да промени поведението на основния обект чрез промяна на обекта, на който делегира.

Къде са тези държавни обекти?

Има нещо, което не ти казах. За да променим състоянието, трябва да присвоим на state_ нова стойност, сочеща към новото състояние, но откъде идва този обект? В нашия пример с enum няма какво да мислим: стойностите на enum са просто примитиви като числа. Но сега нашите състояния са представени от класове, което означава, че имаме нужда от указатели към реални екземпляри. Има два най-често срещани отговора:

Статични състояния

Ако даден обект на състояние няма други полета, единственото нещо, което съхранява, е указател към вътрешна виртуална таблица с методи, така че тези методи да могат да бъдат извикани. В този случай не е необходимо да имате повече от един екземпляр на класа: всеки екземпляр ще бъде същият.

Ако вашето състояние няма полета и има само един виртуален метод, можете да опростите модела още повече. Ние ще заменим всеки Классъстояние функциясъстояние - редовна функция от най-високо ниво. И съответно полето състояние_в основния ни клас ще се превърне в прост указател на функция.

Напълно възможно е да минете само с един статиченкопие. Дори ако имате цял куп FSM, всички в едно и също състояние по едно и също време, всички те могат да сочат към един и същ статичен екземпляр, защото в него няма нищо специфично за машината на състоянието.

Къде ще поставите статичния екземпляр зависи от вас. Намерете място, където ще бъде подходящо. Нека поставим нашия екземпляр в базов клас. Без причина.

клас HeroineState ( публичен : статичен StandingState изправен; статичен DuckingState навеждане; статичен JumpingState скачане; статичен DivingState гмуркане; // Останалата част от кода... };

Всяко от тези статични полета е екземпляр на състояние, използвано от играта. За да накара героинята да скочи, изправеното състояние ще направи нещо като:

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

Държавни инстанции

Понякога предишната опция не излита. Статичното състояние не е подходящо за приклекнало състояние. Има поле chargeTime_ и е специфично за героинята, която ще приклекне. Това ще работи още по-добре в нашия случай, защото имаме само една героиня, но ако искаме да добавим кооперация за двама играчи, ще имаме големи проблеми.

В този случай трябва да създадем обект на състояние, когато се преместим в него. Това ще позволи на всеки FSM да има свое собствено състояние. Разбира се, ако разпределим памет за новусловие, това означава, че трябва освобождаванезаета памет от текущата. Трябва да внимаваме, защото кодът, който причинява промените, е в текущото състояние на метода. Не искаме сами да премахваме това отдолу.

Вместо това ще оставим handleInput() на HeroineState по избор да върне ново състояние. Когато това се случи, Heroine ще премахне старото състояние и ще го замени с новото, както следва:

void Heroine::handleInput(Input input) ( HeroineState* state = state_->handleInput(*this, input); if (state != NULL) ( delete state_; state_ = state; ) )

По този начин ние не премахваме предишното състояние, докато не се върнем от нашия метод. Сега изправеното състояние може да премине към състояние на гмуркане чрез създаване на нов екземпляр:

HeroineState* StandingState::handleInput(Heroine& heroine, Input input) ( if (input == PRESS_DOWN) ( // Друг код... връща ново DuckingState(); ) // Останете в това състояние. return NULL ; )

Когато мога, предпочитам да използвам статични състояния, защото те не заемат памет и цикли на процесора, като разпределят обекти всеки път, когато състоянието се промени. За условия, които не са повече от справедливи състояние- точно това ви трябва.

Разбира се, когато динамично разпределяте памет за състояние, трябва да помислите за възможно фрагментиране на паметта. Шаблонът Object Pool може да помогне.

Стъпки за влизане и излизане

Моделът на състоянието е проектиран да капсулира цялото поведение и свързаните с него данни в рамките на един клас. Справяме се доста добре, но все още има неясни подробности.

Когато героинята промени състоянието, ние също сменяме нейния спрайт. В момента този код принадлежи на държавата, с на когототя превключва. Когато състоянието премине от гмуркане към изправено положение, гмуркането установява своя образ:

HeroineState* DuckingState::handleInput(Heroine& heroine, Input input) ( if (input == RELEASE_DOWN) ( heroine.setGraphics(IMAGE_STAND); return new StandingState(); ) // Друг код... )

Това, което наистина искаме, е всяка държава да контролира собствената си графика. Можем да постигнем това, като добавим към държавата входно действие (действие за влизане):

клас StandingState: публичен HeroineState ( публичен: влизане във виртуална празнота (героиня и героиня)( heroine.setGraphics(IMAGE_STAND); ) // Друг код...);

Връщайки се към Heroine, ние променяме кода, за да гарантираме, че промяната на състоянието е придружена от извикване на функцията за действие за въвеждане на новото състояние:

void Heroine::handleInput(Input input) ( HeroineState* state = state_->handleInput(*this, input); if (state != NULL) ( delete state_; state_ = state; // Извикване на входното действие на новото състояние.състояние_->въведете(*това); ) )

Това ще опрости кода на DuckingState:

HeroineState* DuckingState::handleInput(Heroine& heroine, Input input) ( if (input == RELEASE_DOWN) ( return new StandingState(); ) // Друг код... )

Всичко, което прави, е да превключите в изправено положение и изправеното състояние се грижи за графиките. Сега нашите държави са наистина капсулирани. Друга хубава характеристика на такова действие за въвеждане е, че то се задейства при влизане в състояние, независимо от състоянието в койтобяхме там.

Повечето графики на състоянието в реалния живот имат множество преходи към едно и също състояние. Например, нашата героиня може да стреля с оръжие, докато стои, седи или скача. Това означава, че може да имаме дублиране на код, където и да се случи. Действието за въвеждане ви позволява да го съберете на едно място.

Можете да го направите по аналогия изходно действие (изходно действие). Това просто ще бъде метод, който ще призовем държавата преди това напусканего и превключете към ново състояние.

И какво постигнахме?

Прекарах толкова много време, продавайки ви FSM, а сега съм на път да издърпам килима изпод вас. Всичко, което казах досега, е вярно и чудесно решава проблеми. Но така се случва, че най-важните предимства на крайните автомати са и най-големите им недостатъци.

Машината на състоянието ви помага сериозно да разплитате кода си, като го организира в много строга структура. Всичко, което имаме, е фиксиран набор от състояния, едно текущо състояние и твърдо кодирани преходи.

Един краен автомат не е пълен по Тюринг. Теорията на автоматите описва пълнотата чрез серия от абстрактни модели, всеки по-сложен от предишния. Машината на Тюринг е една от най-експресивните.

„Завършен на Тюринг“ означава система (обикновено език за програмиране), която е достатъчно изразителна, за да реализира машина на Тюринг. Това от своя страна означава, че всички пълни езици на Тюринг са приблизително еднакво изразителни. FSM не са достатъчно изразителни, за да влязат в този клуб.

Ако се опитате да използвате държавна машина за нещо по-сложно, като AI за игри, веднага ще се натъкнете на ограниченията на този модел. За щастие нашите предшественици се научиха да заобикалят някои пречки. Ще завърша тази глава с няколко такива примера.

Състезателна държавна машина

Решихме да добавим способността на нашата героиня да носи оръжие. Въпреки че сега е въоръжена, тя все още може да прави всичко, което е можела да прави преди: да бяга, да скача, да прикляка и т.н. Но сега, докато прави всичко това, тя може да стреля и с оръжие.

Ако искаме да вместим това поведение в рамката на FSM, ще трябва да удвоим броя на състоянията. За всяко от състоянията ще трябва да създадем друго от същото, но за героинята с оръжие: стои, стои с оръжие, скача, скача с оръжие... Е, схванахте идеята.

Ако добавите още няколко оръжия, броят на състоянията ще се увеличи комбинаторно. И това не е просто куп състояния, но и куп повторения: въоръжените и невъоръжените състояния са почти идентични, с изключение на частта от кода, отговорна за стрелбата.

Проблемът тук е, че бъркаме две части на държавата – че тя правиКакво от това? държи в ръце- в една машина. За да моделираме всички възможни комбинации, трябва да създадем състояние за всяка двойки. Решението е очевидно: трябва да създадете две отделни държавни машини.

Ако искаме да се обединим нсъстояния на действие и мсъстояния на това, което държим в ръцете си в една крайна машина - имаме нужда n×mдържави. Ако имаме две картечници, ще ни трябват n+mдържави.

Ще оставим нашата първа държавна машина с действия непроменени. И в допълнение към нея ще създадем друга машина, която да описва какво държи героинята. Сега Heroine ще има две референции за „състояние“, по една за всяка машина.

класна героиня ( // Останалата част от кода... private : HeroineState* state_; HeroineState* оборудване_; );

За илюстрация използваме пълната реализация на модела State за втората държавна машина, въпреки че на практика обикновен булев флаг би бил достатъчен в този случай.

Когато героинята делегира въвеждане на състояния, тя предава превода на двете държавни машини:

void Heroine::handleInput(Входен вход) ( състояние_->handleInput(*това, вход); оборудване_->handleInput(*това, вход);)

По-сложните системи могат да включват крайни автомати, които могат да абсорбират част от входа, така че други машини вече да не го получават. Това ще ни позволи да предотвратим ситуация, при която няколко машини отговарят на едно и също въвеждане.

Всяка държавна машина може да реагира на въвеждане, да произвежда поведение и да променя състоянието си независимо от други щатни машини. И когато двете състояния практически не са свързани, работи чудесно.

На практика може да се натъкнете на ситуация, при която държавите взаимодействат помежду си. Например, тя не може да стреля, докато скача или, например, да извършва плъзгаща се атака, когато е въоръжена. За да осигурите това поведение и координация на автоматите в кода, ще трябва да се върнете към същата груба проверка чрез if другкраен автомат. Не е най-елегантното решение, но поне работи.

Йерархична държавна машина

След по-нататъшно съживяване на поведението на героинята, тя вероятно ще има цял куп подобни състояния. Например не може да се случи стоене, ходене, бягане и плъзгане по склонове. Във всяко от тези състояния натискането на B я кара да скочи, а натискането надолу я кара да приклекне.

В най-простата реализация на машина със състояния, ние дублирахме този код за всички състояния. Но, разбира се, би било много по-добре, ако трябва да напишем кода само веднъж и след това да можем да го използваме повторно за всички състояния.

Ако това беше просто обектно-ориентиран код, а не машина със състояния, бихме могли да използваме техника за разделяне на кода между състоянията, наречена наследяване. Можете да дефинирате клас за основното състояние, който ще се справя със скачане и приклекване. Стоенето, ходенето, бягането и търкалянето са наследени за него и добавят собствено допълнително поведение.

Това решение има както добри, така и лоши последици. Наследяването е мощен инструмент за повторно използване на код, но в същото време осигурява много силна кохезия между две части от код. Чукът е твърде тежък, за да го удряте безсмислено.

В тази форма ще се извика получената структура йерархична държавна машина(или йерархичен автомат). И всяко условие може да има свое собствено свръхдържава(самата държава се нарича подсъстояние). Когато възникне събитие и подсъстоянието не го обработва, то се предава нагоре по веригата от суперсъстояния. С други думи, изглежда като отмяна на наследен метод.

Всъщност, ако използваме оригиналния модел на състояние, за да внедрим FSM, вече можем да използваме наследяване на класове, за да приложим йерархията. Нека дефинираме базов клас за суперкласа:

клас OnGroundState: публичен HeroineState ( публичен: виртуална празнота handleInput (Героиня и героиня, входно въвеждане)( if (input == PRESS_B) ( // Jump... ) else if (input == PRESS_DOWN) ( // Squat... ) ) );

И сега всеки подклас ще го наследи:

клас DuckingState: публичен OnGroundState ( публичен: виртуална празнота handleInput (Героиня и героиня, входно въвеждане)( if (input == RELEASE_DOWN) ( // Стани... ) else ( // Входът не е обработен. Затова го предаваме по-високо в йерархията. OnGroundState::handleInput(героиня, вход); )))

Разбира се, това не е единственият начин за прилагане на йерархия. Но ако не използвате шаблона Gang of Four State, той няма да работи. Вместо това можете да моделирате ясна йерархия от текущи състояния и свръхсъстояния с стексъстояния вместо едно състояние в основния клас.

Текущото състояние ще бъде в горната част на стека, под него е неговото супер състояние, след това суперсъстоянието за товасвръхдържави и др. И когато трябва да приложите специфично за състоянието поведение, започвате от върха на стека и продължавате надолу, докато състоянието се справи с него. (И ако не го обработи, тогава просто го игнорирате).

Автоматична машина с пълнителна памет

Има друго общо разширение за държавни машини, което също използва стек на състояния. Само че тук стекът представлява напълно различна концепция и се използва за решаване на различни проблеми.

Проблемът е, че държавната машина няма концепция истории. Знаете ли в какво състояние сте? ти си, но нямате информация в какво състояние се намирате бяха. И съответно няма лесен начин за връщане в предишното състояние.

Ето един прост пример: Преди позволихме на нашата безстрашна героиня да се въоръжи до зъби. Когато тя стреля с оръжието си, имаме нужда от ново състояние, за да възпроизведем анимацията на изстрела, да създадем куршума и съпътстващите визуални ефекти. За да направим това, създаваме нов FiringState и правим преходи към него от всички състояния, в които героинята може да стреля чрез натискане на бутона за огън.

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

Трудността тук е, че трябва по някакъв начин да разберете в какво състояние трябва да отидете. следстрелба. Героинята може да изстреля целия клип, докато стои неподвижно, тича, скача или е приклекнала. Когато последователността от снимане приключи, тя трябва да се върне в състоянието, в което е била преди да снима.

Ако се привържем към чистия FSM, веднага забравяме в какво състояние сме били. За да следим това, трябва да дефинираме много почти идентични състояния - стрелба от изправено положение, стрелба от бягане, стрелба от скок и т.н. По този начин имаме твърдо кодирани преходи, които преминават в правилното състояние след завършване.

Това, от което наистина се нуждаем, е способността да съхраняваме състоянието, в което сме били преди снимането, и да го помним отново след снимането. Тук отново може да ни помогне теорията на автоматите. Съответстващата структура от данни се нарича Pushdown Automaton.

Когато в краен автомат имаме един указател към състоянието, в автомат с памет за съхранение има техен стек. При FSM преходът към ново състояние замества предишното. Машина с памет за магазин също ви позволява да направите това, но добавя още две операции:

    Можеш място (тласък) ново състояние в стека. Текущото състояние винаги ще бъде в горната част на стека, така че това е операцията за преминаване към ново състояние. Но в същото време старото състояние остава точно под текущото в стека и не изчезва безследно.

    Можеш екстракт (поп) горно състояние от стека. Държавата изчезва и това, което е било под нея, става актуално.

Това е всичко, което ни трябва за стрелба. Ние създаваме единственото нещосъстояние на снимане. Когато натиснем бутона за пожар, докато сме в различно състояние, ние място (тласък) състояние на снимане в купчина. Когато снимачната анимация приключи, ние екстракт (поп) състояние и машина с пълнителна памет автоматично ни връща в предишното състояние.

Колко полезни са наистина?

Дори и с това разширяване на държавните машини, техните възможности все още са доста ограничени. В AI днес преобладаващата тенденция е да се използват неща като дървета на поведението(дървета на поведението) и системи за планиране(системи за планиране). И ако се интересувате особено от областта на ИИ, цялата тази глава трябва да събуди апетита ви. За да го задоволите, ще трябва да се обърнете към други книги.

Това изобщо не означава, че автоматите с ограничени състояния, машините с пълнителна памет и други подобни системи са напълно безполезни. За някои неща това са добри инструменти за моделиране. Щатските машини са полезни, когато:

  • Имате обект, чието поведение се променя в зависимост от вътрешното му състояние.
  • Това условие е строго разделено на сравнително малък брой специфични опции.
  • Обектът непрекъснато отговаря на поредица от входни команди или събития.

В игрите държавните машини обикновено се използват за моделиране на AI, но те могат да се използват и за въвеждане на потребителски данни, навигация в менюто, анализ на текст, мрежови протоколи и друго асинхронно поведение.

"Моделдържава"източник.ру

Състоянието е модел на поведение на обекта, който определя различна функционалност в зависимост от вътрешното състояние на обекта. уебсайт уебсайт източник оригинал

Условия, задача, цел

Позволява на обект да променя поведението си в зависимост от вътрешното си състояние. Тъй като поведението може да се променя напълно произволно без никакви ограничения, отвън изглежда, че класът на обекта се е променил.

Мотивация

Помислете за класа TCP връзка, което представлява мрежова връзка. Обект от този клас може да бъде в едно от няколко състояния: Установен(инсталиран), Слушане(слушане), Затворено(затворен). Когато обект TCP връзкаполучава заявки от други обекти, той отговаря по различен начин в зависимост от текущото състояние. Например отговорът на искане Отворете(open) зависи от това дали връзката е в състояние Затвореноили Установен. Моделът на състоянието описва как даден обект TCP връзкаможе да се държи различно, когато е в различни състояния. източник на сайта оригинален сайт

Основната идея на този модел е да се въведе абстрактен клас TCPStateза представяне на различни състояния на връзка. Този клас декларира интерфейс, който е общ за всички класове, които описват различни работници. оригинален източник.ru

състояние. В тези подкласове TCPStateприлага се специфично за състоянието поведение. Например в часовете TCP е установенИ TCP е затворенвнедрено специфично за състоянието поведение УстановенИ Затвореносъответно. уебсайт уебсайт оригинален източник

original.ru

Клас TCP връзкасъхранява обект на състояние (екземпляр на подклас TCPState), представляващ текущото състояние на връзката, и делегира всички зависещи от състоянието заявки към този обект. TCP връзкаизползва своя собствена инстанция на подкласа TCPStateдоста просто: извикване на методи на един интерфейс TCPState, само в зависимост от това какъв конкретен подклас се съхранява в момента TCPState-a - резултатът е различен, т.е. в действителност се изпълняват операции, които са специфични само за това състояние на връзката. източник original.ru

И всеки път, когато състоянието на връзката се промениTCP връзкапроменя своя обект на състояние. Например, когато установена връзка е затворена, TCP връзказамества екземпляр на клас TCP е установенкопие TCP е затворен. сайт сайт с оригинален източник

Признаци на приложение, използване на държавния модел

Използвайте модела на състоянието в следните случаи: source.ru
  1. Когато поведението на обект зависи от неговото състояние и трябва да се промени по време на изпълнение. източник original.ru
  2. Когато кодът на операцията съдържа условни инструкции, състоящи се от много разклонения, в които изборът на разклонение зависи от състоянието. Обикновено в този случай състоянието се представя от изброени константи. Често същата структура на условен оператор се повтаря в няколко операции.Моделът на състоянието предполага поставянето на всеки клон в отделен клас. Това ви позволява да третирате състоянието на даден обект като независим обект, който може да се променя независимо от другите. източник на сайта оригинален сайт

Решение

уебсайт уебсайт оригинален източник

източник.ру

Участници в държавния модел

източник.ру
  1. Контекст(TCPConnection) - контекст.
    Дефинира единен интерфейс за клиентите.
    Съхранява екземпляр на подклас ConcreteState, което определя текущото състояние. кодова лаборатория.
  2. състояние(TCPState) - състояние.
    Дефинира интерфейс за капсулиране на поведението, свързано с конкретно състояние на контекста. сайт източник сайт оригинал
  3. Подкласове ConcreteState(TCPEstablished, TCPListen, TCPClosed) - специфично състояние.
    Всеки подклас изпълнява поведение, свързано с някакво състояние на контекста Контекст. сайт сайт с оригинален източник

Схема за използване на шаблона State

Клас Контекстделегира заявки към текущия обект ConcreteState. сайт сайт с оригинален източник

Контекстът може да се предава като аргумент към обект състояние, който ще обработи заявката. Това позволява обектът състояние ( ConcreteState) достъп до контекста, ако е необходимо. кодова лаборатория.

Контекст- Това е основният интерфейс за клиентите. Клиентите могат да конфигурират контекст с обекти на състояние състояние(по-точно ConcreteState). След като контекстът е конфигуриран, клиентите вече не трябва да комуникират директно с обекти на състояние (само чрез общия интерфейс състояние). източник.ру

В този случай също Контекст, или самите подкласове ConcreteStateможе да реши при какви условия и в какъв ред настъпва промяната на състоянията. уебсайт уебсайт източник оригинал

Въпроси относно прилагането на държавния модел

Въпроси относно прилагането на държавния модел: източник original.ru
  1. Какво определя преходите между състоянията.
    Моделът на състоянието не казва нищо за това кой участник определя условията (критериите) за преход между състоянията. Ако критериите са фиксирани, тогава те могат да бъдат внедрени директно в класа Контекст. Въпреки това, като цяло, по-гъвкав и правилен подход е да се разрешат подкласове на самия клас състояниеопределя следващото състояние и момент на преход. За да направите това в клас Контексттрябва да добавим интерфейс, който позволява от обекти състояниезадайте състоянието му.
    Тази децентрализирана логика на преход е по-лесна за модифициране и разширяване - просто трябва да дефинирате нови подкласове състояние. Недостатъкът на децентрализацията е, че всеки подклас състояниетрябва да „знае“ за поне един подклас на друго състояние (към което той всъщност може да превключи текущото състояние), което въвежда зависимости на изпълнението между подкласовете. сайт източник сайт оригинал

    източник original.ru
  2. Таблична алтернатива.
    Има друг начин за структуриране на управляван от състояние код. Това е принципът на краен автомат. Той използва таблица за картографиране на входовете към преходите на състоянията. С негова помощ можете да определите в кое състояние трябва да преминете, когато пристигнат определени входни данни. По същество ние заменяме условния код с търсене в таблица.
    Основното предимство на машината е нейната редовност: за да промените критериите за преход, е достатъчно да промените само данните, а не кода. Но има и недостатъци:
    - търсенето в таблица често е по-малко ефективно от извикването на функция,
    - представянето на логиката на прехода в единен табличен формат прави критериите по-малко ясни и следователно по-трудни за разбиране,
    - обикновено е трудно да се добавят действия, които придружават преходите между състоянията. Табличният метод отчита състоянията и преходите между тях, но трябва да бъде допълнен, за да могат да се извършват произволни изчисления при всяка промяна на състоянието.
    Основната разлика между базираните на таблици машини за състояние и Pattern State може да се формулира по следния начин: Pattern State моделира зависимо от състоянието поведение, а методът на таблицата се фокусира върху дефинирането на преходи между състояния. original.ru

    сайт сайт с оригинален източник
  3. Създаване и унищожаване на държавни обекти.
    По време на процеса на разработка обикновено трябва да избирате между:
    - създаване на държавни обекти, когато са необходими и унищожаването им веднага след употреба,
    - създавайки ги предварително и завинаги.

    Първият вариант е за предпочитане, когато не е известно предварително в какви състояния ще изпадне системата и контекстът сравнително рядко променя състоянието. В същото време ние не създаваме обекти, които никога няма да бъдат използвани, което е важно, ако много информация се съхранява в обекти на състояние. Когато промените в състоянието се случват често, така че не искате да унищожите обектите, които ги представляват (защото те може да са необходими отново много скоро), трябва да използвате втория подход. Времето за създаване на обекти се изразходва само веднъж, в самото начало, а времето за унищожаване изобщо не се изразходва. Вярно е, че този подход може да се окаже неудобен, тъй като контекстът трябва да съхранява препратки към всички състояния, в които системата теоретично може да изпадне. източник original.ru

    source.ru оригинал
  4. Използване на динамична промяна.
    Възможно е да се променя поведението при поискване чрез промяна на класа на обекта по време на изпълнение, но повечето обектно-ориентирани езици не поддържат това. Изключение правят Perl, JavaScript и други езици, базирани на скриптова машина, които предоставят такъв механизъм и следователно поддържат Pattern State директно. Това позволява на обектите да променят поведението си, като променят кода на своя клас. източник.ру

    .ru източник оригинал

резултати

Резултати от употреба състояние на шаблон: източник original.ru
  1. Локализира зависимо от състоянието поведение.
    И го разделя на части, съответстващи на състояния. Моделът на състоянието поставя цялото поведение, свързано с конкретно състояние, в отделен обект. Тъй като зависимият от състоянието код се съдържа изцяло в един от подкласовете на класа състояние, тогава можете да добавяте нови състояния и преходи, просто като създавате нови подкласове.
    Вместо това, може да се използват членове на данни, за да се дефинират вътрешни състояния, след това операциите на обекта Контекстще провери тези данни. Но в този случай подобни условни изрази или изрази за разклонения биха били разпръснати из кода на класа Контекст. Добавянето на ново състояние обаче би изисквало промяна на няколко операции, което би затруднило поддръжката. Моделът на състоянието решава този проблем, но също така създава друг, тъй като поведението за различни състояния в крайна сметка се разпределя между няколко подкласа състояние. Това увеличава броя на класовете. Разбира се, един клас е по-компактен, но ако има много състояния, тогава такова разпределение е по-ефективно, тъй като в противен случай ще трябва да се справяте с тромави условни изрази.
    Наличието на тромави условни изрази е нежелателно, както и дългите процедури. Те са твърде монолитни, поради което модифицирането и разширяването на кода се превръща в проблем. Моделът на състоянието предлага по-добър начин за структуриране на зависим от състоянието код. Логиката, описваща преходите на състояния, вече не е обвита в монолитни изявления акоили превключвател, но разпределени между подкласове състояние. Чрез капсулиране на всеки преход и действие в клас, държавата се превръща в пълноценен обект. Това подобрява структурата на кода и прави целта му по-ясна. original.ru
  2. Прави преходите между състоянията ясни.
    Ако даден обект дефинира текущото си състояние единствено от гледна точка на вътрешни данни, тогава преходите между състоянията нямат изрично представяне; те се появяват само като присвояване на определени променливи. Въвеждането на отделни обекти за различни състояния прави преходите по-ясни. Освен това обекти състояниеможе да защити контекста Контекстот несъответствие на вътрешни променливи, тъй като преходите от гледна точка на контекста са атомарни действия. За да направите прехода, трябва да промените стойността само на една променлива (обектна променлива състояниев клас Контекст), а не няколко. източник.ру
  3. Обектите на състоянието могат да се споделят.
    Ако в състояние обект състояниеняма екземплярни променливи, което означава, че състоянието, което представлява, е кодирано единствено от самия тип, тогава различни контексти могат да споделят един и същ обект състояние. Когато държавите са разделени по този начин, те по същество са опортюнисти (виж опортюнистичен модел), които нямат вътрешно състояние, а само поведение. източник на сайта оригинален сайт

Пример

Нека разгледаме реализацията на примера от раздела "", т.е. изграждане на проста архитектура на TCP връзка. Това е опростена версия на TCP протокола; разбира се, тя не представлява целия протокол или дори всички състояния на TCP връзките. source.ru оригинал

Първо, нека дефинираме класа TCP връзка, който предоставя интерфейс за пренос на данни и обработва заявки за промяна на състоянието: TCPConnection. source.ru оригинал

В членска променлива състояниеклас TCP връзкасе съхранява екземпляр на класа TCPState. Този клас дублира интерфейса за промяна на състоянието, дефиниран в класа TCP връзка. източник original.ru

сайт сайт с оригинален източник

TCP връзкаделегира всички зависещи от състоянието заявки към екземпляра, съхранен в състояние TCPState. Също в клас TCP връзкаима операция ChangeState, с който можете да запишете указател към друг обект в тази променлива TCPState. Конструктор на класове TCP връзкаинициализира състояниеуказател към затворено състояние TCP е затворен(ще го дефинираме по-долу). източник original.ru

source.ru оригинал

Всяка операция TCPStateприема инстанция TCP връзкакато параметър, като по този начин позволява на обекта TCPStateдостъп до обектни данни TCP връзкаи променете състоянието на връзката. .ru

В клас TCPStateвнедри поведение по подразбиране за всички делегирани към него заявки. Може също така да промени състоянието на даден обект TCP връзкачрез операция ChangeState. TCPStateнамира се в същата опаковка като TCP връзка, така също има достъп до тази операция: TCPState . сайт сайт с оригинален източник

original.ru

В подкласове TCPStateприложено зависимо от състоянието поведение. TCP връзката може да бъде в много състояния: Установен(инсталиран), Слушане(слушане), Затворено(затворен) и т.н., като всеки от тях има свой собствен подклас TCPState. За простота ще разгледаме подробно само 3 подкласа - TCP е установен, TCPListenИ TCP е затворен. уебсайт уебсайт оригинален източник

Ru източник

В подкласове TCPStateприлага зависимо от състоянието поведение за онези заявки, които са валидни в това състояние. .ru

сайт сайт с оригинален източник

След извършване на специфични за състоянието действия, тези операции източник original.ru

причина ChangeStateза промяна на състоянието на обект TCP връзка. Самият той няма информация за TCP протокола. Това е подкласове TCPStateдефинирайте преходи между състояния и действия, продиктувани от протокола. original.ru

източник original.ru

Известни приложения на модела на състоянието

Ралф Джонсън и Джонатан Цвайг характеризират модела на състоянието и го описват във връзка с TCP протокола.
Повечето популярни програми за интерактивно рисуване предоставят "инструменти" за извършване на директни манипулационни операции. Например, инструмент за чертане на линия позволява на потребителя да щракне върху произволна точка с мишката и след това да премести мишката, за да начертае линия от тази точка. Инструментът за избор ви позволява да изберете някои форми. Обикновено всички налични инструменти се поставят в палитрата. Работата на потребителя е да избере и приложи инструмент, но в действителност поведението на редактора варира в зависимост от промяната на инструмента: с инструмента за рисуване създаваме форми, с инструмента за избор ги избираме и т.н. уебсайт уебсайт оригинален източник

За да отразите зависимостта на поведението на редактора от текущия инструмент, можете да използвате модела на състоянието. оригинален източник.ru

Можете да дефинирате абстрактен клас Инструмент, чиито подкласове изпълняват специфично за инструмента поведение. Графичният редактор съхранява връзка към текущия обект същоли му делегира входящи заявки. Когато изберете инструмент, редакторът използва различен обект, което води до промяна на поведението. .ru

Тази техника се използва в рамките на графичните редактори HotDraw и Unidraw. Тя позволява на клиентите лесно да дефинират нови видове инструменти. IN HotDrawКлас DrawingControllerпрепраща заявки към текущия обект Инструмент. IN Unidrawсе извикват съответните класове зрителИ Инструмент. Диаграмата на класовете по-долу предоставя схематично представяне на интерфейсите на класовете Инструмент

original.ru

Предназначение на държавния модел

  • Моделът State позволява на даден обект да променя поведението си в зависимост от вътрешното си състояние. Изглежда, че обектът е променил класа си.
  • Моделът State е обектно-ориентирана реализация на държавна машина.

Проблем за разрешаване

Поведението на даден обект зависи от неговото състояние и трябва да се променя по време на изпълнение на програмата. Такава схема може да се реализира с помощта на много условни оператори: въз основа на анализ на текущото състояние на обекта се предприемат определени действия. Въпреки това, с голям брой състояния, условните оператори ще бъдат разпръснати из целия код и такава програма ще бъде трудна за поддържане.

Обсъждане на държавния модел

Държавният модел решава този проблем, както следва:

  • Въвежда клас Context, който дефинира интерфейс към външния свят.
  • Въвежда абстрактния клас State.
  • Представя различните "състояния" на държавна машина като подкласове на състоянието.
  • Контекстният клас има указател към текущото състояние, който се променя, когато състоянието на крайната машина се промени.

Шаблонът State не определя къде точно се определя условието за преминаване към ново състояние. Има две опции: клас Context или подкласове State. Предимството на последната опция е, че е лесно да се добавят нови производни класове. Недостатъкът е, че всеки подклас на състояние трябва да знае за своите съседи, за да направи преход към ново състояние, което въвежда зависимости между подкласовете.

Съществува и алтернативен подход, базиран на таблици, за проектиране на крайни машини, базиран на използването на таблица, която уникално картографира входните данни към преходите между състоянията. Този подход обаче има недостатъци: трудно е да се добави изпълнение на действия при изпълнение на преходи. Подходът на модела на състоянието използва код (вместо структури от данни), за да прави преходи между състояния, така че тези действия са лесни за добавяне.

Структура на държавния модел

Класът Context дефинира външния интерфейс за клиентите и съхранява препратка към текущото състояние на обекта State. Интерфейсът на абстрактния базов клас State е същият като Context интерфейса с изключение на един допълнителен параметър - указател към Context екземпляр. Произведените от състояние класове дефинират специфично за състоянието поведение. Класът обвивка на контекст делегира всички получени заявки към обект "текущо състояние", който може да използва получения допълнителен параметър за достъп до екземпляра на контекста.

Моделът State позволява на даден обект да променя поведението си в зависимост от вътрешното си състояние. Подобна картина може да се наблюдава и при работата на вендинг машина. Машините могат да имат различни състояния в зависимост от наличността на стоките, количеството получени монети, възможността за обмен на пари и т.н. След като купувачът е избрал и заплатил продукта, са възможни следните ситуации (състояния):

  • Дайте стоките на купувача, промяна не е необходима.
  • Дайте на купувача стоката и ресто.
  • Купувачът няма да получи стоката поради липса на достатъчно пари.
  • Купувачът няма да получи стоката поради липсата й.

Използване на модела на състоянието

  • Дефинирайте съществуващ или създайте нов клас обвивка на контекста, който да се използва от клиента като "държавна машина".
  • Създайте базов клас State, който репликира интерфейса на Context класа. Всеки метод приема един допълнителен параметър: екземпляр на класа Context. Класът State може да дефинира всяко полезно поведение "по подразбиране".
  • Създайте класове, извлечени от състояние, за всички възможни състояния.
  • Класът обвивка на контекста има препратка към обекта на текущото състояние.
  • Класът Context просто делегира всички заявки, получени от клиента, към обекта „текущо състояние“, като адресът на обекта Context се предава като допълнителен параметър.
  • Използвайки този адрес, методите на класа State могат да променят "текущото състояние" на класа Context, ако е необходимо.

Характеристики на държавния модел

  • Обектите на състоянието често са единични.
  • Flyweight показва как и кога обектите на състоянието могат да бъдат разделени.
  • Моделът на интерпретатора може да използва състояние, за да дефинира контексти за анализиране.
  • Моделите State и Bridge имат сходни структури, с изключение на това, че Bridge позволява йерархия на класове обвивки (аналози на класове "обвивки"), докато State не позволява. Тези модели имат подобни структури, но решават различни проблеми: State позволява на даден обект да променя поведението си в зависимост от вътрешното си състояние, докато Bridge разделя абстракцията от нейната реализация, така че те да могат да се променят независимо една от друга.
  • Изпълнението на модела Държава се основава на модела Стратегия. Разликите са в тяхното предназначение.

Изпълнение на модела на държавата

Нека разгледаме пример за краен автомат с две възможни състояния и две събития.

#включи използване на пространство от имена std; class Machine ( class State *current; public: Machine(); void setCurrent(State *s) ( current = s; ) void on(); void off(); ); class State ( public: virtual void on (Machine *m) ( cout<< " already ON\n"; } virtual void off(Machine *m) { cout << " already OFF\n"; } }; void Machine::on() { current->по този); ) void Machine::off() ( current->off(this); ) class ON: public State ( public: ON() ( cout<< " ON-ctor "; }; ~ON() { cout << " dtor-ON\n"; }; void off(Machine *m); }; class OFF: public State { public: OFF() { cout << " OFF-ctor "; }; ~OFF() { cout << " dtor-OFF\n"; }; void on(Machine *m) { cout << " going from OFF to ON"; m->setCurrent(ново ВКЛ()); изтрийте това; ) ); void ON::off(Machine *m) ( cout<< " going from ON to OFF"; m->setCurrent(ново ИЗКЛ()); изтрийте това; ) Machine::Machine() ( current = new OFF(); cout<< "\n"; } int main() { void(Machine:: *ptrs)() = { Machine::off, Machine::on }; Machine fsm; int num; while (1) { cout << "Enter 0/1: "; cin >>брой; (fsm. *ptrs)(); ) )

15.02.2016
21:30

Моделът State е предназначен за проектиране на класове, които имат множество независими логически състояния. Нека преминем направо към пример.

Да кажем, че разработваме клас за управление на уеб камера. Камерата може да бъде в три състояния:

  1. Не е инициализирано. Нека го наречем NotConnectedState ;
  2. Инициализирано и готово за работа, но все още не се заснемат кадри. Нека бъде ReadyState;
  3. Активен режим на заснемане на кадри. Нека обозначим ActiveState.

Тъй като работим с модела на състоянието, най-добре е да започнем с изображението на диаграмата на състоянието:

Сега нека превърнем тази диаграма в код. За да не усложняваме реализацията, пропускаме кода за работа с уеб камери. Ако е необходимо, можете сами да добавите съответните извиквания на библиотечни функции.

Веднага ще предоставя пълния списък с минимални коментари. След това ще обсъдим по-подробно ключовите детайли на това внедряване.

#включи #define DECLARE_GET_INSTANCE(ClassName) \ static ClassName* getInstance() (\ static ClassName instance;\ return \ ) class WebCamera ( public: typedef std::string Frame; public: // *********** *************************************** // Изключения // ****** ******************************************** class NotSupported: public std: :exception (); public: // ********************************************* ******* ********* // Щати // ***************************** ******* *************** клас NotConnectedState; клас ReadyState; клас ActiveState; клас State ( public: virtual ~State() ( ) virtual void connect(WebCamera*) ( throw NotSupported(); ) virtual void disconnect(WebCamera* cam) ( std::cout<< "Деинициализируем камеру..." << std::endl; // ... cam->changeState(NotConnectedState::getInstance()); ) virtual void start(WebCamera*) ( throw NotSupported(); ) virtual void stop(WebCamera*) ( throw NotSupported(); ) virtual Frame getFrame(WebCamera*) ( throw NotSupported(); ) protected: State() ( ) ); // ************************************************ ** клас NotConnectedState: обществено състояние ( public: DECLARE_GET_INSTANCE(NotConnectedState) void connect(WebCamera* cam) ( std::cout<< "Инициализируем камеру..." << std::endl; // ... cam->changeState(ReadyState::getInstance()); ) void disconnect(WebCamera*) ( throw NotSupported(); ) private: NotConnectedState() ( ) ); // ************************************************ ** клас ReadyState: публично състояние ( public: DECLARE_GET_INSTANCE(ReadyState) void start(WebCamera* cam) ( std::cout<< "Запускаем видео-поток..." << std::endl; // ... cam->changeState(ActiveState::getInstance()); ) private: ReadyState() ( ) ); // ************************************************ ** клас ActiveState: публично състояние ( public: DECLARE_GET_INSTANCE(ActiveState) void stop(WebCamera* cam) ( std::cout<< "Останавливаем видео-поток..." << std::endl; // ... cam-> << "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } }; public: explicit WebCamera(int camID) : m_camID(camID), m_state(NotConnectedState::getInstance()) { } ~WebCamera() { try { disconnect(); } catch(const NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } } void connect() { m_state->свържете (това); ) void disconnect() ( m_state->disconnect(this); ) void start() ( m_state->start(this); ) void stop() ( m_state->stop(this); ) Frame getFrame() ( return m_state ->getFrame(this); ) private: void changeState(State* newState) ( m_state = newState; ) private: int m_camID; Състояние* m_състояние; );

Насочвам вниманието ви към макроса DECLARE_GET_INSTANCE. Разбира се, използването на макроси в C++ не се препоръчва. Това обаче се отнася за случаите, когато макросът действа като аналог на функция на шаблон. В този случай винаги давайте предпочитание на последното.

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

Ние декларираме класове State в основния клас - WebCamera. За краткост използвах вградени дефиниции на членски функции от всички класове. В реални приложения обаче е по-добре да следвате препоръките за разделяне на декларацията и имплементацията в h и cpp файлове.

Класовете на състоянието се декларират в WebCamera, така че да имат достъп до частните полета на този клас. Разбира се, това създава изключително тясна връзка между всички тези класове. Но държавите се оказват толкова специфични, че повторното им използване в други контексти е изключено.

Основата на йерархията на класовете на състоянието е абстрактният клас WebCamera::State:

Class State ( public: virtual ~State() ( ) virtual void connect(WebCamera*) ( throw NotSupported(); ) virtual void disconnect(WebCamera* cam) ( std::cout<< "Деинициализируем камеру..." << std::endl; // ... cam->changeState(NotConnectedState::getInstance()); ) virtual void start(WebCamera*) ( throw NotSupported(); ) virtual void stop(WebCamera*) ( throw NotSupported(); ) virtual Frame getFrame(WebCamera*) ( throw NotSupported(); ) protected: State() ( ) );

Всички негови функции-членове съответстват на функциите на самия клас WebCamera. Директно делегиране възниква:

Class WebCamera ( // ... void connect() ( m_state->connect(this); ) void disconnect() ( m_state->disconnect(this); ) void start() ( m_state->start(this); ) void stop() ( m_state->stop(this); ) Frame getFrame() ( return m_state->getFrame(this); ) // ... Състояние* m_state; )

Ключовата характеристика е, че обектът State приема указател към екземпляра WebCamera, който го извиква. Това ви позволява да имате само три обекта състояние за произволно голям брой камери. Тази възможност се постига чрез използването на модела Singleton. Разбира се, в контекста на примера, няма да получите значителна печалба от това. Но познаването на тази техника все още е полезно.

Сам по себе си класът WebCamera не прави почти нищо. Той е напълно зависим от своите държави. А тези държави от своя страна определят условията за извършване на операции и осигуряват необходимия контекст.

Повечето функции-членове на WebCamera::State хвърлят нашия собствен WebCamera::NotSupported. Това е напълно подходящо поведение по подразбиране. Например, ако някой се опита да инициализира камера, когато тя вече е инициализирана, съвсем естествено ще получи изключение.

В същото време предоставяме внедряване по подразбиране за WebCamera::State::disconnect(). Това поведение е подходящо за две от трите състояния. В резултат на това предотвратяваме дублирането на код.

За да промените състоянието, използвайте функцията частен член WebCamera::changeState() :

Void changeState(State* newState) ( m_state = newState; )

Сега към прилагането на конкретни държави. За WebCamera::NotConnectedState е достатъчно да замените операциите connect() и disconnect():

Клас NotConnectedState: публично състояние ( public: DECLARE_GET_INSTANCE(NotConnectedState) void connect(WebCamera* cam) ( std::cout<< "Инициализируем камеру..." << std::endl; // ... cam->changeState(ReadyState::getInstance()); ) void disconnect(WebCamera*) ( throw NotSupported(); ) private: NotConnectedState() ( ) );

За всяко състояние можете да създадете един екземпляр. Това ни е гарантирано чрез деклариране на частен строител.

Друг важен елемент от представената реализация е, че преминаваме към ново състояние само ако успеем. Например, ако възникне повреда по време на инициализацията на камерата, е твърде рано да влезете в ReadyState. Основната идея е пълното съответствие между действителното състояние на камерата (в нашия случай) и обекта State.

И така, камерата е готова за работа. Нека създадем съответния клас WebCamera::ReadyState State:

Клас ReadyState: публично състояние ( public: DECLARE_GET_INSTANCE(ReadyState) void start(WebCamera* cam) ( std::cout<< "Запускаем видео-поток..." << std::endl; // ... cam->changeState(ActiveState::getInstance()); ) private: ReadyState() ( ) );

От състоянието Ready можем да влезем в активното състояние Frame Capture. За целта е предвидена операцията start(), която внедрихме.

Най-накрая стигнахме до последното логично състояние на камерата, WebCamera::ActiveState:

Клас ActiveState: публично състояние ( public: DECLARE_GET_INSTANCE(ActiveState) void stop(WebCamera* cam) ( std::cout<< "Останавливаем видео-поток..." << std::endl; // ... cam->changeState(ReadyState::getInstance()); ) Frame getFrame(WebCamera*) ( std::cout<< "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } };

В това състояние можете да спрете заснемането на кадри с помощта на stop(). В резултат на това ще бъдем върнати обратно към WebCamera::ReadyState. Освен това можем да получаваме кадри, които се натрупват в буфера на камерата. За простота, под „рамка“ имаме предвид обикновен низ. В действителност това ще бъде някакъв вид байтов масив.

Сега можем да запишем типичен пример за работа с нашия клас WebCamera:

Int main() ( WebCamera cam(0); try ( // камера в NotConnectedState cam.connect(); // камера в ReadyState cam.start(); // камера в ActiveState std::cout<< cam.getFrame() << std::endl; cam.stop(); // Можно было сразу вызвать disconnect() // cam в Состоянии ReadyState cam.disconnect(); // cam в Состоянии NotConnectedState } catch(const WebCamera::NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } return 0; }

Ето какво ще бъде изведено на конзолата като резултат:

Инициализирайте камерата... Стартирайте видеопотока... Вземете текущия кадър... Текущият кадър Спрете видеопотока... Деинициализирайте камерата...

Сега нека се опитаме да провокираме грешка. Нека извикаме connect() два пъти подред:

Int main() ( WebCamera cam(0); try ( // cam в NotConnectedState cam.connect(); // cam в ReadyState // Но за това състояние операцията connect() не е предоставена! cam.connect( ); // Хвърля изключение NotSupported ) catch(const WebCamera::NotSupported& e) ( std::cout<< "Произошло исключение!!!" << std::endl; // ... } catch(...) { // Обрабатываем исключение } return 0; }

Ето какво излиза от това:

Камерата се инициализира... Възникна изключение!!! Нека деинициализираме камерата...

Моля, имайте предвид, че камерата все още е деинициализирана. Извикването disconnect() се случи в деструктора на WebCamera. Тези. вътрешното състояние на обекта остава абсолютно правилно.

заключения

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