Обзор шаблона проектирования State и его реализации в Dart и Flutter

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

Оглавление

  • Что такое паттерн государственного проектирования?
  • Анализ
  • Реализация
  • Другие статьи из этой серии
  • Ваш вклад

Что такое паттерн государственного проектирования?

Состояние - это поведенческий шаблон проектирования, намерение которого в книге GoF описывается следующим образом:

Разрешить объекту изменять свое поведение при изменении его внутреннего состояния. Объект изменит свой класс.

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

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

Аналогичный подход можно применить к объектам в ООП. Чтобы просто реализовать конечный автомат в коде, нужно использовать несколько условных операторов в одном классе и выбрать соответствующее поведение в зависимости от текущего состояния объекта. Однако из-за введения новых состояний такой код становится очень трудно поддерживать, состояния чередуются друг с другом, класс привязан к определенному поведению, зависящему от состояния. Я даже не упомянул, насколько сложно тестировать такой код и реализованную внутри него бизнес-логику.

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

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

Анализ

Общая структура паттерна государственного проектирования выглядит так:

  • Контекст - поддерживает экземпляр подкласса ConcreteState, который определяет текущее состояние. Класс Context не знает никаких подробностей о ConcreteStates и взаимодействует с объектом состояния через интерфейс State. Кроме того, Context предоставляет метод установки для изменения текущего состояния классов ConcreteStates;
  • Состояние - определяет интерфейс, который инкапсулирует поведение и методы, зависящие от состояния. Эти методы должны иметь смысл для всех ConcreteStates, не должно быть определенных методов, которые никогда не будут вызываться из конкретной реализации интерфейса State;
  • ConcreteStates - каждый класс реализует поведение, связанное с состоянием контекста. Объекты состояния могут ссылаться на Контекст для получения любой необходимой информации из контекста или для выполнения перехода состояния путем замены объекта состояния, связанного с классом Context (через открытый метод установки);
  • Клиент - использует объект Context для ссылки на текущее состояние и инициирования его перехода, а также при необходимости может установить начальное состояние для класса Context. .

Стратегия против государства

Я думаю, вы обратили внимание, что структура шаблона проектирования Состояние действительно похожа на структуру Стратегии. Основное различие между этими шаблонами - в шаблоне State отдельные состояния могут знать друг друга, в то время как конкретные стратегии почти никогда не знают никаких деталей о других реализациях стратегии. По сути, это ключевое отличие меняет смысл шаблона и его использование в коде.

Применимость

Шаблон проектирования State следует использовать, когда у вас есть объект, который ведет себя по-разному в зависимости от своего текущего состояния, количество состояний огромно и код, зависящий от состояния, часто изменяется. Инкапсулируя каждое состояние и детали его реализации в отдельный класс, вы можете более легко добавлять новые состояния, а также можете изменять существующие состояния независимо друг от друга. Эта идея продвигает несколько ТВЕРДЫХ принципов, которые уже обсуждались в серии статей: Принцип единой ответственности (каждое состояние инкапсулировано в своем классе) и Принцип открытости / закрытости ( новые состояния могут быть введены без изменения существующих классов состояний). Практически любая логика, основанная на идее конечного автомата, может быть реализована с использованием шаблона проектирования State. Некоторые возможные примеры из реальной жизни: управление состоянием заказа в приложении электронной коммерции, отображение соответствующего цвета на светофоре в зависимости от текущего состояния, различное состояние статьи на Medium (черновик, отправлено, опубликовано…) и т. Д. В разделе, я представлю еще один актуальный вариант использования шаблона проектирования State - загрузка ресурсов из API.

Реализация

Следующая реализация (или, по крайней мере, стоящая за ней идея) может быть применена практически к каждому приложению Flutter, которое загружает ресурсы из любого внешнего источника, например API. Например, когда вы загружаете ресурсы асинхронно с помощью HTTP и вызываете REST API, обычно для завершения запроса требуется некоторое время. Как с этим справиться и не «заморозить» приложение на время выполнения запроса? Что делать, если во время этого запроса возникнет какая-то ошибка? Простой подход - использовать анимацию, экраны загрузки / ошибок и т. Д. Это может стать громоздким, если вам нужно реализовать одну и ту же логику для разных типов ресурсов. В этом может помочь шаблон проектирования State. Прежде всего, вы уточняете состояния, общие для всех ваших ресурсов:

  • Пусто - результатов нет;
  • Загрузка - выполняется запрос на загрузку ресурсов;
  • Загружено - ресурсы загружаются из внешнего источника;
  • Ошибка - произошла ошибка при загрузке ресурсов.

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

Давайте углубимся в детали реализации шаблона проектирования State и его пример во Flutter!

Диаграмма классов

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

IState определяет общий интерфейс для всех конкретных состояний:

  • nextState () - изменяет текущее состояние в объекте StateContext на следующее состояние;
  • render () - отображает пользовательский интерфейс определенного состояния.

NoResultsState, ErrorState, LoadingState и LoadedState - это конкретные реализации интерфейса IState. Каждое из состояний определяет свой репрезентативный компонент пользовательского интерфейса через метод render (), также использует определенное состояние (или состояния, если следующее состояние выбирается из нескольких возможных вариантов в зависимости от контекста) типа IState в nextState (), который будет изменен путем вызова метода nextState (). В дополнение к этому LoadedState содержит список имен, который вводится с помощью конструктора состояния, а LoadingState использует FakeApi для получения списка случайно сгенерированных имен.

StateContext сохраняет текущее состояние типа IState в частном свойстве currentState, определяет несколько методов:

  • setState () - изменяет текущее состояние;
  • nextState () - запускает метод nextState () для текущего состояния;
  • dispose () - безопасно закрывает поток stateStream.

Текущее состояние отображается в пользовательском интерфейсе с помощью потока outState.

Виджет StateExample содержит объект StateContext для отслеживания и запуска изменений состояния, а также использует NoResultsState в качестве начального состояния для примера.

IState

Интерфейс, определяющий методы, которые будут реализованы всеми конкретными классами состояний. Язык Dart не поддерживает интерфейс как тип класса, поэтому мы определяем интерфейс, создавая абстрактный класс и предоставляя заголовок метода (имя, тип возвращаемого значения, параметры) без реализации по умолчанию.

StateContext

Класс, который хранит текущее состояние в свойстве currentState и предоставляет его пользовательскому интерфейсу через поток outState. Контекст состояния также определяет метод nextState (), который используется пользовательским интерфейсом для запуска изменения состояния. Само текущее состояние изменяется / устанавливается с помощью метода setState (), предоставляя ему следующее состояние типа IState в качестве параметра.

Конкретные реализации интерфейса IState

ErrorState реализует конкретное состояние, которое используется, когда в API возникает необработанная ошибка и должен отображаться виджет ошибки.

LoadedState реализует конкретное состояние, которое используется, когда ресурсы загружаются из API без ошибок, а виджет результата должен отображаться на экране.

NoResultsState реализует конкретное состояние, которое используется, когда список ресурсов загружается из API без ошибок, но список пуст. Кроме того, это состояние изначально используется в виджете StateExample.

LoadingState реализует конкретное состояние, которое используется при загрузке ресурсов из FakeApi. Кроме того, на основе загруженного результата следующее состояние устанавливается в методе nextState ().

FakeApi

Поддельный API используется для случайного создания списка имен людей. Метод getNames () может возвращать список имен или случайным образом генерировать исключение (ошибку). Точно так же метод getRandomNames () случайным образом возвращает список имен или пустой список. Это поведение реализовано в демонстрационных целях, чтобы показать все возможные различные состояния в пользовательском интерфейсе.

Пример

Прежде всего, подготавливается файл разметки, который предоставляется как описание шаблона:

StateExample реализует пример виджета шаблона проектирования State. Он содержит StateContext, подписывается на поток outState текущего состояния и предоставляет соответствующий виджет пользовательского интерфейса, выполняя метод состояния render (). Текущее состояние можно изменить, запустив метод changeState () (нажав кнопку Загрузить имена в пользовательском интерфейсе).

Виджет StateExample знает только о классе начального состояния - NoResultsState, но не знает никаких подробностей о других возможных состояниях, поскольку их обработка определяется в StateContext класс. Это позволяет отделить бизнес-логику от репрезентативного кода, добавить новые состояния типа IState в приложение без внесения каких-либо изменений в компоненты пользовательского интерфейса.

Окончательный результат реализации паттерна проектирования State выглядит так:

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

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

Другие статьи из этой серии

Ваш вклад

👏 Нажмите кнопку хлопка ниже, чтобы выразить свою поддержку и побудить меня писать лучше!
💬 Оставьте отзыв на эту статью, поделившись своими мыслями, комментариями или пожеланиями относительно серии.
📢 Поделитесь этой статьей со своими друзья, коллеги в социальных сетях.
➕ Подписывайтесь на меня на Medium.
⭐ Пометьте репозиторий Github.