Шаблон состояния считается шаблоном дизайна поведения. Поведенческие шаблоны стремятся гарантировать, что объекты могут по-прежнему обмениваться сообщениями и в то же время быть слабо связанными. Это обеспечивается правильным распределением обязанностей между объектами. В случае с шаблоном состояния это реализуется, позволяя объекту изменять поведение при изменении его внутреннего состояния - иногда до такой степени, что это может выглядеть так, как будто объект изменил свой класс. Говоря простым языком, шаблон состояния можно использовать для значительного улучшения вашего кода, облегчая поддержку и расширение кода, когда новые функции должны быть добавлены в базу кода.

Кому следует прочитать эту статью?

Я предполагаю, что читатель хорошо знаком с программированием, но хочет узнать больше о шаблонах проектирования. По этой причине некоторые части кода не объясняются подробно, поскольку я предполагал, что читатель уже имеет твердое представление о программировании. Примеры написаны на Go, но шаблон состояния можно применить к любому языку, поддерживающему полиморфизм.

Проэкт

Вместе построим светофор - дважды.
Чтобы подчеркнуть, почему важно знать о шаблоне состояний, я сначала продемонстрирую, какие проблемы могут возникнуть, если мы не будем использовать его там, где это необходимо. После первоначальной демонстрации мы поговорим об ограничениях кода. Затем, наконец, мы собираемся найти гораздо лучшее решение, используя полиморфизм для реализации паттерна состояний. Наше окончательное решение также будет придерживаться принципов SOLID. В этой статье мы сосредоточимся в основном на первых двух принципах.

Принцип единственной ответственности

Функция должна делать только одно - и делать это хорошо. Имея только одну ответственность, класс, структура или функция должны иметь только одну причину для изменения. Если есть несколько причин, по которым функцию или структуру может потребоваться изменить позже, лучше всего разделить код на несколько меньших фрагментов, каждый из которых отвечает за основную задачу.

Вместо того, чтобы втиснуть множество различных функций в один класс, структуру или функцию, нужно стремиться разделить код на менее сложные конструкции и разместить их в отдельных классах, структурах и функциях.

Принцип открытости-закрытости

Приложения должны допускать простое расширение поведения без изменения существующего кода, поскольку это может привести к ошибкам при изменении рабочего кода.

Первая реализация светофора не соответствует этим важным правилам кодирования.

Хватит теории. Теперь мы собираемся совершить авантюрное погружение в теплые воды Государственного Образца, проявив прагматичность и запачкав руки - или, лучше сказать, мокрые? Мы будем учиться, написав код.

Давайте начнем, ладно?

Простая реализация

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

Поскольку писать белый моноширинный текст на черном фоне консоли - это немного обыденно, мы будем визуализировать светофор, используя крошечную игровую библиотеку под названием Pixel. Обещаю, у нас будет достаточно красивых цветов и мигающих огней, чтобы начать эпилептический припадок. Если серьезно, то нам нужно немного усложнить задачу, чтобы действительно увидеть, как все упрощается в дальнейшем.

Мы начнем с определения нашей структуры светофора.

Мы создаем структуру SimpleTrafficLight. Структура содержит поле Light и два поля, которые необходимы для рисования на экране. По передовой практике мы предоставляем конструктор для нашего светофора. Конструктор принимает два параметра, которые необходимы для вывода светофора на экран. Первоначально светофор будет гореть красным светом, поскольку в поле Свет установлено значение КРАСНЫЙ.

Затем мы продолжаем и создаем несколько методов для нашего светофора, из которых наиболее интересными являются методы NextLight и Draw. Эти методы отвечают за переключение светофора с одного режима Light на другой и отображение правильных световых сигналов на экране соответственно.

Остальные методы не требуют пояснений и используются для рисования цветных кругов на экране, который представляет собой светофор. Это дает нам простой способ узнать, как сейчас работает наш светофор.

Довольно просто, правда? Теперь давайте перейдем к основному циклу, который отвечает за изменение цвета светофора между циклами цикла, и один раз за цикл вызовем метод светофора Draw.

Как видно из представленного выше кода, мы создаем экземпляр SimpleTrafficLight, который затем используется внутри бесконечного цикла. Внутри цикла мы используем метод светофора NextLight вместе с методом Draw объекта светофора. Между этими звонками мы спим на секунду. Засыпание всего потока считается плохой практикой, так как это замораживает окно, но мы стараемся, чтобы код оставался простым, поскольку мы сосредоточены на использовании шаблонов проектирования. Позже мы улучшаем интервальный код, чтобы окно не зависало, пока светофор ожидает подходящего момента для изменения и рисования нового цветового режима.

Скомпилируйте и запустите код, и вы должны увидеть светофор, который меняет цветовой режим каждую секунду.

Добавить первое новое требование

Выглядит круто, но с этим есть проблема. Чтобы выделить точные проблемы с нашим текущим кодом и прояснить вышеупомянутые принципы, мы добавим новые функции к нашему текущему светофору. Представим, что один из наших клиентов, муниципалитет, потребовал, чтобы мы обновили все светофоры, чтобы они соответствовали новому набору требований. Муниципалитет только что купил физические сканеры скорости автомобилей для всех светофоров в одном из своих городов. Сканер - это современное устройство Интернета вещей, которое измеряет скорость автомобиля. Сканер поддерживает несколько протоколов связи, поэтому он может уведомлять систему управления дорожным движением о том, насколько быстро автомобиль едет на определенном участке дороги. Идея состоит в том, что светофор, оснащенный сканером, может обнаруживать автомобили, движущиеся на красный свет, и автомобили, движущиеся слишком быстро. Светофор может отреагировать на эту информацию, сфотографировав водителя и номерной знак и отправив его в ближайший полицейский участок.

Чтобы использовать эти новые сканеры, наш светофор должен теперь поддерживать уведомление о скорости проезжающего автомобиля при въезде на перекресток. Поэтому муниципалитет потребовал, чтобы мы добавили новые методы светофора:

(tl * SimpleTrafficLight) func CarPassingSpeed ​​(speed int, licensePlate string)

func reportDriver ()

функция redLighRunner ()

Дальнейшие требования к методу следующие. Об автомобилях, превышающих допустимую скорость, следует сообщать с помощью вызова функции reportDriver, за исключением автомобилей, движущихся на красный цвет, которые всегда должны быть оштрафованы независимо от скорости.

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

В решении используется новый оператор switch со всеми различными световыми режимами в качестве случаев. Полагаю, вы уже заметили здесь проблему?

Добавить второе новое требование

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

Свет должен измениться с RED_AND_AMBER на PURPLE на GREEN.

Опять же, я попрошу вас подумать, как вы решите эту головоломку кодирования. Когда вы задумаетесь, взгляните на приведенный ниже код. Код показывает, как можно расширить код с помощью этой новой функции.

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

Проблемы с текущим решением

Можно задать два вопроса, чтобы определить, нужно ли реорганизовать код.

Есть ли у меня один и тот же оператор if / switch в разных местах?

Замечу ли я, что я делаю одно и то же изменение одного и того же переключателя if / в нескольких местах?

К сожалению, мы можем ответить утвердительно на оба вопроса выше, а это означает, что нам необходимо рассмотреть возможность реорганизации нашего кода, поскольку мы противоречим принципам DRY (не повторяйтесь) и SOLID, которые были упомянуты ранее в этой статье. Несоблюдение принципов заставляет программиста копировать оператор switch при добавлении некоторых новых требований, как мы видели при реализации первого требования. Добавление других требований имеет неприятный побочный эффект: программист должен вносить изменения в уже работающий код. Это случилось с нами, когда мы второй раз расширили кодовую базу.

Кроме того, у нас есть логика для различных функций, размещенных в одних и тех же функциях, что усложняет чтение и понимание кода. Это стало заметно, когда мы добавили новую логику для пурпурного фонаря автобуса, когда штраф должен быть наложен только в некоторых случаях. У нас есть логика для разных режимов освещения, близких друг к другу, потому что все содержится в одном операторе switch.

Реализация конечного автомата

Иногда менее очевидно, что программа работает в состояниях, но в случае светофора это вполне очевидно. Я бы сказал, что светофор требует реализации как конечный автомат, поскольку он явно имеет несколько состояний. По этой же причине светофор был выбран в качестве примера для этой статьи. В этой статье мы определили различные светофоры как световые режимы. Фактически их следует рассматривать как состояния. Светофор должен вести себя по-разному в зависимости от того, какое состояние активно. Одновременно может быть активным только одно состояние.

Теперь мы перепишем наш светофор, чтобы он соответствовал паттерну состояний. Концепция этого шаблона состоит в том, что в любой момент времени существует четко определенное количество состояний, в которых может находиться программа. Поведение программы изменяется в зависимости от того, какое состояние в данный момент активно. Может существовать несколько состояний, но только по одному. Переход из одного состояния в другое называется переходом и может произойти в любой момент времени. Обычно это контролируется изнутри самого государства; следовательно, каждое состояние должно знать, в какое другое состояние оно может перейти.

Диаграммы UML могут отображать состояние. Схема состояния традиционного светофора представлена ​​ниже.

Состояния представлены в виде прямоугольников, а переходы - стрелками. Стрелки могут иметь текст, описывающий, какое событие вызвало переход. Черный кружок - начало выполнения нашей программы.

Хватит теории, приступим к переписыванию нашей программы.

Сначала мы создаем тип интерфейса с именем LightState и используем этот интерфейс в поле с именем State в нашей структуре TrafficLight. Мы добавляем в TrafficLight метод под названием TransitionState. Этот метод вызывается состоянием, когда нужно изменить состояние светофора. Состояние может нуждаться в некоторой инициализации; следовательно, все состояния необходимы для реализации метода EnterState. Этот метод вызывается сразу после перехода. При желании вы также можете использовать метод Leave, однако сейчас он не понадобится. В каждом состоянии также должны быть методы Draw и NextLight. Обратите внимание, как методы принимают указатель TrafficLight в качестве параметра. Таким образом состояния могут взаимодействовать с T rafficLight, частью которого они являются.

В результате наш основной файл теперь должен вызывать эти методы в состоянии светофора, а не напрямую.

Теперь все состояния могут существовать в виде собственной структуры, записанной в собственном файле.

Мы создадим эти файлы

  • red_state.go
  • red_amber_state.go
  • amber_state.go
  • green_state.go

Таким образом мы разделяем логику для каждого состояния, что упрощает поддержку кода. Отладка может выполняться быстрее, а также мы точно знаем, какая часть кода вызвала ошибку, если ошибка возникает в определенном состоянии.

Мы используем небольшой необязательный трюк, чтобы позволить нашим состояниям иметь методы по умолчанию. Уловка состоит в том, чтобы создать состояние по умолчанию, которое мы назовем DefaultLightState. Это состояние не должно использоваться само по себе, и поэтому оно не реализует все необходимые методы состояния. Не имея состояния по умолчанию, которое можно было бы использовать, нам, возможно, пришлось бы реализовать пустые методы, чтобы удовлетворить интерфейс во всех наших состояниях, даже если бы в них не требовалось никакой логики. Например, это могло произойти, если интерфейс State требовал, чтобы все состояния имели метод Leave, который мы на самом деле никогда не использовали.

Добавьте следующее в файл trafficlight.go.

В целях отладки и демонстрации мы предоставили состояниям поле StateName и метод EnterState по умолчанию, который распечатает имя текущего состояния, как только светофор перейдет из одно состояние в другое. Метод CarPassingSpeed ​​ по умолчанию также предоставляется в состоянии по умолчанию. В противном случае нам пришлось бы повторить это на нескольких этапах. Красный цвет отменяет значение по умолчанию CarPassingSpeed ​​, поскольку ни одна машина не должна проезжать на красный свет - независимо от скорости. По этой причине красное состояние нуждается в собственной реализации.

Теперь, как нам расширить структуру в Go. Этого можно добиться с помощью того, что Go называет встроенными полями. Поле, объявленное с типом, но без явного имени поля, называется встроенным полем. Лучше всего это показать на примере. Взгляните на новое состояние красного света.

Посмотрите, как мы использовали вышеупомянутое встроенное поле в структуре redState?

Также следует отметить метод NextLight. Это показывает, как мы можем переходить из одного состояния в другое, которое должно контролироваться состояниями. Часто у вас не было бы метода с названием что-то вроде NextLight или NextState. Переход может происходить изнутри метода состояния и происходить из-за некоторой управляющей логики, решающей, когда пора переходить. Одно состояние может иметь разные состояния для перехода в зависимости от различных переменных. В случае со светофором у нас очень линейный поток перехода, и я решил, что состояние может предоставлять собственный метод перехода, хотя это не так часто.

Теперь другие состояния должны быть созданы аналогичным образом, и все готово.

В качестве упражнения для читателя я не стал использовать освещение полосы движения для автобусов. Я рекомендую вам познакомиться с паттерном состояний, расширив код, добавив этот свет самостоятельно.

Заключительные слова

Надеюсь, теперь у вас есть четкое представление о том, как и когда использовать шаблон состояния в ваших проектах кода. Я надеюсь, что вам понравилось читать эту статью, и теперь у вас есть внутреннее желание встраивать шаблон состояния в свои собственные проекты. Если вам понравилась статья, пожалуйста, поаплодируйте, щелкнув значки рук, чтобы больше программистов могли быть представлены к статье.

GitHub

Обе реализации светофора можно найти в моем репозитории GitHub.

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