Exercícios — O Objeto como Máquina de Estados

Aula 2 — Programação Orientada a Objetos

Autor

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

Aula Soluções

Questões discursivas

  1. Numa auditoria de um sistema de pagamentos, encontrou-se a classe Transferencia com 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.

  2. Um estagiário validou salários no construtor de Funcionario assim: 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.

  3. Dois usuários preenchem os mesmos dados num formulário e o sistema cria dois objetos Passaporte distintos, com dados idênticos. Explique por que p1.equals(p2) retorna false por padrão, e como corrigir esse comportamento para refletir a identidade lógica correta do domínio.

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

NotaTeste 1 — Estado, Comportamento e Identidade (revisitados)
  • □ Um objeto sem nenhum atributo (nenhum campo mutável) não possui Estado algum, já que não haveria nada para variar ao longo do tempo.
  • □ Usando reflection (java.lang.reflect), é possível alterar um atributo private diretamente, sem passar por nenhum método — o que mostra que “Comportamento como única via de mutação” é uma convenção de projeto, não uma barreira física imposta pela JVM.
  • □ 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.
NotaTeste 2 — Struct Passivo vs. Agente Ativo
  • □ Em C, uma struct agrupa dados e as funções que operam sobre eles na mesma unidade.
  • □ Se a função que manipula uma struct em C for declarada static no mesmo arquivo-fonte da struct, ela deixa de ser “externa” e a struct passa a ter o mesmo encapsulamento forte de uma classe Java.
  • □ Basta que os atributos de uma classe Java sejam private para que o objeto recuse automaticamente qualquer mutação que viole suas regras internas, mesmo que o método que os altera não contenha nenhuma verificação.
  • □ Uma classe cujos atributos são todos public, mas que também oferece métodos como pagar() com toda a validação de regras de negócio, ainda apresenta encapsulamento forte, desde que os programadores usem apenas esses métodos por convenção, e nunca acessem os atributos diretamente.
NotaTeste 3 — O Modelo de Domínio Anêmico
  • □ Uma classe com atributos private, apenas getters e nenhum setter (os atributos só podem ser lidos, nunca alterados após a construção), ainda se qualifica como Modelo de Domínio Anêmico.
  • □ É considerado um design rico, pois maximiza a flexibilidade de quem consome a classe.
  • □ A classe ProdutoBuilder, cujos métodos internos (comNome(...), comPreco(...)) apenas acumulam valores sem nenhuma validação, e cuja validação real só ocorre no método final build(), sofre exatamente do mesmo problema estrutural do Modelo Anêmico.
  • □ O design correto prefere métodos verbais de intenção (pagar(), cancelar()) a setters genéricos.
NotaTeste 4 — A Máquina de Estados Finita (DFA)
  • □ Um autômato com um número infinito de estados possíveis ainda seria, por definição, determinístico, desde que cada par (estado, evento) leve a exatamente um único próximo estado.
  • □ No DFA do Produto apresentado na aula, chamar vender(quantidade) com uma quantidade maior que o estoque, estando o produto em DISPONÍVEL, não corresponde a nenhuma transição definida no diagrama — por isso o método precisa lançar uma exceção em vez de deixar a máquina seguir para um estado inexistente.
  • □ 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.
NotaTeste 5 — A Máquina de Estados Encapsulada em Java
  • □ Um atributo declarado static numa classe Java não faz parte do “nó” de estado de nenhuma instância individual da máquina, porque seu valor é compartilhado por todos os objetos daquela classe, e não isolado por instância.
  • □ Uma classe cujos únicos métodos públicos, além do construtor, são getters (nenhum método além deles), ainda define uma máquina de estados capaz de alcançar mais de um estado depois de criado o objeto.
  • □ Uma classe cujo atributo private List<Item> itens é exposto só por um getter que devolve a referência direta à lista (return this.itens;), mantém o mesmo nível de proteção contra transições ilegais que um atributo private bem encapsulado, já que a lista nunca é declarada public.
  • □ Um método público restaurarEstado(EstadoSerializado dump), que copia todos os campos de um objeto externo dump diretamente para os atributos private do objeto, sem nenhuma validação, preserva a proteção do encapsulamento, mesmo aceitando dados vindos de fora do sistema (como um arquivo ou uma requisição de rede).
NotaTeste 6 — Invariantes de Classe
  • □ Durante a execução do corpo de um método público — antes de chegar à instrução return —, a invariante de classe pode estar temporariamente violada, desde que seja restaurada até o momento em que o método devolve o controle para quem o chamou.
  • □ Uma propriedade derivada, como o total de um carrinho, dispensa proteção por invariante.
  • □ Numa ContaBancaria cujo atributo saldo é public, mas que hoje é manipulada por um único módulo em todo o sistema, o risco de quebrar a invariante desaparece, pois só existe um lugar de código que precisa lembrar de checar a regra.
  • □ Centralizar a regra da invariante dentro de um único método synchronized do próprio objeto garante, sozinho, que a invariante nunca será violada, mesmo que a classe tenha outros métodos públicos que também alterem o mesmo atributo sem essa palavra-chave.
NotaTeste 7 — O Construtor como Base da Indução
  • □ Numa classe cujo único meio de criação para o mundo externo é um método estático de fábrica (Produto.criar(...)), que internamente chama um construtor private sem nenhuma validação, a responsabilidade de servir como “base” da indução passa a ser do método de fábrica, e não do construtor em si.
  • □ 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.
  • □ Numa classe totalmente imutável — todos os atributos final, sem nenhum método que altere o estado depois da construção —, o “passo indutivo” da prova por indução é trivialmente satisfeito, pois não existe nenhuma transição capaz de levar o objeto para fora do conjunto \(V\) depois do nascimento.
NotaTeste 8 — CQS: Comandos vs. Consultas
  • □ O construtor de uma classe, por ser responsável por estabelecer o estado inicial do objeto, deve ser classificado como um Comando dentro da disciplina de CQS, já que ele também determina o estado do objeto.
  • □ Um método como StringBuilder.append(String s), que muda o estado do objeto e também devolve a própria instância (return this;) para permitir encadeamento de chamadas (sb.append(a).append(b)), viola a recomendação estrita do CQS de que um Comando deve retornar void.
  • □ Uma Consulta que lança uma exceção quando chamada em condições inválidas — como Iterator.next(), que lança NoSuchElementException se chamado sem checar hasNext() antes — deixa de ser uma Consulta e passa a se comportar como um Comando, já que alterou o fluxo normal do programa.
  • □ Chamar uma Consulta livre de efeitos colaterais um milhão de vezes em sequência deixa o estado observável do objeto exatamente igual a como estava antes da primeira chamada.
NotaTeste 9 — Design by Contract
  • □ Um método que não tem nenhuma verificação de validade no início do seu corpo (nenhum if de guarda) ainda possui uma pré-condição no sentido do Design by Contract — ela apenas é a condição trivial “verdadeiro para qualquer entrada e qualquer estado”.
  • □ Se uma pré-condição falha, a responsabilidade recai sobre quem escreveu a classe, não sobre o chamador.
  • □ Um método processarSaque(double valor) que não contém nenhuma instrução assert no seu corpo não está sujeito a nenhuma pós-condição, dentro da disciplina de Design by Contract.
  • □ Num método transferir(ContaBancaria destino, double valor) que debita da própria conta e credita na conta destino, se ele não verifica que destino é diferente de this, e um chamador passa a própria conta como destino (contaA.transferir(contaA, 100)), quebrando a invariante de saldo, a culpa é do autor do método transferir, não de quem o chamou.
NotaTeste 10 — Fail-Fast e Exceções Padrão
  • □ Diferente de um código de erro -1 em C, uma exceção checked em Java (como IOException, que exige throws na assinatura do método) não pode ser silenciosamente ignorada pelo chamador, porque o compilador obriga o tratamento do erro.
  • IllegalArgumentException deve ser usada quando o objeto está no estado errado para a operação.
  • □ Se um método recebe, ao mesmo tempo, um argumento inválido (ex.: quantidade negativa) e é chamado num objeto que também está no estado errado para a operação (ex.: um carrinho já finalizado), a linguagem Java exige que a checagem de estado (IllegalStateException) seja sempre feita antes da checagem do argumento (IllegalArgumentException).
  • □ Chamar Objects.requireNonNull(cliente, "...") na última linha do corpo de um método, depois de todas as outras mutações de estado já terem sido executadas, cumpre igualmente bem o espírito do Fail-Fast, desde que a exceção ainda seja lançada antes do método retornar o controle ao chamador.
NotaTeste 11 — Identidade Física vs. Lógica
  • □ Para dois objetos Integer criados por autoboxing com o mesmo valor pequeno (Integer a = 100; Integer b = 100;), o operador == se comporta exatamente como no exemplo de Produto com new — sempre resulta em false, porque cada variável aponta para um objeto Integer diferente na Heap.
  • □ Ao contrário de comparar duas instâncias de Produto, comparar dois valores do tipo enum Estado (como no exemplo Pedido do início da aula) com == — por exemplo, this.status == Estado.PAGO — é seguro e dá o mesmo resultado que usar .equals(), porque a JVM garante que cada constante do enum existe como uma única instância na Heap.
  • □ Se um programador sobrescrever equals() de Produto para sempre devolver true, independentemente do objeto comparado, mas não sobrescrever hashCode() (mantendo o padrão herdado de Object, baseado no endereço), um HashSet<Produto> vai tratar dois Produto diferentes, adicionados nele, como duplicados.
  • □ Um TreeSet<Produto> configurado com um Comparator que ordena por SKU sofre exatamente do mesmo problema de aceitar “duplicatas” de negócio que um HashSet<Produto> sem equals()/hashCode() sobrescritos, pois ambas as coleções dependem exclusivamente de equals() para decidir se dois elementos são o mesmo.
NotaTeste 12 — O Contrato equals()/hashCode()
  • □ Pelo mesmo contrato de equals()/hashCode(), se dois objetos Produto têm o mesmo hashCode(), então equals() entre eles deve obrigatoriamente devolver true.
  • □ O hashCode() pode usar atributos completamente diferentes dos usados pelo equals(), sem risco.
  • □ Se um Produto com equals()/hashCode() mal implementados for usado apenas como VALOR (não como chave) dentro de um HashMap<Integer, Produto>, o mesmo risco de “desaparecimento silencioso” descrito na aula para chaves se aplica igualmente a ele.
  • □ IDEs modernas geram os dois métodos juntos justamente para preservar essa coerência.