В объектно-ориентированном программировании объекты определяются своим внутренним состоянием (свойствами) и поведением (методами).

Иногда внутреннее состояние объекта определяет его поведение. Методы объекта могут выполнять разные строки кода в зависимости от значения одного (или нескольких) его свойств с помощью операторов if или switch case. В этом случае шаблон состояния может быть очень полезен, чтобы сделать ваш код более объектно-ориентированным.

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

Мы можем смоделировать этот пример с помощью следующей диаграммы конечного автомата:

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

Класс обращения в службу поддержки может иметь свойство состояния, отслеживающее его текущий статус, и три метода: «разрешить», «повторно открыть» и «закрыть». Вот диаграмма классов для класса SupportTicket.

А вот как может выглядеть класс SupportTicket (код на Java):

public class SupportTicket {
 
 /* An enum holding the values PENDING, RESOLVED and CLOSED */
 private TicketStatus status;
 
  public SupportTicket() {
  /* When a ticket is initially created, its status is "pending" */
  this.status = TicketStatus.PENDING;
  }
   
  public void resolve() {
  
   if(status == TicketStatus.PENDING) {
   /* "resolving a pending ticket" logic */
   }
  
   if(status == TicketStatus.RESOLVED) {
   /* "cannot resolve a resolved ticket" logic */
   }
  
   if(status == TicketStatus.CLOSED) {
   /* "cannot resolve a closed ticket" logic */
   }
  }
 
  public void reopen() {
  
   if(status == TicketStatus.PENDING) {
   /* "cannot reopen a pending ticket" logic */
   }
  
   if(status == TicketStatus.RESOLVED) {
   /* "reopening a resolved ticket" logic */
   }
  
   if(status == TicketStatus.CLOSED) {
   /* "cannot reopen a closed ticket" logic */
   }
  
  }

  public void close() {
  
   if(status == TicketStatus.PENDING) {
   /* "cannot close a pending ticket" logic */
   }
  
   if(status == TicketStatus.RESOLVED) {
   /* "closing a resolved ticket" logic */
   }
  
   if(status == TicketStatus.CLOSED) {
   /* "cannot close a closed ticket" logic */
   }
  }
}

Как видите, поскольку SupportTicketметоды зависят от status (состояния), в котором в данный момент находится заявка, мы жестко запрограммировали все возможные действия внутри методов и используем условные операторы, чтобы проверить, какое действие должно быть выполнено на основе текущего состояния. состояние билета.

Инкапсулируйте то, что меняется

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

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

Для каждого конкретного класса состояния каждый конкретный метод будет содержать только код, который будет выполняться для этого конкретного состояния.

Например, для ResolvedState у нас может быть метод resolve(), который выдает исключение, поскольку мы не можем разрешить уже разрешенную заявку, метод reopen() может обрабатывать логику повторного открытия разрешенной заявки, и, наконец, метод close() может обрабатывать закрытие разрешенной заявки. . Вот как выглядит класс ResolvedState:

public class ResolvedState implements SupportTicketState {
  private SupportTicket ticket;
 
  public ResolvedState(SupportTicket ticket){
   this.ticket = ticket;
  }
  public void resolve() {
   /* "cannot resolve a resolved ticket" logic */
  }
  public void reopen() {
   /* "reopening a resolved ticket" logic */
  }
  public void close() {
   /* "closing a resolved ticket" logic */
  }
}

Я также добавил свойство ticket, которое ссылается на билет, которому принадлежит состояние. Почему? Потому что мы можем захотеть вызывать методы объекта ticket из объекта состояния.

Делегация

Теперь, когда мы успешно инкапсулировали состояние вместе с соответствующим поведением, теперь мы будем использовать другой важный принцип OOD — «Делегирование».

SupportTicket будет содержать ссылку на объект состояния, представляющий его текущее состояние, и любые вызовы методов SupportTicket будут делегированы объекту состояния для выполнения соответствующей логики через SupportTicketState интерфейс( Программируйте интерфейсы, а не реализации!.

Вот как выглядит класс SupportTicket:

public class SupportTicket {
 
 private SupportTicketState currentState;
 public SupportTicket() {
    currentState = new PendingState(this);
 }
 
 public void resolve() {
  currentState.resolve();
 }
 
 public void reopen() {
  currentState.reopen();
 }
 
 public void close() {
  currentState.close();
 }
 
 public void updateState(SupportTicketState newState) {
  currentState = newState;
 }
}

В конструкторе класса мы инициализируем состояние тикета значением PendingState и передаем объекту состояния ссылку на текущий тикет через ключевое слово this. Каждый метод SupportTicket делегирует объект состояния через свойство currentState для выполнения правильной логики. Наконец, я добавил метод updateState(), который позволит изменить состояние тикета. Это полезно, так как мы можем обновлять состояние заявки непосредственно из объектов состояния с помощью таких вызовов, как:

ticket.updateState(new ReopenedState(ticket));

Это полная диаграмма классов UML для нашего примера системы тикетов:

Основная идея заключается в том, чтобы просто извлечь логику, связанную с состоянием, из класса заявки (контекст) в отдельные классы состояния. А затем делегируйте класс контекста «текущему» объекту состояния для задач, связанных с состоянием. Кроме того, объекты состояния могут содержать ссылку на объект контекста для связи с ним.

Теперь наш объект билета может динамически изменять свое поведение в зависимости от текущего состояния, на которое он ссылается. Нам больше не нужно загрязнять наш класс контекста жестко закодированной логикой о каждом возможном состоянии внутри больших условных операторов. Мы также можем приспособиться к новым состояниям и их поведению, например, к состоянию «Архив», добавив конкретный класс ArchivedState, реализующий интерфейс состояния.

Наконец, вот диаграмма классов UML для шаблона проектирования State:

Спасибо за чтение!