O Objeto como Máquina de Estados

Aula 2 — Programação Orientada a Objetos

Marcos M. Raimundo — Instituto de Computação, UNICAMP

2026-08-30

Proposta da Aula

Aula 1 deixou uma pergunta em aberto: o construtor deve consertar um preço negativo, ou recusar o objeto?

Esta aula responde formalizando: um objeto bem projetado é uma máquina de estados finita, protegida pelo construtor e por métodos-transição.

Roteiro: máquina de estados → invariantes → construtor (Fail-Fast) → exceções e identidade.

Do Saco de Variáveis ao Agente Ativo

Procedural: o Dado Passivo

struct Pedido { int status; };
void pagar(struct Pedido* p) { p->status = 1; } // mutacao direta!

Orientado a Objetos: o Agente Ativo

public class Pedido {
    private Estado status;
    public void pagar() {
        if (this.status.podePagar()) this.status = Estado.PAGO;
    }
}

O Anti-Padrão do Modelo Anêmico

public void setStatus(String s) { this.status = s; } // permite qualquer absurdo!
pedido.setStatus("ENTREGUE"); // e o pagamento?

Regra de ouro: precisar de get para decidir por fora e depois set = o design falhou.

A Máquina de Estados Finita e o Objeto

Um DFA: Estados, Transições, Determinismo

  • Número limitado de estados
  • Transições, disparadas por eventos
  • Determinismo: para cada (estado, evento), um único próximo estado

O Mapeamento para Java

Atributos private (em conjunto) = o estado

Métodos públicos = as transições — as únicas autorizadas

O Produto como Máquina de Estados

Invariante: estoque \(\ge 0\), sempre. É o único motivo de a máquina existir.

Defendendo a Transição em Código

public void vender(int quantidade) {
    if (quantidade <= 0) throw new IllegalArgumentException("...");
    if (this.estoque < quantidade) throw new IllegalStateException("...");
    this.estoque -= quantidade;
}

Se os atributos de um objeto fossem todos public, a máquina de estados continuaria funcionando — só ficaria “mais rápida”? Por quê?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Julgue também V ou F os quatro itens abaixo — a questão só conta se acertar os 4:

  • □ Os atributos privados materializam os “nós” do grafo de estados do objeto.
  • □ Os métodos públicos são os únicos gatilhos autorizados a processar uma mudança de estado.
  • □ Tornar o atributo estoque público tornaria as transições mais rápidas, sem custo arquitetural.
  • □ Um DFA bem definido garante que, para cada estado e evento, exista exatamente uma transição válida.

A Máquina de Estados Encapsulada — Resposta

Dica

  • ✔ Os atributos privados materializam os “nós” do grafo de estados do objeto — o conjunto de valores dos atributos privados é o estado atual da máquina.
  • ✔ Os métodos públicos são os únicos gatilhos autorizados a processar uma mudança de estado — é exatamente essa a garantia que o encapsulamento protege.
  • ✗ Tornar o atributo estoque público tornaria as transições mais rápidas, sem custo arquitetural — tornaria o atributo público, permitindo que qualquer código “teletransporte” o objeto para um estado ilegal, pulando a validação.
  • ✔ Um DFA bem definido garante que, para cada estado e evento, exista exatamente uma transição válida — é a definição de determinismo de um DFA.

Voltando à pergunta: não — tornar os atributos públicos não deixa a máquina “mais rápida”, deixa de existir como máquina de estados: qualquer código externo passa a poder “teletransportar” o objeto para um estado ilegal, pulando toda transição validada.

Invariantes: do Laço à Classe

Invariante de Laço (Insertion Sort)

“No início da iteração \(i\), o subvetor \([0, i-1]\) contém os elementos originais, já ordenados.”

Inicialização → Manutenção → Término: a prova de corretude do algoritmo.

Invariante de Classe: a Mesma Ideia, Outra Escala

  • ContaBancaria: saldo + limite \(\ge 0\)
  • Triangulo: \(a+b>c\) (e permutações)
  • Carrinho: total = soma dos itens (propriedade derivada)

Por Que Não um setTotal(valor)?

public void adicionarItem(Item i) {
    if (i != null && i.getPreco() > 0) this.total += i.getPreco();
}

O total é resultado de adicionar itens, não uma entrada livre — se fosse public, 50 módulos teriam que lembrar de mantê-lo coerente.

O Construtor como Base da Indução

A Analogia da Indução

  • Base (\(n=0\)): o construtor garante \(o \in V\) desde o nascimento
  • Passo (\(n \Rightarrow n+1\)): todo método preserva \(V\)

Construtor permissivo → Objeto Zumbi: ocupa memória, dados sem sentido, bugs distantes.

Fail-Fast no Construtor

public Produto(String nome, double preco) {
    if (nome == null || nome.trim().isEmpty())
        throw new IllegalArgumentException("Nome obrigatorio.");
    if (preco <= 0)
        throw new IllegalArgumentException("Preco deve ser positivo.");
    this.nome = nome; this.preco = preco;
}

Erro comum: if (preco < 0) this.preco = 0.0; — mascarar, não corrigir. O construtor deve recusar, não consertar.

Engenharia de Métodos: CQS e Design by Contract

CQS: Comandos vs. Consultas

Comando

  • Muda o estado
  • Retorna void

Consulta

  • Devolve um dado
  • Zero efeito colateral

Misturar os dois: verificarSaldo() que cobra taxa a cada chamada — “perguntar” deixa de ser seguro.

Design by Contract

public void processarSaque(double valor) {
    if (valor <= 0) throw new IllegalArgumentException("...");        // PRE
    if (this.saldo < valor) throw new IllegalStateException("...");   // PRE
    this.saldo -= valor;                                              // COMANDO
    assert this.saldo >= 0 : "Invariante violada!";                   // POS
}

Pré-condição falha → culpa do chamador. Pós-condição falha → culpa do autor da classe.

Um método getSaldoLiquido() calcula um imposto e, de quebra, atualiza um contador interno de “quantas vezes o saldo foi consultado”. Isso viola algum princípio desta aula?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Julgue também V ou F os quatro itens abaixo — a questão só conta se acertar os 4:

  • □ Um Comando deve, em geral, retornar void, sinalizando que sua função é mutar o estado, não devolver dado.
  • □ Uma Consulta pode alterar o estado interno do objeto, desde que devolva o valor correto ao chamador.
  • □ Se uma pré-condição falha, a responsabilidade recai sobre quem chamou o método fora de hora ou com dado inválido.
  • □ Se uma pós-condição falha após pré-condições válidas, o bug é do autor da classe, não de quem a chamou.

CQS e Design by Contract — Resposta

Dica

  • ✔ Um Comando deve, em geral, retornar void, sinalizando que sua função é mutar o estado, não devolver dado — é a convenção que sinaliza “isto muda o mundo”.
  • ✗ Uma Consulta pode alterar o estado interno do objeto, desde que devolva o valor correto ao chamador — é exatamente a violação de CQS; uma consulta deve ser idempotente.
  • ✔ Se uma pré-condição falha, a responsabilidade recai sobre quem chamou o método fora de hora ou com dado inválido — pré-condição é o contrato de entrada, sob responsabilidade do chamador.
  • ✔ Se uma pós-condição falha após pré-condições válidas, o bug é do autor da classe, não de quem a chamou — pós-condição é o contrato de saída, sob responsabilidade de quem escreveu o método.

Voltando à pergunta: sim — getSaldoLiquido() está nomeado e usado como Consulta, mas tem efeito colateral (o contador), violando CQS; a “pergunta” deixou de ser segura e idempotente.

Fail-Fast: Exceções como Defesa

Códigos de Erro: o Perigo do Silêncio

-1 pode ser ignorado. O programa segue rodando com dados já corrompidos.

Fail-Fast: o Kit de Exceções

  • IllegalArgumentException — culpa da carga (argumento ruim)
  • IllegalStateException — culpa do momento (objeto no estado errado)
  • NullPointerException — via Objects.requireNonNull(...), proativo

Elevador: Defendendo a Transição

public void subir() {
    if (this.portaAberta) throw new IllegalStateException("...");
    if (this.emMovimento) throw new IllegalStateException("...");
    this.emMovimento = true;
}

RuntimeException (não-checada): erro de contrato é bug de quem chamou — deve quebrar visivelmente, não ser engolido por try-catch.

Identidade Física vs. Identidade Lógica

O Paradoxo da Igualdade

Produto p1 = new Produto(1, "Teclado");
Produto p2 = new Produto(1, "Teclado");
p1 == p2;       // false — enderecos diferentes
p1.equals(p2);  // false! — equals herdado usa ==

Para a JVM: objetos diferentes. Para o negócio: mesmo produto (mesmo SKU).

Corrigindo: equals() + hashCode()

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    return this.sku == ((Produto) o).sku;
}
@Override
public int hashCode() { return Objects.hash(sku); }

Armazém: hashCode = corredor; equals = etiqueta. Um sem o outro coerente → item “some” num HashSet/HashMap.

Por que reescrever equals() sem reescrever hashCode() junto é mais perigoso do que não reescrever nenhum dos dois?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Julgue também V ou F os quatro itens abaixo — a questão só conta se acertar os 4:

  • □ O operador == em Java compara sempre a identidade física — o endereço na Heap.
  • □ O equals() herdado de Object, sem sobrescrita, já entende qual atributo define a igualdade de negócio.
  • □ Se dois objetos são iguais por equals(), seus hashCode() devem obrigatoriamente coincidir.
  • □ Um HashSet pode aceitar “duplicatas” de negócio se equals() não foi sobrescrito para refletir a identidade lógica correta.

Identidade Física e Lógica — Resposta

Dica

  • ✔ O operador == em Java compara sempre a identidade física — o endereço na Heap — == nunca considera conteúdo, só endereço.
  • ✗ O equals() herdado de Object, sem sobrescrita, já entende qual atributo define a igualdade de negócio — o equals() padrão de Object também usa == por baixo; não conhece o domínio.
  • ✔ Se dois objetos são iguais por equals(), seus hashCode() devem obrigatoriamente coincidir — é o contrato inseparável de equals/hashCode.
  • ✔ Um HashSet pode aceitar “duplicatas” de negócio se equals() não foi sobrescrito para refletir a identidade lógica correta — sem equals() correto, dois objetos “iguais” para o negócio são tratados como distintos.

Voltando à pergunta: porque um equals() novo sem hashCode() correspondente quebra o contrato entre os dois — dois objetos “iguais” pelo equals() podem cair em posições diferentes de um HashSet/HashMap, e o item some silenciosamente, um bug muito mais difícil de rastrear do que usar a identidade física padrão dos dois métodos.

Conclusão

O que Aprendemos

  1. Objeto = máquina de estados: atributos = estado, métodos = transições
  2. Invariante: a regra que o encapsulamento existe para proteger
  3. Construtor = base da indução; Fail-Fast recusa “objetos zumbis”
  4. IllegalArgumentException (dado errado) vs. IllegalStateException (momento errado)

Próxima aula: as bordas da máquina — contrato, escopo, ciclo de vida e identidade.