Поиск по сайту:

Реализация шаблона состояния в TypeScript


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

Шаблон проектирования — это шаблон, который решает часто возникающую проблему при проектировании программного обеспечения.

Шаблон состояния — это шаблон поведения, который позволяет объекту изменять свое поведение при изменении его внутреннего состояния.

Здесь вы узнаете, как использовать шаблон состояния в TypeScript.

Что такое государственный образец?

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

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

Для контекста: в интернет-магазине, если заказ клиента «доставлен», его нельзя «отменить», поскольку он уже «доставлен». «Доставлено» и «Отменено» — это конечные состояния заказа, и заказ будет вести себя по-разному в зависимости от его состояния.

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

Пример приложения на основе состояния

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

Вы можете визуализировать различные состояния и переходы приложения статьи с помощью диаграммы состояний ниже:

При реализации этого сценария в коде сначала необходимо объявить интерфейс для статьи:

interface ArticleInterface {
  pitch(): void;
  draft(): void;
  edit(): void;
  publish(): void;
}

Этот интерфейс будет иметь все возможные состояния приложения.

Далее создайте приложение, реализующее все методы интерфейса:

// Application
class Article implements ArticleInterface {
  constructor() {
    this.showCurrentState();
  }
 
  private showCurrentState(): void {
    //...
  }
 
  public pitch(): void {
    //...
  }
 
  public draft(): void {
    //...
  }
 
  public edit(): void {
    //...
  }
 
  public publish(): void {
    //...
  }
}

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

Обработка переходов между состояниями

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

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

Например:

abstract class ArticleState implements ArticleInterface {
  pitch(): ArticleState {
    throw new Error("Invalid Operation: Cannot perform task in current state");
  }
 
  draft(): ArticleState {
    throw new Error("Invalid Operation: Cannot perform task in current state");
  }
 
  edit(): ArticleState {
    throw new Error("Invalid Operation: Cannot perform task in current state");
  }
 
  publish(): ArticleState {
    throw new Error("Invalid Operation: Cannot perform task in current state");
  }
}

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

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

Например:

class PendingDraftState extends ArticleState {
  pitch(): ArticleState {
    return new DraftState();
  }
}

Метод pitch в приведенном выше классе инициализирует приложение, устанавливая текущее состояние в DraftState .

Затем переопределите остальные методы следующим образом:

class DraftState extends ArticleState {
  draft(): ArticleState {
    return new EditingState();
  }
}

Этот код переопределяет метод draft и возвращает экземпляр EditingState.

class EditingState extends ArticleState {
  edit(): ArticleState {
    return new PublishedState();
  }
}

Приведенный выше блок кода переопределяет метод edit и возвращает экземпляр PublishedState.

class PublishedState extends ArticleState {
  publish(): ArticleState {
    return new PendingDraftState();
  }
}

Приведенный выше блок кода переопределяет метод publish и возвращает приложение в исходное состояние. состояние простоя, PendingDraftState.

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

private state: ArticleState = new PendingDraftState();

Затем обновите метод showCurrentState, чтобы он выводил текущее значение состояния:

private showCurrentState(): void {
  console.log(this.state);
}

Метод showCurrentState записывает текущее состояние приложения на консоль.

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

Например, обновите метод презентации для своих приложений. в блок кода ниже:

public pitch(): void {
  this.state = this.state.pitch();
  this.showCurrentState();
}

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

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

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

Метод черновик:

public draft(): void {
  this.state = this.state.draft();
  this.showCurrentState();
}

Метод edit:

public edit(): void {
  this.state = this.state.edit();
  this.showCurrentState();
}

И опубликовать метод:

public publish(): void {
  this.state = this.state.publish();
  this.showCurrentState();
}

Использование готового приложения

Готовый класс приложения должен быть похож на блок кода ниже:

// Application
class Article implements ArticleInterface {
  private state: ArticleState = new PendingDraftState();
 
  constructor() {
    this.showCurrentState();
  }
 
  private showCurrentState(): void {
    console.log(this.state);
  }
 
  public pitch(): void {
    this.state = this.state.pitch();
    this.showCurrentState();
  }
 
  public draft(): void {
    this.state = this.state.draft();
    this.showCurrentState();
  }
 
  public edit(): void {
    this.state = this.state.edit();
    this.showCurrentState();
  }
 
  public publish(): void {
    this.state = this.state.publish();
    this.showCurrentState();
  }
}

Вы можете протестировать переходы состояний, вызывая методы в правильной последовательности. Например:

const docs = new Article(); // PendingDraftState: {}
 
docs.pitch(); // DraftState: {}
docs.draft(); // EditingState: {}
docs.edit(); // PublishedState: {}
docs.publish(); // PendingDraftState: {}

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

Если вы попытаетесь изменить состояние недопустимым способом, например, из состояния питча в состояние редактирования, приложение выдаст ошибку:

const docs = new Article(); // PendingDraftState: {}
docs.pitch() // DraftState: {}
docs.edit() // Invalid Operation: Cannot perform task in current state

Вам следует использовать этот шаблон только в следующих случаях:

  • Вы создаете объект, который ведет себя по-разному в зависимости от его текущего состояния.
  • Объект имеет множество состояний.
  • Поведение, зависящее от состояния, часто меняется.

Преимущества и компромиссы шаблона состояний

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

Статьи по данной тематике: