Паттерн Состояние
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy. Урок 95 из 107.
Паттерн State позволяет объекту изменять свое поведение при изменении его внутреннего состояния, создавая впечатление, будто объект изменил свой класс. В то время как Template Method управляет шагами алгоритма, State инкапсулирует поведение, специфичное для состояния, в отдельных объектах и делегирует выполнение текущему состоянию.
В Go мы определяем интерфейс состояния и создаём конкретные состояния, которые реализуют различное поведение:
type State interface {
Handle(d *Document) string
}
type Document struct {
state State
}
func (d *Document) SetState(s State) {
d.state = s
}
func (d *Document) Publish() string {
return d.state.Handle(d)
}Каждое состояние определяет, что происходит и какое состояние наступает следующим:
type DraftState struct{}
func (s DraftState) Handle(d *Document) string {
d.SetState(ModerationState{})
return "Draft submitted for moderation"
}
type ModerationState struct{}
func (s ModerationState) Handle(d *Document) string {
d.SetState(PublishedState{})
return "Moderation approved, now published"
}
type PublishedState struct{}
func (s PublishedState) Handle(d *Document) string {
return "Already published"
}Один и тот же вызов method даёт разные результаты в зависимости от current состояния:
doc := &Document{state: DraftState{}}
fmt.Println(doc.Publish()) // Черновик отправлен на модерацию
fmt.Println(doc.Publish()) // Модерация одобрена, теперь опубликовано
fmt.Println(doc.Publish()) // Уже опубликованоState идеально подходит для объектов с различными режимами работы, например для рабочих процессов обработки заказов, компонентов UI или обработчиков подключений, где behavior полностью зависит от current состояния.
Задание
ЛегкоДавайте создадим систему поддержки тикетов, используя паттерн State! Вы создадите тикет поддержки, который проходит через разные этапы: от открытия до обработки и разрешения, причём каждое состояние определяет, какие действия возможны и что произойдёт дальше.
Вы организуете код в трёх файлах:
state.go: Define интерфейс состояния и конкретные состояния, представляющие каждый этап жизненного цикла тикета.Создайте интерфейс
TicketStateс методомHandle(t *Ticket) string, который обрабатывает тикет и при необходимости переводит его в следующее состояние.Implement три состояния:
OpenState: при обработке переводит тикет вInProgressStateи returnsTicket opened, assigning to support teamInProgressState: при обработке переводит тикет вResolvedStateи returnsWorking on ticket, issue resolvedResolvedState: при обработке остаётся в том же состоянии и returnsTicket already resolved
ticket.go: Создайте структуру тикета, которая хранит current состояние и делегирует ей behavior.Создайте структуру
Ticketс полемID(string) и полемstate(TicketState). Добавьте следующие методы:SetState(s TicketState)для изменения current состояния тикетаProcess() string, который делегирует выполнение методу Handle текущего состояния
Создайте конструктор
NewTicket(id string) *Ticket, который returns тикет, начинающий в состоянииOpenState.main.go: Покажите, как одно и то же действие produces разные результаты в зависимости от current состояния тикета.Считайте ID тикета и number раз, которые нужно обработать тикет. Создайте новый тикет с этим ID, затем вызовите
Process()указанное number раз, выводя each result в отдельной строке.
Будут предоставлены следующие входные данные:
- Line 1: ID тикета
- Line 2: Number раз для обработки тикета
Например, если given:
TKT-001
3Ваш вывод должен быть таким:
Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolvedА если given:
TKT-500
5Ваш вывод должен быть таким:
Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolved
Ticket already resolved
Ticket already resolvedА если given:
ISSUE-42
1Ваш вывод должен быть таким:
Ticket opened, assigning to support teamОбратите внимание, как вызов Process() для одного и того же тикета каждый раз produces разные результаты: behavior тикета changes по мере его перехода между состояниями. После разрешения он остаётся разрешённым независимо от того, сколько раз вы его обрабатываете. Кажется, что объект тикета changes свой behavior, но на самом деле он делегирует выполнение разным объектам состояния!
Попробуйте сами
package main
import (
"fmt"
)
func main() {
// Прочитать ввод
var ticketID string
var numProcesses int
fmt.Scanln(&ticketID)
fmt.Scanln(&numProcesses)
// TODO: Создать новый тикет с заданным ID
// TODO: Обработать тикет указанное количество раз
// и вывести каждый результат на отдельной строке
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker PoolПотренируйтесь самостоятельно: Онлайн-компилятор Go