Паттерн Состояние
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 95 из 107.
Паттерн Состояние (State) позволяет объекту изменять свое поведение при изменении его внутреннего состояния, создавая впечатление, будто объект изменил свой класс. В то время как Шаблонный метод (Template Method) управляет шагами алгоритма, Состояние инкапсулирует поведение, зависящее от состояния, в отдельные объекты и делегирует выполнение текущему состоянию.
В 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"
}Один и тот же вызов метода дает разные результаты в зависимости от текущего состояния:
doc := &Document{state: DraftState{}}
fmt.Println(doc.Publish()) // Черновик отправлен на модерацию
fmt.Println(doc.Publish()) // Модерация одобрена, теперь опубликовано
fmt.Println(doc.Publish()) // Уже опубликованоСостояние (State) идеально подходит для объектов с четко выраженными режимами работы, такими как процессы обработки заказов, компоненты пользовательского интерфейса или обработчики соединений, где поведение полностью зависит от текущего состояния.
Задание
ЛегкоДавайте создадим систему тикетов поддержки, используя паттерн Состояние (State)! Вы создадите тикет поддержки, который проходит через различные стадии — от открытия до работы над ним и разрешения — при этом каждое состояние определяет, какие действия возможны и что произойдет дальше.
Вы организуете свой код в трех файлах:
state.go: Определите интерфейс состояния и конкретные состояния, представляющие каждый этап жизненного цикла тикета.Создайте интерфейс
TicketStateс методомHandle(t *Ticket) string, который обрабатывает тикет и потенциально переводит его в следующее состояние.Реализуйте три состояния:
OpenState— при обработке переводит тикет вInProgressStateи возвращает"Ticket opened, assigning to support team"InProgressState— при обработке переводит вResolvedStateи возвращает"Working on ticket, issue resolved"ResolvedState— при обработке остается в том же состоянии и возвращает"Ticket already resolved"
ticket.go: Создайте структуру тикета, которая хранит текущее состояние и делегирует ему поведение.Создайте структуру
Ticketс полемID(string) и полемstate(TicketState). Добавьте следующие методы:SetState(s TicketState)для изменения текущего состояния тикетаProcess() string, который делегирует выполнение методу Handle текущего состояния
Создайте конструктор
NewTicket(id string) *Ticket, который возвращает тикет, начинающийся в состоянииOpenState.main.go: Продемонстрируйте, как одно и то же действие дает разные результаты в зависимости от текущего состояния тикета.Считайте ID тикета и количество раз, которое необходимо обработать тикет. Создайте новый тикет с этим ID, затем вызовите
Process()указанное количество раз, выводя каждый результат на отдельной строке.
Будут предоставлены следующие входные данные:
- Строка 1: ID тикета
- Строка 2: Количество раз для обработки тикета
Например, при вводе:
TKT-001
3Ваш вывод должен быть:
Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolvedИ при вводе:
TKT-500
5Ваш вывод должен быть:
Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolved
Ticket already resolved
Ticket already resolvedИ при вводе:
ISSUE-42
1Ваш вывод должен быть:
Ticket opened, assigning to support teamОбратите внимание, как вызов Process() для одного и того же тикета каждый раз дает разные результаты — поведение тикета меняется по мере его перехода между состояниями. После разрешения он остается разрешенным, сколько бы раз вы его ни обрабатывали. Кажется, что объект тикета меняет свое поведение, но на самом деле он делегирует задачи разным объектам состояний!
Шпаргалка
Паттерн Состояние (State) позволяет объекту изменять свое поведение при изменении его внутреннего состояния. Объект делегирует поведение отдельным объектам состояний, представляющим различные состояния.
Определите интерфейс состояния:
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"
}Каждое состояние определяет, что происходит, и переходит к следующему состоянию:
doc := &Document{state: DraftState{}}
fmt.Println(doc.Publish()) // Черновик отправлен на модерацию
fmt.Println(doc.Publish()) // Модерация одобрена, теперь опубликовано
fmt.Println(doc.Publish()) // Уже опубликованоПаттерн Состояние идеально подходит для объектов с четко выраженными режимами работы, где поведение полностью зависит от текущего состояния.
Попробуйте сами
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