Menu
Coddy logo textTech

Паттерн Состояние

Часть раздела Объектно-ориентированное программирование путешествия по 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 состояния.

challenge icon

Задание

Легко

Давайте создадим систему поддержки тикетов, используя паттерн State! Вы создадите тикет поддержки, который проходит через разные этапы: от открытия до обработки и разрешения, причём каждое состояние определяет, какие действия возможны и что произойдёт дальше.

Вы организуете код в трёх файлах:

  • state.go: Define интерфейс состояния и конкретные состояния, представляющие каждый этап жизненного цикла тикета.

    Создайте интерфейс TicketState с методом Handle(t *Ticket) string, который обрабатывает тикет и при необходимости переводит его в следующее состояние.

    Implement три состояния:

    • OpenState: при обработке переводит тикет в InProgressState и returns Ticket opened, assigning to support team
    • InProgressState: при обработке переводит тикет в ResolvedState и returns Working on ticket, issue resolved
    • ResolvedState: при обработке остаётся в том же состоянии и returns Ticket 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: Обработать тикет указанное количество раз
	// и вывести каждый результат на отдельной строке
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор Go