Что такое шаблон проектирования State, как он работает и когда его следует применять?

В оригинальной книге Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения описаны 23 классических шаблона проектирования. Эти шаблоны обеспечивают решения конкретных проблем, часто повторяющихся при разработке программного обеспечения.

В этой статье я собираюсь описать, как работает шаблон State и когда его следует применять.

Состояние: основная идея

Википедия дает нам следующее определение:

Шаблон состояния — это поведенческий шаблон проектирования программного обеспечения, который позволяет объекту изменять свое поведение при изменении его внутреннего состояния. Этот паттерн близок к концепции конечных автоматов. Паттерн состояния можно интерпретировать как «паттерн стратегии, который способен переключать стратегию посредством вызова методов, определенных в интерфейсе паттерна». — Википедия

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

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

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

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

Это классы, которые составляют этот шаблон:

  • Context — интерфейс, представляющий интерес для клиентов. Этот класс поддерживает экземпляр State, который определяет текущее состояние.
  • State определяет интерфейс для инкапсуляции поведения, связанного с конкретным состоянием Context.
  • ConcreteStateA и ConcreteStateB — это подклассы, которые реализуют поведение, связанное с состоянием контекста.

Шаблон состояния: когда его использовать

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

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

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

Шаблон состояния: преимущества и недостатки

Шаблон состояния имеет ряд преимуществ, которые можно обобщить в следующих пунктах:

  • Код более удобен в сопровождении, поскольку он менее связан между объектом и его состояниями. Объекту (context) нужно только знать, что существует State, который будет обрабатываться с использованием интерфейса State.
  • Чистый код. Принцип открытия-закрытия (OCP) гарантируется, поскольку новые состояния могут быть введены без нарушения существующего кода в цепочке.
  • Более чистый код. Принцип единой ответственности (SRP) соблюдается, поскольку ответственность каждого штата передается его ConcreteState классу, а не бизнес-логике в Context.

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

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

Узнайте больше здесь:



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

Далее мы проиллюстрируем два примера шаблона State:

  • Базовая структура шаблона State. В этом примере мы собираемся перевести теоретическую диаграмму UML в код TypeScript, чтобы идентифицировать каждый из классов, участвующих в шаблоне.
  • Изменение состояния аниме-персонажа, такого как злодей Фриза из Dragon Ball Z, который может изменять свое физическое состояние с помощью различных трансформаций. В нашем случае у нас будет до 5 различных изменений состояния. Имея в виду, что между состояниями можно продвинуться или вернуться на один уровень трансформации. Однако мы должны были сгенерировать конечный автомат, который нам нужен в этом процессе трансформации.

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

Пример 1: Базовая структура шаблона состояния

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

На этой диаграмме мы видим, что у нас есть класс Context, который соответствует объекту, который имеет различное поведение в зависимости от состояния, в котором он находится. Эти состояния можно смоделировать с помощью класса Enum, где у нас будут разные возможные состояния, как Например, у нас будет два разных состояния: StateA и StateB.

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

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

import { State } from  "./state.enum";

export class Context {

    private state = State.stateA;

    request(operation: string) {  
        switch (this.state){
            case State.stateA:
                if(operation === 'request1'){
                    console.log('ConcreteStateA handles request1.'); //request1()
                    console.log('ConcreteStateA wants to change the state of the context.');
                    this.state = State.stateB; // Transition to another state
                    console.log(`Context: Transition to concrete-state-B.`);
                }else {
                    console.log('ConcreteStateA handles request2.'); // request2()
                }
                break
            case State.stateB:
                if(operation === 'request1'){
                    console.log('ConcreteStateB handles request1.'); //request1()
                }else{
                    console.log('ConcreteStateB handles request2.'); //request2()
                    console.log('ConcreteStateB wants to change the state of the context.');
                    this.state = State.stateA; // Transition to another state
                    console.log(`Context: Transition to concrete-state-A.`);
                }
            default: // Do nothing.
                break
        }
    }
}

В этом коде мы можем видеть в методе request, как реализована управляющая структура switch, которая связывает код с Context состояниями. Обратите внимание, что изменение состояния выполняется в самом этом методе, когда мы меняем состояние, мы меняем будущее поведение этого метода, поскольку будет осуществляться доступ к коду, соответствующему новому состоянию.

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

import { Context } from "./context";

const context = new Context();
context.request('request1');
context.request('request2');t

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

Теперь мы сосредоточимся на решении этой проблемы, применяя шаблон State. Начнем с рассмотрения диаграммы классов.

Класс Context теперь связан по составу с новым объектом, который является State контекста. У Context по-прежнему будут методы, связанные с функциональностью, которую он выполнял ранее, например, request1 и request2. Кроме того, добавлен новый метод под названием transitionTo, который будет переходить между различными состояниями. То есть переход из одного состояния в другое будет осуществляться через метод, инкапсулирующий эту логику.

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

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

После того, как мы увидели диаграмму классов UML, мы увидим, как будет выглядеть реализация этого шаблона проектирования.

import { State } from "./state";

export class Context {
    private state: State;

    constructor(state: State) {
        this.transitionTo(state);
    }

    public transitionTo(state: State): void {
        console.log(`Context: Transition to ${state.constructor.name}.`);
        this.state = state;
        this.state.setContext(this);
    }

    public request1(): void {
        this.state.handle1();
    }

    public request2(): void {
        this.state.handle2();
    }
}

Начнем с класса Context, и первое, что мы заметим, это то, что атрибут состояния является объектом класса State, а не класса Enum. Этот State класс является abstract, так что ответственность может быть делегирована конкретным штатам. Если мы посмотрим на методы request1 и request2, то увидим, что они используют объект state и делегируют ответственность этому классу.

С другой стороны, у нас есть реализация метода transitionTo, которую мы собираемся использовать просто для изменения state, в котором находится Context, и, как мы уже сказали, чтобы нам было проще не распространять контекст через дескрипторы объекта состояния, мы собираемся вызвать метод setContext, чтобы назначить контекст state, сделав связь между state и context постоянной, а не через ссылки между методами.

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

import { Context } from "./context";

export abstract class State {
    protected context: Context;

    public abstract handle1(): void;
    public abstract handle2(): void;
}

Конкретные состояния — это те, которые инкапсулируют бизнес-логику, соответствующую каждому из состояний, когда объект контекста находится в них. Если мы увидим код, связанный с этими классами, мы увидим, как классы ConcreteStateA и ConcreteStateB реализуют методы handle1 и handle2, соответствующие интерфейсу State, и как в нашем конкретном случае мы переходим от StateA к StateB, когда handle1 выполняется, когда контекст находится в StateA . Принимая во внимание, что мы переходим от StateB к StateA, когда handle2 выполняется, когда context находится в StateB.

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

import { ConcreteStateB } from "./concrete-state-B";
import { State } from "./state";

export class ConcreteStateA extends State {
    public handle1(): void {
        console.log('ConcreteStateA handles request1.');
        console.log('ConcreteStateA wants to change the state of the context.');
        this.context.transitionTo(new ConcreteStateB());
    }

    public handle2(): void {
        console.log('ConcreteStateA handles request2.');
    }
}

/*****/
import { ConcreteStateA } from "./concrete-state-A";
import { State } from "./state";

export class ConcreteStateB extends State {
    public handle1(): void {
        console.log('ConcreteStateB handles request1.');
    }

    public handle2(): void {
        console.log('ConcreteStateB handles request2.');
        console.log('ConcreteStateB wants to change the state of the context.');
        this.context.transitionTo(new ConcreteStateA());
    }
}

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

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

import { ConcreteStateA } from "./concrete-state-A";
import { Context } from "./context";

const context = new Context(new ConcreteStateA()); // Initial State
context.request1();
context.request2();

Пример 2: Dragon Ball Z: Трансформация Фризы

В этом примере мы собираемся смоделировать видеоигру «Жемчуг дракона», в которой у нас будет любимый злодей, такой как Фриза, который, как мы знаем, может претерпевать различные трансформации во время битвы.

То есть у нас будет персонаж по имени Freeza, у которого есть разные состояния, которые являются трансформациями, в которых они находятся, это состояние может меняться, когда Freeza побеждает или проигрывает в битве. В нашем случае Freeza начнет с состояния Transformation1 и сможет изменить свое состояние на Transformation2. В этот момент переход между состояниями может заключаться в возврате в Transformation1 в случае победы в бою или переходе в состояние Transformation3. В состоянии преобразования номер 3 произойдет то же самое, и мы сможем перейти к Transformation2 или Transformation4. Наконец, когда мы находимся в Transformation4, мы можем вернуться в Transformation3 или перейти в состояние Golden Freeza, которое будет последним возможным состоянием, из которого мы можем вернуться только назад.

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

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

Различные STATES были смоделированы с Enum. То есть у нас будет класс enum, где описаны разные возможные состояния, через которые может пройти Freeza. С другой стороны, у нас есть класс Freeza, который моделирует нашего персонажа, этот класс состоит из различных атрибутов, таких как power, energy и state. И power, и energy помогают нам определить, когда менять состояние, и являются частью игры. Методы, с помощью которых Freeza состоит:

  • Constructor, который получает начальное состояние Freeza.
  • isAlive, который вернет значение boolean, чтобы узнать, жив ли еще Freeza.
  • transitionTo, attack и defend, логика которых связана с каждым из возможных states. Метод transitionTo изменяет значения energy и power в зависимости от state, в котором находится Freeza, точно так же attack и defend определяют power, с помощью которого Freeza атакует и защищает себя, что определяется state, в котором находится.

Мы оставили в качестве примечания на диаграмме UML, что эти методы разработаны на основе структуры switch-case, в которой каждый из случаев этой структуры реализует желаемое поведение.

Конечно, здесь мы можем видеть, как с тех пор нарушаются как принцип единой ответственности (SRP), так и принцип открытости/закрытости (OCP). Во-первых, SRP нарушается, потому что в одном и том же классе или методе смоделировано разное поведение. Во-вторых, OCP нарушается, потому что, если бы мы захотели ввести новое преобразование Freeza, нам пришлось бы изменить этот класс, а поскольку мы хотим ввести новые функции, мы вынуждены изменить этот класс.

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

Прежде всего, мы видим перечисление State, которое, как мы уже говорили, определяет только различные состояния, через которые может переходить Freeza.

export enum State {
    TRANSFORMATION1 = 'transformation1',
    TRANSFORMATION2 = 'transformation2',
    TRANSFORMATION3 = 'transformation3',
    TRANSFORMATION4 = 'transformation4',
    GOLDEN_FREEZA = 'golden_freeza',
}

Следующим шагом будет рассмотрение класса Freeza, который имеет атрибуты, о которых мы уже упоминали выше, и здесь мало что можно добавить. Если мы посмотрим на конструктор, то увидим, что при получении состояния, в котором находится Freeza, мы переходим в это состояние. Другой метод, который мы внедрили, — это isAlive, который проверяет, больше ли энергия Freeza больше нуля.

С другой стороны, когда мы смотрим на метод transitionTo, именно здесь можно обнаружить запахи кода или как нарушаются два принципа SOLID, поскольку мы видим, что этот метод изменяет значения energy и power в соответствии с состоянием, в котором находится объект. находится, и даже если мы вынесем эту логику во вспомогательные функции или методы, логика каждого из состояний все равно будет там. То есть у нас будет пять разных состояний, смоделированных одним и тем же методом.

Ситуация усугубляется, если мы видим код, связанный с методом attack. У нас снова есть переключатель для определения бизнес-логики в соответствии с состоянием Freeza’s, но, как мы видим, хотя эта бизнес-логика проста, поскольку они представляют собой простые математические расчеты, определяющие силу, с которой он атакует врага, и энергию, которая восстанавливается в каждый ваш ход.

Наконец, так же, как и attack, определяется метод defend, который снова будет иметь бизнес-логику, основанную на состоянии Freeza, но в этой бизнес-логике мы видим, что у нас есть переход между состояниями. Например, в состоянии, связанном с Transformation2, если значение energy для Freeza превышает 20, оно переходит в Transformation3, а с другой стороны, если energy равно 5, оно переходит в Transformation1.

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

import { State } from "./state.enum";

export class Freeza {
   
    private power: number;
    private energy: number;
    private state: State;

    constructor(state: State) {
        this.transitionTo(state);
    }

    isAlive(): boolean {
        return this.energy > 0;
    }
    
    public transitionTo(state: State): void {
        console.log('-----------------------------')
        console.log(`Freeze: Transition to ${state}.`);
        console.log('-----------------------------')
        this.state = state;
        switch(state) {
            case State.TRANSFORMATION1:
                this.power = 530000;
                this.energy = 5;
            break;
            case State.TRANSFORMATION2: 
                this.power = 106000;
                this.energy = 10;
            break;
            case State.TRANSFORMATION3: 
                this.power = 212000;
                this.energy = 15;
            break;
            case State.TRANSFORMATION4:
                this.power = 106000;
                this.energy = 20;
            break;
            case State.GOLDEN_FREEZA:
                this.power = 212000;
                this.energy = 30;
            break;
        }
    }


    public attack(): void {
        let attackToEnemy, restoreEnergy;
        switch(this.state){
            case State.TRANSFORMATION1: 
                attackToEnemy = Math.round(this.power * (Math.random()/8));
                restoreEnergy = Math.round(Math.random());
                this.energy = this.energy + restoreEnergy;
                console.log('Freeza attack in the state form 1 -->', attackToEnemy);
                console.log(`Freese restore energy ${restoreEnergy} and his energy is ${this.energy}\n`);
                break;
            case State.TRANSFORMATION2: 
                attackToEnemy = Math.round(this.power * (Math.random()/7));   
                restoreEnergy = Math.round(Math.random() * 2);
                this.energy = this.energy + restoreEnergy;
        
                console.log('Freeza attack in the state form 2 -->', attackToEnemy);
                console.log(`Freese restore energy ${restoreEnergy} and his energy is ${this.energy}\n`);
                break;       
            case State.TRANSFORMATION3: 
                attackToEnemy = Math.round(this.power * (Math.random()/6));   
                restoreEnergy = Math.round(Math.random() * 3);
                this.energy = this.energy + restoreEnergy;
        
                console.log('Freeza attack in the state form 3 -->', attackToEnemy);
                console.log(`Freese restore energy ${restoreEnergy} and his energy is ${this.energy}\n`);
    
            break;
            case State.TRANSFORMATION4:
                attackToEnemy = Math.round(this.power * (Math.random()/5));   
                restoreEnergy = Math.round(Math.random() * 4);
                this.energy = this.energy + restoreEnergy;
        
                console.log('Freeza attack in the state form 4 -->', attackToEnemy);
                console.log(`Freese restore energy ${restoreEnergy} and his energy is ${this.energy}\n`);                
            break;
            case State.GOLDEN_FREEZA: 
                attackToEnemy = Math.round(this.power * (Math.random()/4));   
                restoreEnergy = Math.round(Math.random() * 5);
                this.energy = this.energy + restoreEnergy;

                console.log('Freeza attack in the state Golden Freeza -->', attackToEnemy);
                console.log(`Freese restore energy ${restoreEnergy} and his energy is ${this.energy}\n`);
            break; 
        }
    }
    
    public defend(attack: number): void {
        let attackFromEnemy;

        switch(this.state){
            case State.TRANSFORMATION1: 
                attackFromEnemy = Math.round(attack * (Math.random()));
                this.energy = this.energy - attackFromEnemy;

                console.log('Freeza defend in form 1');
                console.log(`Freeza received an attack of ${attackFromEnemy} and his energy is ${this.energy}\n`);
                
            break;

            case State.TRANSFORMATION2: 
                attackFromEnemy = Math.round(attack * (Math.random()));
                this.energy = this.energy - attackFromEnemy;
                
                console.log('Freeza defend in form 2');
                console.log(`Freeza received an attack of ${attackFromEnemy} and his energy is ${this.energy}\n`);
        
                if(this.energy < 5){
                    this.transitionTo(State.TRANSFORMATION1);
                }
                if(this.energy > 20){
                    this.transitionTo(State.TRANSFORMATION3);
                }
            break;
            case State.TRANSFORMATION3: /* more code*/ break;
            case State.TRANSFORMATION4: /* more code*/ break;
            case State.GOLDEN_FREEZA: /* more code*/ break;

        }
    }
}

Наконец, мы собираемся закончить, показав код, связанный с клиентом. Если вы заметили, что у нас есть только один цикл, пока Freeza жив, Freeza атакует, ждет секунду, затем Freeza защищается и так далее, пока Freeza не перестанет существовать.

import { Freeza } from "./freeza";
import { State } from "./state.enum";

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const freeza = new Freeza(State.TRANSFORMATION1); // Initial State

(async () => {
  while(freeza.isAlive()){
    freeza.attack();
    await sleep(1000);
    freeza.defend(10);
    await sleep(1000);
  } 
})();

Пример 2: Dragon Ball Z: Freeza Transformation (шаблон состояния)

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

Итак, первое, что мы делаем, — это смотрим, как будет выглядеть диаграмма классов UML нашего проекта, применяя шаблон состояния.

Начнем с того, что класс Freeza имеет те же методы, что и в решении без применения шаблона. Однако мы видим, что теперь у нас есть атрибут State, который, в отличие от предыдущего решения, который был enum, теперь будет абстрактным классом, представляющим состояние Freeza в данный момент. Очень важно, вместо класса Freeza, атрибуты power и energy были делегированы классу State, потому что эти два атрибута изменяются в зависимости от состояния, в котором находится Freeza. Если бы какой-либо из этих атрибутов или другой атрибут не зависел от состояния преобразования Freeza, он был бы в классе Freeza.

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

Таким образом, мы видим, что у нас есть атрибуты power и energy, определенные как абстрактные, потому что каждое из конкретных состояний будет изменять их в зависимости от состояния, в котором находится Freeza; и здесь, для простоты, у нас есть ссылка на Freeza, чтобы иметь возможность вызывать метод transitionTo в разных конкретных состояниях для перехода между состояниями, в которых можно найти Freeza. В этом классе определены абстрактные методы attack и defend, которые будут реализованы в каждом из конкретных состояний.

После определения абстрактного класса State мы должны определить каждое из конкретных состояний, которыми будут пять классов от Transformation1 до Transformation4 и преобразование Golden Freeza. Обратите внимание, что мы установили отношения перехода между различными состояниями, и очень важно отметить, что состояния известны друг другу, то есть состояния, в которые можно перейти, известны из исходного состояния.

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

Прежде чем оставить диаграмму классов UML, важно отметить, что если бы у нас было новое преобразование Freeza, нам нужно было бы только реализовать новое состояние без необходимости изменять приложение. Поэтому мы будем уважать OCP, поскольку мы можем расширить программное обеспечение, закрыв его и защитив как основу. Кроме того, SRP соблюдается еще и потому, что мы распределили ответственность каждого из штатов на определенный класс. Конечно, наше программное обеспечение сейчас более целостное.

А теперь давайте перейдем к нашей реализации.

import { State } from "./state";

export class Freeza {
   
    private state: State;

    constructor(state: State) {
        this.transitionTo(state);
        this.state.setFreeza(this);
    }

    isAlive(): boolean {
        return this.state.getEnergy() > 0;
    }
    
    public transitionTo(state: State): void {
        console.log('-----------------------------')
        console.log(`Freeze: Transition to ${state.constructor.name}.`);
        console.log('-----------------------------')
        this.state = state;
        this.state.setFreeza(this);
    }

    public attack(): void {
        this.state.attack();
    }
    public defend(value: number): void {
        this.state.defend(value);
    }
}

Обратите внимание, что класс Freeza теперь довольно прост, есть атрибут State, в котором делегирована ответственность в зависимости от состояния, в котором находится Freeza. Обратите внимание, что методы attack и defend вызывают только соответствующий метод класса abstract, который будет использовать один класс или другой в зависимости от state, в котором находится Freeza. Метод transitionTo присваивает состояние, в котором находится Freeza.

Теперь начинаем реализовывать состояния Freeza, в первую очередь показано состояние State abstract class.

import { Freeza } from "./freeza";

export abstract class State {
    abstract power: number;
    abstract energy: number;
    protected freeza: Freeza;

    public setFreeza(freeza: Freeza) {
        this.freeza = freeza;
    }
    public getEnergy() {
        return this.energy;
    }

    public abstract attack(): void;
    public abstract defend(value: number): void 
}

В этом классе мы просто определяем атрибуты power и energy как abstract, а методы attack и defend также как abstract, поскольку именно они будут реализованы в каждом конкретном состоянии. Кроме того, у нас есть ссылка на объект Freeza для возможности перехода между состояниями.

А теперь нужно было бы увидеть реализацию конкретных состояний, мы определили пять разных состояний, которые являются разными преобразованиями Freeza, логика очень проста и предназначена только для демонстрационных целей. Все эти состояния реализуют методы attack и defend. В методах attack мы рассчитываем, как Frieza будет attack и сколько energy будет восстановлено, мы просто модифицируем эти расчеты.

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

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

import { State } from "../state";
import { Transformation2 } from "./transformation2";

export class Transformation1 extends State {
    power = 530000;
    energy = 5;

    public attack(): void {
        const attackToEnemy = Math.round(this.power * (Math.random()/8));
        const restoreEnergy = Math.round(Math.random());
        this.energy = this.energy + restoreEnergy;
        console.log('Freeza attack in the state form 1 -->', attackToEnemy);
        console.log(`Freese restore energy ${restoreEnergy} and his energy is ${this.energy}\n`);
    }

    public defend(attack: number): void {
        const attackFromEnemy = Math.round(attack * (Math.random()/7));
        this.energy = this.energy - attackFromEnemy;

        console.log('Freeza defend in form 1');
        console.log(`Freeza received an attack of ${attackFromEnemy} and his energy is ${this.energy}\n`);
  
        if(this.energy < 2){
            this.freeza.transitionTo(new Transformation2());
        }
    }
}
import { State } from "../state";
import { Transformation1 } from "./transformation1";
import { Transformation3 } from "./transformation3";

export class Transformation2 extends State {
    power = 106000;
    energy = 10;
    
    public attack(): void {
        const attackToEnemy = Math.round(this.power * (Math.random()/7));   
        const restoreEnergy = Math.round(Math.random() * 2);
        this.energy = this.energy + restoreEnergy;

        console.log('Freeza attack in the state form 2 -->', attackToEnemy);
        console.log(`Freese restore energy ${restoreEnergy} and his energy is ${this.energy}\n`);
    }

    public defend(attack: number) {
        const attackFromEnemy = Math.round(attack * (Math.random()/6));
        this.energy = this.energy - attackFromEnemy;
        
        console.log('Freeza defend in form 2');
        console.log(`Freeza received an attack of ${attackFromEnemy} and his energy is ${this.energy}\n`);

        if(this.energy < 5){
            this.freeza.transitionTo(new Transformation3());
        }
        if(this.energy > 20){
            this.freeza.transitionTo(new Transformation1());
        }
    }
}
import { State } from "../state";
import { Transformation2 } from "./transformation2";
import { Transformation4 } from "./transformation4";

export class Transformation3 extends State {
    power = 212000;
    energy = 15;
    
    public attack() {
        const attackToEnemy = Math.round(this.power * (Math.random()/6));   
        const restoreEnergy = Math.round(Math.random() * 3);
        this.energy = this.energy + restoreEnergy;

        console.log('Freeza attack in the state form 3 -->', attackToEnemy);
        console.log(`Freese restore energy ${restoreEnergy} and his energy is ${this.energy}\n`);
    }

    public defend(attack: number) {
        const attackFromEnemy = Math.round(attack * (Math.random()/5));
        this.energy = this.energy - attackFromEnemy;
        
        console.log('Freeza defend in form 3');
        console.log(`Freeza received an attack of ${attackFromEnemy} and his energy is ${this.energy}\n`);

        if(this.energy < 5){
            this.freeza.transitionTo(new Transformation4());
        }
        if(this.energy > 25){
            this.freeza.transitionTo(new Transformation2());
        }
    }
}
import { GoldenFreeza } from "./golden-freeza";
import { State } from "../state";
import { Transformation3 } from "./transformation3";

export class Transformation4 extends State {
    power = 406000;
    energy = 20;
    
    public attack() {
        const attackToEnemy = Math.round(this.power * (Math.random()/5));   
        const restoreEnergy = Math.round(Math.random() * 4);
        this.energy = this.energy + restoreEnergy;

        console.log('Freeza attack in the state form 4 -->', attackToEnemy);
        console.log(`Freese restore energy ${restoreEnergy} and his energy is ${this.energy}\n`);
    }

    public defend(attack: number) {
        const attackFromEnemy = Math.round(attack * (Math.random()/6));
        this.energy = this.energy - attackFromEnemy;
        
        console.log('Freeza defend in form 4');
        console.log(`Freeza received an attack of ${attackFromEnemy} and his energy is ${this.energy}\n`);

        if(this.energy < 5){
            this.freeza.transitionTo(new GoldenFreeza());
        }
        if(this.energy > 25){
            this.freeza.transitionTo(new Transformation3());
        }
    }
}
import { State } from "../state";
import { Transformation4 } from "./transformation4";

export class GoldenFreeza extends State {
    power = 812000;
    energy = 30;
    
    public attack() {
        const powerAttack = Math.round(this.power * (Math.random()/4));   
        const restoreEnergy = Math.round(Math.random() * 5);
        this.energy = this.energy + restoreEnergy;

        console.log('Freeza attack in the state Golden Freeza -->', powerAttack);
        console.log(`Freese restore energy ${restoreEnergy} and his energy is ${this.energy}\n`);
    }

    public defend(attack: number) {
        const attackFromEnemy = Math.round(attack * (Math.random()/5));
        this.energy = this.energy - attackFromEnemy;
        
        console.log('Freeza defend in Golden Freeza');
        console.log(`Freeza received an attack of ${attackFromEnemy} and his energy is ${this.energy}\n`);

        if(this.energy > 50){
            this.freeza.transitionTo(new Transformation4());
        }
    }
}

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

import { Freeza } from "./freeza";
import { Transformation1 } from "./states/transformation1";

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const freeza = new Freeza(new Transformation1()); // Initial State

(async () => {
  while(freeza.isAlive()){
    freeza.attack();
    await sleep(1000);
    freeza.defend(10);
    await sleep(1000);
  } 
})();

Наконец, я создал несколько npm scripts, через которые может выполняться код, представленный в этой статье:

npm run example1-problem
npm run example1-state-solution-1

npm run example2-problem
npm run example2-state-solution-1

Полный код см. в этом репозитории GitHub.

Заключение

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

Самое важное в этом паттерне — не его конкретная реализация, а способность распознать проблему, которую этот паттерн может решить, и когда его можно применить. Конкретная реализация не так важна, поскольку она зависит от используемого языка программирования.

Создавайте приложения с повторно используемыми компонентами, как Lego

Инструмент с открытым исходным кодом Bit помогает более чем 250 000 разработчиков создавать приложения с компонентами.

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

Подробнее

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

Микро-интерфейсы

Система дизайна

Совместное использование кода и повторное использование

Монорепо

Узнать больше: