O Objeto como Máquina de Estados

Aula 2 — Programação Orientada a Objetos

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

2026-08-26

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).

A Máquina de Estados Encapsulada

Dica

Julgue V ou F — a questão só conta se acertar os 4 itens:

  1. Os atributos privados materializam os “nós” do grafo de estados do objeto.
  2. Os métodos públicos são os únicos gatilhos autorizados a processar uma mudança de estado.
  3. Tornar o atributo estoque público tornaria as transições mais rápidas, sem custo arquitetural.
  4. 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

  1. Verdadeiro — o conjunto de valores dos atributos privados é o estado atual da máquina.
  2. Verdadeiro — é exatamente essa a garantia que o encapsulamento protege.
  3. Falso — tornaria o atributo público, permitindo que qualquer código “teletransporte” o objeto para um estado ilegal, pulando a validação.
  4. Verdadeiro — é a definição de determinismo de um DFA.

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).

CQS e Design by Contract

Dica

Julgue V ou F — a questão só conta se acertar os 4 itens:

  1. Um Comando deve, em geral, retornar void, sinalizando que sua função é mutar o estado, não devolver dado.
  2. Uma Consulta pode alterar o estado interno do objeto, desde que devolva o valor correto ao chamador.
  3. Se uma pré-condição falha, a responsabilidade recai sobre quem chamou o método fora de hora ou com dado inválido.
  4. 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

  1. Verdadeiro — é a convenção que sinaliza “isto muda o mundo”.
  2. Falso — é exatamente a violação de CQS; uma consulta deve ser idempotente.
  3. Verdadeiro — pré-condição é o contrato de entrada, sob responsabilidade do chamador.
  4. Verdadeiro — pós-condição é o contrato de saída, sob responsabilidade de quem escreveu o método.

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).

Identidade Física e Lógica

Dica

Julgue V ou F — a questão só conta se acertar os 4 itens:

  1. O operador == em Java compara sempre a identidade física — o endereço na Heap.
  2. O equals() herdado de Object, sem sobrescrita, já entende qual atributo define a igualdade de negócio.
  3. Se dois objetos são iguais por equals(), seus hashCode() devem obrigatoriamente coincidir.
  4. 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

  1. Verdadeiro== nunca considera conteúdo, só endereço.
  2. Falso — o equals() padrão de Object também usa == por baixo; não conhece o domínio.
  3. Verdadeiro — é o contrato inseparável de equals/hashCode.
  4. Verdadeiro — sem equals() correto, dois objetos “iguais” para o negócio são tratados como distintos.

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.