State

Intenção

Permite um objeto alterar o seu comportamento quando o seu estado interno muda. O objeto parecerá ter mudado sua classe.

Vantagens

Desvantagens

Exemplos

Exemplo #1

Uma revista deseja automatizar seu serviço de assinaturas e para isso precisa gerenciar a situação dos assinantes que realizam pagamentos por boleto. Uma assinatura pode estar “ativa” quando o pagamento está regularizado; “em atraso” durante os cinco dias corridos após o vencimento do boleto; “em recuperação” entre 5 e 10 dias corridos após o vencimento do boleto, quando é feita a tentativa de regularização do pagamento junto ao assinante; “inadimplente” quando não há pagamento mesmo após 10 dias do vencimento e “suspensa” quando a interrupção da assinatura é solicitada pelo cliente. Exceto quando a assinatura está suspensa, sempre que o pagamento é realizado a situação dela muda para o estado “ativa”. Quando o pagamento ocorre após o vencimento, são cobrados juros de 10%. Caso esse pagamento seja feito durante a recuperação, os juros podem ser menores em função da negociação com o assinante. Em caso de inadimplência, são cobrados juros de 10% mais mora de 0,2 ao dia e, no caso de interrupção da assinatura, um valor proporcional pode ser cobrado. Uma classe abstrata Situation representa um estado de uma assinatura e as classes Active, Late, Recovering, Overdue e Suspended herdam de Situation e definem os estados possíveis e seus comportamentos. Situation também apresenta a operação calculateSubscriptionValue, que recebe a assinatura como parâmetro e retorna o valor total que deve ser pago, dependendo da situação. A classe Subscription armazena o valor da assinatura e a data de vencimento estipulada, além de possuir as operações sendContent, que verifica o conteúdo disponibilizado, suspendSubscription quando é necessário que a assinatura seja interrompida e verifyPayment que verifica como está a situação do pagamento.

Diagrama de Classe

Exemplo #1 - Diagrama

Participantes

  • Context (Subscription): Define o objeto com estado variável. Mantém uma instância de ConcreteState armazenada.
  • State (Situation): Define uma interface comum para os possíveis estados de Context.
  • ConcreteState subclasses (Active, Late, Recovering, Overdue, Suspended): Cada subclasse herda de State e implementa um comportamento associado a um estado de Context.

Código

package state;

public class Active implements Situation {
    // Cálculo para situação "Ativa". Apenas retorna o valor base da assinatura.
    public float calculateSubscriptionValue(Subscription context){
        return context.getPlanValue();
    }
}
package state;

import java.util.Date;

public class Client {
    public static void main(String[] args) {

        // Subscription policies setup
        Subscription.statesSetup(
                new Active(),
                new Late(),
                new Recovering(),
                new Overdue(),
                new Suspended()
        );

        // Cria uma nova assinatura com a data de vencimento no dia atual do mês.
        Subscription subscription = new Subscription(29.00f, new Date(2020, 7, 6));

        System.out.println("\nAfter subscription start:");
        System.out.println("Total to pay: " + subscription.calculateSubscriptionValue());
        System.out.println("Receiving content? " + subscription.sendContent());

        System.out.println("\nDay after due date:");
        subscription.setSubscriptionLate();
        System.out.println("Total to pay: " + subscription.calculateSubscriptionValue());
        System.out.println("Receiving content? " + subscription.sendContent());

        System.out.println("\n6 days after due date:");
        subscription.setSubscriptionRecovering(0.05f);
        System.out.println("Total to pay: " + subscription.calculateSubscriptionValue());
        System.out.println("Receiving content? " + subscription.sendContent());

        System.out.println("\n11 days after due date:");
        subscription.setSubscriptionOverdue(new Date(2020, 7, 17)); // Data de hoje
        System.out.println("Total to pay: " + subscription.calculateSubscriptionValue());
        System.out.println("Receiving content? " + subscription.sendContent());

        System.out.println("\nAfter suspending subscription:");
        subscription.suspendSubscription(new Date(2020, 7, 25));
        System.out.println("Total to pay: " + subscription.calculateSubscriptionValue());
        System.out.println("Receiving content? " + subscription.sendContent());

        // Após a realização do pagamento é definido o novo prazo
        subscription.setDueDate(new Date(2020, 8, 6));
        subscription.activateSubscription();
    }
}
package state;

public class Late implements Situation {
    // Na situacao "Atrasada", sao cobrados juros de 10% sobre o valor da assinatura
    public float calculateSubscriptionValue(Subscription context){
        return context.getPlanValue() + 0.1f* context.getPlanValue();
    }
}
package state;

import java.util.Date;
import java.util.concurrent.TimeUnit;

public class Overdue implements Situation {

    // Na situacao "Inadimplente", sao cobrados juros de 10% mais 0,2 de multa diarios.

    public float calculateSubscriptionValue(Subscription context){

        // Calculo dos dias de atraso para definir o valor da multa
        long totalDays = context.getToday().getTime() - context.getDueDate().getTime();
        totalDays = TimeUnit.DAYS.convert(totalDays, TimeUnit.MILLISECONDS);

        return context.getPlanValue() + 0.1f* context.getPlanValue() + 0.2f*totalDays;
    }
}
package state;

public class Recovering implements PaymentState {

    public float calculateSubscriptionValue(Subscription context){
        return context.getPlanValue() + context.getNegotiatedInterest()* context.getPlanValue();
    }
}
package state;

public interface Situation {
    // Interface que define a operacao de calculo do valor a ser pago

    public abstract float calculateSubscriptionValue(Subscription context);

}
package state;

import java.util.Date;

public class Subscription {

    private final float planValue;

    private Date dueDate;

    private boolean lateInterest;
    private float negotiatedInterest;
    private Date suspensionDate;
    private Date today;

    private static Situation active;
    private static Situation late;
    private static Situation recovering;
    private static Situation overdue;
    private static Situation suspended;

    // No construtor, são definidos o valor da assinatura e a data de vencimento do pagamento.
    public Subscription(float planValue, Date dueDay){
        this.dueDate = dueDay;
        this.planValue = planValue;

        this.lateInterest = false;
        this.negotiatedInterest = 0;
        this.suspensionDate = null;
        this.today = null;
    }

    public static void statesSetup(
            Situation _active,
            Situation _late,
            Situation _recovering,
            Situation _overdue,
            Situation _suspended
    ) {
        active = _active;
        late = _late;
        recovering = _recovering;
        overdue = _overdue;
        suspended = _suspended;
    }

    public boolean sendContent(){
        return verifyPayment() == active;
    }

    // Modificadores de situação
    public void activateSubscription() {
        this.suspensionDate = null;
        this.today = null;
        this.lateInterest = false;
        this.negotiatedInterest = 0;
    }

    public void setSubscriptionLate() {
        this.suspensionDate = null;
        this.today = null;
        this.lateInterest = true;
        this.negotiatedInterest = 0;
    }

    public void setSubscriptionRecovering(float negotiatedInterest){
        this.suspensionDate = null;
        this.today = null;
        this.lateInterest = false;
        this.negotiatedInterest = negotiatedInterest;
    }

    public void setSubscriptionOverdue(Date today) {
        this.today = today;
        this.suspensionDate = null;
        this.lateInterest = false;
        this.negotiatedInterest = 0;
    }

    public void suspendSubscription(Date today){
        this.suspensionDate = today;
        this.today = null;
        this.lateInterest = false;
        this.negotiatedInterest = 0;
    }

    private Situation verifyPayment(){
        if(this.lateInterest)
            return late;
        if(this.negotiatedInterest != 0)
            return recovering;
        if(this.suspensionDate!=null)
            return suspended;
        if(this.today!=null)
            return overdue;
        return active;
    }

    public float calculateSubscriptionValue() {
        Situation situationSubscription = this.verifyPayment();
        return situationSubscription.calculateSubscriptionValue(this);
    }


    // Getters & Setters

    public float getPlanValue() {
        return planValue;
    }

    public float getNegotiatedInterest() {
        return negotiatedInterest;
    }

    public Date getSuspensionDate() {
        return suspensionDate;
    }

    public void setSuspensionDate(Date suspensionDate) {
        this.suspensionDate = suspensionDate;
    }

    public Date getDueDate() {
        return dueDate;
    }

    public void setDueDate(Date dueDate) {
        this.dueDate = dueDate;
    }

    public Date getToday() {
        return today;
    }

    public void setToday(Date today) {
        this.today = today;
    }


}
package state;

import java.util.Date;
import java.util.concurrent.TimeUnit;

public class Suspended implements Situation {
    // No estado "Suspenso", é necessário saber a data do vencimento e a data da suspensão
    // da assinatura para calcular o valor a ser pago.

    public float calculateSubscriptionValue(Subscription context){
        // Cálculo de dias entre o vencimento e o cancelamento da assinatura
        long totalDays = context.getSuspensionDate().getTime() - context.getDueDate().getTime();
        totalDays = TimeUnit.DAYS.convert(totalDays, TimeUnit.MILLISECONDS);

        return context.getPlanValue() + 0.1f* context.getPlanValue() *totalDays;
    }
}
Clique aqui para fazer o download do código completo de implementação deste Design Pattern.

Padrões Relacionados

Este Padrão pode ser usado para resolver os seguintes problemas: