Padrão de Design de Estado em Java

Padrão de Design de Estado em Java

1. Visão geral

Neste tutorial, apresentaremos um dos padrões de design comportamental do GoF - o padrão State.

Inicialmente, forneceremos uma visão geral de seu objetivo e explicaremos o problema que ele tenta resolver. Em seguida, veremos o diagrama UML do Estado e a implementação do exemplo prático.

2. Estado Design Pattern

A ideia principal do padrão State éallow the object for changing its behavior without changing its class. Além disso, ao implementá-lo, o código deve permanecer mais limpo sem muitas instruções if / else.

Imagine que temos um pacote que é enviado para uma agência postal, o pacote em si pode ser encomendado, depois entregue em uma agência postal e finalmente recebido por um cliente. Agora, dependendo do estado real, queremos imprimir seu status de entrega.

A abordagem mais simples seria adicionar alguns sinalizadores booleanos e aplicar instruções if / else simples em cada um dos nossos métodos na classe. Isso não vai complicar muito em um cenário simples. No entanto, isso pode complicar e poluir nosso código quando obtermos mais estados para processar, o que resultará em ainda mais instruções if / else.

Além disso, toda a lógica de cada um dos estados se espalharia por todos os métodos. Agora, é aqui que o padrão State pode ser considerado para uso. Graças ao padrão de design State, podemos encapsular a lógica em classes dedicadas, aplicar o limpador de barbearSingle Responsibility PrincipleeOpen/Closed Principle, e um código mais sustentável.

3. Diagrama UML

UML diagram of state design pattern

No diagrama UML, vemos que a classeContext tem umState associado que vai mudar durante a execução do programa.

Nosso contexto está indo paradelegate the behavior to the state implementation. Em outras palavras, todas as solicitações recebidas serão tratadas pela implementação concreta do estado.

Vemos que a lógica está separada e a adição de novos estados é simples - basta adicionar outra implementaçãoState, se necessário.

4. Implementação

Vamos projetar nosso aplicativo. Como já mencionado, o pacote pode ser pedido, entregue e recebido, portanto, teremos três estados e a classe de contexto.

Primeiro, vamos definir nosso contexto, que vai ser uma classePackage:

public class Package {

    private PackageState state = new OrderedState();

    // getter, setter

    public void previousState() {
        state.prev(this);
    }

    public void nextState() {
        state.next(this);
    }

    public void printStatus() {
        state.printStatus();
    }
}

Como podemos ver, ele contém uma referência para gerenciar o estado, observe os métodospreviousState(), nextState() and printStatus() onde delegamos o trabalho ao objeto de estado. Os estados serão vinculados entre si eevery state will set another one based on this reference serão passados ​​para os dois métodos.

O cliente irá interagir com a classePackage, mas não terá que lidar com a configuração dos estados, tudo o que o cliente precisa fazer é ir para o próximo estado ou para o anterior.

Em seguida, teremosPackageState, que possui três métodos com as seguintes assinaturas:

public interface PackageState {

    void next(Package pkg);
    void prev(Package pkg);
    void printStatus();
}

Essa interface será implementada por cada classe de estado concreta.

O primeiro estado concreto seráOrderedState:

public class OrderedState implements PackageState {

    @Override
    public void next(Package pkg) {
        pkg.setState(new DeliveredState());
    }

    @Override
    public void prev(Package pkg) {
        System.out.println("The package is in its root state.");
    }

    @Override
    public void printStatus() {
        System.out.println("Package ordered, not delivered to the office yet.");
    }
}

Aqui, apontamos para o próximo estado que ocorrerá após o pedido do pacote. O estado ordenado é o nosso estado raiz e o marcamos explicitamente. Podemos ver nos dois métodos como a transição entre estados é tratada.

Vamos dar uma olhada na classeDeliveredState:

public class DeliveredState implements PackageState {

    @Override
    public void next(Package pkg) {
        pkg.setState(new ReceivedState());
    }

    @Override
    public void prev(Package pkg) {
        pkg.setState(new OrderedState());
    }

    @Override
    public void printStatus() {
        System.out.println("Package delivered to post office, not received yet.");
    }
}

Mais uma vez, vemos a ligação entre os estados. O estado do pacote está mudando de pedido para entregue, a mensagem emprintStatus() também muda.

O último status éReceivedState:

public class ReceivedState implements PackageState {

    @Override
    public void next(Package pkg) {
        System.out.println("This package is already received by a client.");
    }

    @Override
    public void prev(Package pkg) {
        pkg.setState(new DeliveredState());
    }
}

É aqui que atingimos o último estado, apenas podemos reverter para o estado anterior.

Já vemos que há recompensa, já que um estado conhece o outro. Estamos tornando-os fortemente acoplados.

5. Teste

Vamos ver como a implementação se comporta. Primeiro, vamos verificar se as transições de configuração funcionam conforme o esperado:

@Test
public void givenNewPackage_whenPackageReceived_thenStateReceived() {
    Package pkg = new Package();

    assertThat(pkg.getState(), instanceOf(OrderedState.class));
    pkg.nextState();

    assertThat(pkg.getState(), instanceOf(DeliveredState.class));
    pkg.nextState();

    assertThat(pkg.getState(), instanceOf(ReceivedState.class));
}

Em seguida, verifique rapidamente se nosso pacote pode voltar com seu estado:

@Test
public void givenDeliveredPackage_whenPrevState_thenStateOrdered() {
    Package pkg = new Package();
    pkg.setState(new DeliveredState());
    pkg.previousState();

    assertThat(pkg.getState(), instanceOf(OrderedState.class));
}

Depois disso, vamos verificar a mudança de estado e ver como a implementação do métodoprintStatus() muda sua implementação no tempo de execução:

public class StateDemo {

    public static void main(String[] args) {

        Package pkg = new Package();
        pkg.printStatus();

        pkg.nextState();
        pkg.printStatus();

        pkg.nextState();
        pkg.printStatus();

        pkg.nextState();
        pkg.printStatus();
    }
}

Isso nos dará a seguinte saída:

Package ordered, not delivered to the office yet.
Package delivered to post office, not received yet.
Package was received by client.
This package is already received by a client.
Package was received by client.

Conforme mudamos o estado de nosso contexto, o comportamento estava mudando, mas a classe permanece a mesma. Assim como a API que usamos.

Além disso, ocorreu a transição entre os estados, nossa classe mudou seu estado e, consequentemente, seu comportamento.

6. Desvantagens

A desvantagem do padrão de estado é a recompensa ao implementar a transição entre os estados. Isso torna o estado codificado, o que é uma má prática em geral.

Mas, dependendo de nossas necessidades e requisitos, isso pode ou não ser um problema.

7. Estado vs. Padrão de Estratégia

Os dois padrões de design são muito semelhantes, mas o diagrama UML é o mesmo, com a idéia por trás deles um pouco diferente.

Primeiro, ostrategy pattern defines a family of interchangeable algorithms. Geralmente, eles atingem o mesmo objetivo, mas com uma implementação diferente, por exemplo, algoritmos de classificação ou renderização.

In state pattern, the behavior might change completely, com base no estado real.

A seguir,in strategy, the client has to be aware of the possible strategies to use and change them explicitly. Enquanto no padrão de estado, cada estado é vinculado a outro e cria o fluxo como na Máquina de Estados Finitos.

8. Conclusão

O padrão de design de estado é ótimo quando queremosavoid primitive if/else statements. Em vez disso, usamosextract the logic to separate classese deixamos nossocontext object delegate the behavior para os métodos implementados na classe de estado. Além disso, podemos alavancar as transições entre os estados, onde um estado pode alterar o estado do contexto.

Em geral, esse padrão de design é ótimo para aplicativos relativamente simples, mas para uma abordagem mais avançada, podemos dar uma olhada emSpring’s State Machine tutorial.

Como de costume, o código completo está disponível emthe GitHub project.