O Objeto como Máquina de Estados
Aula 2 — Programação Orientada a Objetos
1 Proposta da Aula
Na Aula 1, o construtor de Produto chamou setPreco() para validar o preço inicial. Deixamos uma pergunta em aberto: e se o preço vier negativo — o construtor deve “consertar” o valor silenciosamente, ou recusar a existência do objeto? Esta aula responde a essa pergunta formalizando uma ideia que já rondava a Aula 1: todo objeto bem projetado é a implementação física de uma máquina de estados finita, protegida por um construtor que age como guardião e por métodos que agem como transições validadas.
O roteiro, em quatro perguntas:
- O que muda quando paramos de ver uma classe como um “saco de variáveis com getters e setters” e passamos a vê-la como uma máquina de estados?
- O que é uma invariante, e por que ela é o verdadeiro motivo de existir do encapsulamento?
- Por que o construtor é matematicamente diferente dos outros métodos — e o que acontece quando ele falha em proteger essa diferença?
- Quando um objeto deve recusar uma operação lançando uma exceção, e como o Java distingue “argumento errado” de “momento errado”?
2 Do Saco de Variáveis ao Agente Ativo
Em C, uma struct agrupa dados, mas é inteiramente passiva — as funções que operam sobre ela vivem em outro lugar, e a responsabilidade de manter a consistência recai sobre quem escreve essas funções externas:
struct Pedido {
int status;
};
// A funcao externa dita a regra
void pagar(struct Pedido* p) {
p->status = 1; // Mutacao direta!
}Em Java, a classe agrupa os dados e as funções que têm o direito de alterá-los. O objeto deixa de ser memória passiva e se torna um agente ativo: você não altera o estado diretamente, você faz um pedido, e o objeto decide se a transição é legal.
public class Pedido {
private Estado status;
// O objeto controla a propria mutacao
public void pagar() {
if (this.status.podePagar()) {
this.status = Estado.PAGO;
}
}
}Essa união indissociável entre dados e as regras que os governam é o que chamamos de encapsulamento forte.
O anti-padrão do Modelo Anêmico. Um dos maiores desserviços do ensino básico de Java é reduzir o encapsulamento a “deixar os atributos private” sem se importar com o resto. Se todo atributo private ganha automaticamente um getter e um setter cegos, a inteligência vazou de volta para fora da classe — é apenas uma struct glorificada:
public class PedidoAnemico {
private String status;
// Setter cego: permite qualquer absurdo!
public void setStatus(String s) { this.status = s; }
public String getStatus() { return this.status; }
}
// O erro ocorre externamente:
pedido.setStatus("ENTREGUE"); // E o pagamento?A regra de ouro: se você precisa inspecionar os dados de um objeto (get) para decidir por fora e depois alterar seu estado (set), o design falhou. O objeto deve tomar as próprias decisões.
3 A Máquina de Estados Finita e o Objeto
Um Autômato Finito Determinístico (DFA) é um modelo matemático clássico da Ciência da Computação: um sistema com um número limitado de estados, que muda de um estado para outro através de transições disparadas por eventos, de forma determinística — para cada estado e cada evento, existe exatamente um próximo estado.
O mapeamento para Java é direto: os atributos privados (em conjunto) definem em qual estado o objeto se encontra; os métodos públicos são as únicas transições autorizadas. O encapsulamento, sob esta luz, ganha um propósito mais profundo do que “esconder”: ele impede que o código externo “teletransporte” o objeto para um estado ilegal, pulando as transições validadas.
Considere um Produto com três estados lógicos, definidos pelo seu estoque:
A invariante que a máquina inteira existe para proteger é simples e inegociável: \(\text{estoque} \ge 0\). O objeto nasce em CADASTRADO (via new Produto()), transita para DISPONÍVEL ao repor estoque, e só aceita vender() nesse estado. A defesa é feita em código, dentro do próprio método de transição:
public class Produto {
private int estoque; // O Estado Oculto
// A Transicao (Metodo)
public void vender(int quantidade) {
// 1. Verificacao da legalidade da transicao (Guarda da Invariante)
if (quantidade <= 0) {
throw new IllegalArgumentException("Quantidade deve ser positiva.");
}
if (this.estoque < quantidade) {
throw new IllegalStateException("Transicao invalida: Estoque insuficiente.");
}
// 2. Execucao da mudanca de estado
this.estoque -= quantidade;
}
}Se o estoque tem 2 unidades e uma falha no carrinho tenta vender(5), a máquina não processa a requisição e assume um estado impossível de \(-3\): o método interroga o estado atual antes de mudar qualquer byte, e aborta a operação — o padrão Fail-Fast, que voltará com mais rigor ainda no Bloco do construtor.
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).
4 Invariantes: do Laço à Classe
O conceito de invariante não nasceu na Orientação a Objetos — vem da análise de algoritmos. Uma invariante de laço é uma propriedade que se mantém verdadeira antes da primeira iteração (inicialização), permanece verdadeira a cada volta (manutenção), e no fim do laço prova que o algoritmo alcançou seu objetivo (término). No Insertion Sort, por exemplo, a invariante é: “no início de cada iteração \(i\), o subvetor de índices \(0\) a \(i-1\) contém os elementos originais, já ordenados”. Essa garantia é o que permite provar a corretude do algoritmo sem precisar “torcer” para que dê certo no final.
A invariante de classe eleva essa mesma ideia da execução de um laço para toda a vida de um objeto na Heap: é uma regra que deve continuar verdadeira do nascimento (construtor) até qualquer ponto depois de qualquer método. Exemplos concretos:
ContaBancaria: “o saldo mais o limite de crédito aprovado deve ser sempre \(\ge 0\)”.Triangulo: “a soma de dois lados deve ser sempre maior que o terceiro” (\(a+b>c\), e as outras duas permutações).Carrinho: “o total deve ser sempre igual à soma dos preços dos itens” — uma propriedade derivada, que não deveria ter umsetTotal(valor)— o total é resultado, não entrada.
public class Carrinho {
private double total = 0.0; // Estado Oculto
// A transicao oficial garante a Invariante automaticamente
public void adicionarItem(Item i) {
if (i != null && i.getPreco() > 0) {
this.total += i.getPreco();
}
}
}Se você expõe saldo como public numa ContaBancaria usada por 50 módulos diferentes, cada um dos 50 precisaria lembrar de checar o limite antes de alterar o saldo — e basta um esquecimento para quebrar a invariante e criar um risco financeiro real. Encapsulando o atributo, a regra existe em um único lugar: dentro do método que a protege.
5 O Construtor como Base da Indução
Para quem gosta de rigor matemático: a corretude de uma classe pode ser vista como uma prova por indução finita. Seja \(S\) o conjunto de todos os estados possíveis de um objeto, e \(V \subset S\) o subconjunto que respeita todas as invariantes.
- Base (\(n=0\)): o construtor tem o dever de garantir que o objeto recém-criado \(o\) satisfaça \(o \in V\) desde o primeiro milissegundo de vida na Heap. Se os argumentos levariam a \(o \notin V\), o construtor deve falhar ativamente.
- Passo indutivo (\(n \Rightarrow n+1\)): dado um objeto em estado válido \(s_n \in V\), qualquer método público invocado sobre ele deve obrigatoriamente resultar em \(s_{n+1} \in V\).
Se o construtor falha em estabelecer a base, todo o resto do sistema desmorona — os métodos assumem, sem verificar de novo, que o objeto já nasceu válido. Um construtor permissivo cria o que se chama de Objeto Zumbi: uma instância que ocupa espaço na Heap, mas cujos dados não fazem sentido, produzindo erros distantes e difíceis de rastrear.
public class Produto {
private String nome;
private double preco;
/** Constructor: A Base da Inducao */
public Produto(String nome, double preco) {
// FAIL-FAST: Validacao agressiva das Invariantes Iniciais
if (nome == null || nome.trim().isEmpty()) {
throw new IllegalArgumentException("Nome obrigatorio.");
}
if (preco <= 0) {
throw new IllegalArgumentException("Preco deve ser positivo.");
}
// Se sobreviveu as validacoes, o estado V nasce.
this.nome = nome;
this.preco = preco;
}
}Um erro comum de quem está aprendendo: “consertar” o dado ruim silenciosamente (if (preco < 0) this.preco = 0.0;). Isso é um erro de design grave — mascarar dados inválidos cria uma falsa sensação de segurança e esconde uma falha de validação que deveria ter sido pega antes, na interface ou na camada de serviço. O construtor deve ser um guardião honesto: se a entrada está errada, a responsabilidade é de quem tentou criar o objeto, e o sistema deve recusar imediatamente.
6 Engenharia de Métodos: CQS e Design by Contract
Depois que o construtor garante um nascimento íntegro, a responsabilidade de manter a máquina segura passa para os métodos. O princípio CQS (Command-Query Separation) exige que todo método público pertença a exatamente um destes dois papéis:
- Comando (Command): executa uma transição e altera o estado observável. Deve retornar
void. Ex.:pedido.cancelar(). - Consulta (Query): devolve um dado ao chamador. É proibido ter efeito colateral — uma pergunta não pode alterar a resposta. Ex.:
produto.getPrecoLiquido().
O perigo de misturar os dois: imagine verificarSaldo() que, a cada chamada, secretamente cobra R$ 0,50 de taxa. Um desenvolvedor que só queria mostrar o saldo na tela acaba drenando a conta do usuário sem querer. Separar Comandos de Consultas garante que perguntar é sempre uma operação segura e idempotente.
O Design by Contract formaliza essa mesma disciplina em termos de pré e pós-condições:
public void processarSaque(double valor) {
// PRE-CONDICAO: Protegendo a transicao contra chamadas invalidas
if (valor <= 0) throw new IllegalArgumentException("Valor negativo.");
if (this.saldo < valor) throw new IllegalStateException("Saldo insuficiente.");
this.saldo -= valor; // EXECUCAO DA TRANSICAO (Comando)
// POS-CONDICAO e INVARIANTE (Garantida pelo design do codigo acima)
assert this.saldo >= 0 : "Invariante violada apos o saque!";
}Pré-condições são barreiras de entrada: se falham, a culpa é de quem chamou o método fora de hora ou com dado inválido. Pós-condições atestam que, cumpridas as pré-condições, o método devolve o objeto num estado onde todas as invariantes seguem válidas — se isso falhar, o bug é do autor da classe, não de quem a chamou.
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).
7 Fail-Fast: Exceções como Defesa
Em C, uma função de saque devolveria 1 (sucesso) ou -1 (falha). O perigo estrutural: códigos de erro podem ser ignorados silenciosamente — se o chamador esquecer o if (resultado == -1), o programa segue rodando como se nada tivesse acontecido, e os dados começam a se corromper sem aviso.
Fail-Fast propõe o oposto: ao detectar uma pré-condição violada, o método lança uma exceção, interrompendo o fluxo imediatamente, na mesma linha da infração — impossível de ignorar.
Para blindar uma máquina de estados, você não precisa (ainda) criar suas próprias exceções — o kit padrão do Java já cobre os casos comuns:
IllegalArgumentException— a culpa é da carga: um argumento passado não faz sentido no domínio (ex.: idade negativa).IllegalStateException— a culpa é do momento: o dado está correto, mas o objeto não está no estado certo para a operação (ex.:carrinho.finalizarCompra()num carrinho vazio).NullPointerException— usada proativamente viaObjects.requireNonNull(cliente, "..."), para recusar umnulljá na porta de entrada, em vez de deixar o erro estourar linhas abaixo.
public class Elevador {
private boolean portaAberta = false;
private boolean emMovimento = false;
public void subir() {
// 1. Protegendo contra transicao em Estado Invalido
if (this.portaAberta) {
throw new IllegalStateException("Nao pode subir com porta aberta.");
}
if (this.emMovimento) {
throw new IllegalStateException("O elevador ja esta em movimento.");
}
// 2. Mutacao Segura (Comando)
this.emMovimento = true;
}
}Essas três exceções são subclasses de RuntimeException (não-checadas) — não exigem throws na assinatura nem try-catch do lado de quem chama. A filosofia é: um erro de contrato (argumento ruim, chamada fora de hora) é bug de quem chamou, e bugs devem falhar de forma visível para serem corrigidos antes de produção, não silenciados com try-catch.
8 Identidade Física vs. Identidade Lógica
Se criarmos duas instâncias idênticas de Produto na Heap, elas são o mesmo objeto? Para a JVM, não — == compara endereços físicos. Para o negócio, muitas vezes sim: dois objetos com o mesmo SKU deveriam ser tratados como “o mesmo produto”.
Produto p1 = new Produto(1, "Teclado");
Produto p2 = new Produto(1, "Teclado");
System.out.println(p1 == p2); // false (enderecos fisicos diferentes)
System.out.println(p1.equals(p2)); // false! (Java nao sabe que o ID 1 importa)O equals() herdado de Object usa == por padrão — ele não conhece o seu domínio de negócio. Se seu sistema tenta usar um HashSet<Produto> para evitar duplicatas no carrinho, e dois Produto com o mesmo SKU foram instanciados em requisições diferentes, o Set vai aceitar os dois como distintos — e o cliente é cobrado duas vezes pelo mesmo item.
A correção: sobrescrever equals() para comparar o atributo que de fato define identidade de negócio (o SKU), e hashCode() em conjunto — nunca um sem o outro:
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1. mesma identidade fisica?
if (o == null || getClass() != o.getClass()) return false; // 2. protecao de tipo
Produto produto = (Produto) o;
return this.sku == produto.sku; // 3. regra de negocio: SKU define identidade
}
@Override
public int hashCode() {
return Objects.hash(sku); // o hash deve olhar APENAS para os campos do equals
}Analogia do armazém: hashCode() é o número do corredor (busca rápida); equals() é a checagem da etiqueta quando você chega lá. Se equals() compara o SKU mas hashCode() usa outra coisa (a cor da embalagem, digamos), caixas iguais acabam em corredores diferentes — o sistema procura no corredor errado e afirma, silenciosamente, que o item não existe. É por isso que os dois métodos formam um contrato indissociável.
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).
9 Conclusão
Voltando às quatro perguntas da abertura:
- Saco de variáveis vs. máquina de estados: uma classe deixa de ser um
structglorificado quando os métodos, não o mundo externo, decidem se uma mudança de estado é legal. - Invariante e o motivo do encapsulamento: a invariante é a regra que nunca pode ser violada; o encapsulamento existe precisamente para que só o próprio objeto possa proteger essa regra.
- O construtor é diferente: ele é a base de uma indução — se falha em recusar dados ruins, todo o resto do sistema herda a corrupção sob a forma de “objetos zumbis”.
- Quando lançar exceção:
IllegalArgumentExceptionquando o dado está errado,IllegalStateExceptionquando o momento está errado — e ambas devem interromper o fluxo imediatamente, não ser mascaradas.
Ponte para a Aula 3
Falamos da máquina de estados por dentro — invariantes, construtor, transições. A Aula 3 olha para as bordas dessa máquina: o contrato entre interface e implementação visto com mais rigor filosófico, a diferença entre o escopo de uma variável e o ciclo de vida de um objeto (o que já apareceu na Aula 1 com a Heap e o GC), e a identidade revisitada sob a ótica da imutabilidade.
10 Exercícios
10.1 Questões discursivas
Numa auditoria de um sistema de pagamentos, encontrou-se a classe
Transferenciacom getters e setters cegos para todas as propriedades, sem lógica interna alguma. Usando a teoria de máquinas de estado e proteção de invariantes, explique por que o Modelo Anêmico é um risco inaceitável em software financeiro.Um estagiário validou salários no construtor de
Funcionarioassim:if (salario < 0) this.salario = 0;. Usando o conceito de Fail-Fast e a analogia da base da indução, critique esse mecanismo e reescreva o construtor de forma robusta.Dois usuários preenchem os mesmos dados num formulário e o sistema cria dois objetos
Passaportedistintos, com dados idênticos. Explique por quep1.equals(p2)retornafalsepor padrão, e como corrigir esse comportamento para refletir a identidade lógica correta do domínio.
10.2 Questões de Verdadeiro/Falso
Cada bloco de 4 itens trata do mesmo tema. A questão só é considerada correta se todos os 4 itens forem julgados corretamente (deixar em branco tem penalidade de 20% da nota da questão).
- ( ) O Estado de um objeto é definido pelo conjunto de valores de seus atributos num dado instante.
- ( ) O Comportamento é a única via autorizada a alterar o Estado de um objeto bem projetado.
- ( ) Dois objetos com Estado idêntico são sempre a mesma Identidade física na Heap.
- ( ) A Identidade é garantida pela JVM no momento em que o objeto é instanciado via
new.
- ( ) Em C, uma
structagrupa dados e as funções que operam sobre eles na mesma unidade. - ( ) Numa
struct, a responsabilidade de manter a consistência do dado recai sobre a função externa que o manipula. - ( ) Em Java, o objeto pode recusar uma mutação que viole suas próprias regras internas.
- ( ) A união entre dados e as regras que os protegem é o que caracteriza o encapsulamento forte.
- ( ) Ocorre quando todos os atributos privados recebem getters e setters cegos, sem validação.
- ( ) É considerado um design rico, pois maximiza a flexibilidade de quem consome a classe.
- ( ) Esvazia o objeto de regra de negócio, reduzindo-o a uma estrutura de dados passiva.
- ( ) O design correto prefere métodos verbais de intenção (
pagar(),cancelar()) a setters genéricos.
- ( ) Um DFA tem um número limitado e conhecido de estados possíveis.
- ( ) Para cada par (estado, evento), existe exatamente um próximo estado, por definição de determinismo.
- ( ) Um DFA bem definido permite comportamentos “mágicos” e imprevisíveis em casos de borda.
- ( ) O determinismo de um DFA facilita a prova formal da corretude de um sistema.
- ( ) Atributos privados materializam, em conjunto, o “nó” atual da máquina de estados do objeto.
- ( ) Métodos públicos atuam como as transições — os únicos eventos autorizados a mudar o estado.
- ( ) Tornar os atributos públicos tornaria as transições mais eficientes, sem custo arquitetural.
- ( ) O encapsulamento impede que código externo “pule” transições e alcance um estado ilegal.
- ( ) Uma invariante de classe deve permanecer verdadeira após a execução de qualquer método público.
- ( ) Uma propriedade derivada, como o total de um carrinho, dispensa proteção por invariante.
- ( ) Expor o estado publicamente distribui a responsabilidade de manter a invariante por todo o sistema.
- ( ) Centralizar a regra dentro do próprio objeto reduz o risco de um módulo externo esquecer de aplicá-la.
- ( ) O construtor corresponde à etapa “base” de uma prova por indução sobre a validade do objeto.
- ( ) Um construtor permissivo, que aceita qualquer dado, é uma boa prática para evitar exceções.
- ( ) Lançar uma exceção no construtor impede que um “Objeto Zumbi” passe a existir na Heap.
- ( ) Se a base da indução falha, os métodos subsequentes não podem mais confiar na validade do objeto.
- ( ) Comandos são os únicos métodos autorizados a provocar mutação de estado.
- ( ) O CQS recomenda que Comandos retornem
void, sinalizando ausência de dado de retorno. - ( ) Uma Consulta pode alterar o estado interno, desde que devolva o dado correto ao chamador.
- ( ) Consultas idempotentes evitam efeitos colaterais acidentais ao simplesmente inspecionar um objeto.
- ( ) Pré-condições avaliam se o pedido faz sentido antes da execução do método.
- ( ) Se uma pré-condição falha, a responsabilidade recai sobre quem escreveu a classe, não sobre o chamador.
- ( ) Pós-condições atestam que as invariantes permanecem válidas depois da transição.
- ( ) Uma pós-condição violada, após pré-condições satisfeitas, indica um bug no próprio método.
- ( ) Códigos de erro como
-1podem ser silenciosamente ignorados por quem chama a função. - ( )
IllegalArgumentExceptiondeve ser usada quando o objeto está no estado errado para a operação. - ( )
IllegalStateExceptionsinaliza que o dado é válido, mas o momento da chamada não é. - ( )
Objects.requireNonNull(...)permite dispararNullPointerExceptionproativamente, na entrada do método.
- ( ) O operador
==compara sempre o endereço físico na Heap, nunca o conteúdo do objeto. - ( ) O
equals()herdado deObject, sem sobrescrita, já reflete a identidade lógica de negócio. - ( ) Dois objetos podem ser fisicamente distintos (
==falso) e ainda assim logicamente iguais para o negócio. - ( ) Um
HashSetpode aceitar duplicatas lógicas seequals()não foi corretamente sobrescrito.
- ( ) Se dois objetos são iguais por
equals(), seushashCode()devem obrigatoriamente coincidir. - ( ) O
hashCode()pode usar atributos completamente diferentes dos usados peloequals(), sem risco. - ( ) Violar o contrato
equals()/hashCode()pode causar o desaparecimento silencioso de itens numHashMap. - ( ) IDEs modernas geram os dois métodos juntos justamente para preservar essa coerência.