Exercícios — O Objeto como Máquina de Estados
Aula 2 — Programação Orientada a Objetos
Marcos M. Raimundo — Instituto de Computação, UNICAMP
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.
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).
- □ 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 atributoprivatediretamente, 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.
- □ Em C, uma
structagrupa dados e as funções que operam sobre eles na mesma unidade. - □ Se a função que manipula uma
structem C for declaradastaticno 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
privatepara 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 comopagar()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.
- □ 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 finalbuild(), 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.
- □ 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
Produtoapresentado na aula, chamarvender(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.
- □ Um atributo declarado
staticnuma 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 atributoprivatebem encapsulado, já que a lista nunca é declaradapublic. - □ Um método público
restaurarEstado(EstadoSerializado dump), que copia todos os campos de um objeto externodumpdiretamente para os atributosprivatedo 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).
- □ 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
ContaBancariacujo atributosaldoé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
synchronizeddo 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.
- □ 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 construtorprivatesem 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.
- □ 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 retornarvoid. - □ Uma Consulta que lança uma exceção quando chamada em condições inválidas — como
Iterator.next(), que lançaNoSuchElementExceptionse chamado sem checarhasNext()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.
- □ Um método que não tem nenhuma verificação de validade no início do seu corpo (nenhum
ifde 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çãoassertno 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 contadestino, se ele não verifica quedestinoé diferente dethis, 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étodotransferir, não de quem o chamou.
- □ Diferente de um código de erro
-1em C, uma exceção checked em Java (comoIOException, que exigethrowsna assinatura do método) não pode ser silenciosamente ignorada pelo chamador, porque o compilador obriga o tratamento do erro. - □
IllegalArgumentExceptiondeve 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.
- □ Para dois objetos
Integercriados por autoboxing com o mesmo valor pequeno (Integer a = 100; Integer b = 100;), o operador==se comporta exatamente como no exemplo deProdutocomnew— sempre resulta emfalse, porque cada variável aponta para um objetoIntegerdiferente na Heap. - □ Ao contrário de comparar duas instâncias de
Produto, comparar dois valores do tipoenum Estado(como no exemploPedidodo 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()deProdutopara sempre devolvertrue, independentemente do objeto comparado, mas não sobrescreverhashCode()(mantendo o padrão herdado deObject, baseado no endereço), umHashSet<Produto>vai tratar doisProdutodiferentes, adicionados nele, como duplicados. - □ Um
TreeSet<Produto>configurado com umComparatorque ordena por SKU sofre exatamente do mesmo problema de aceitar “duplicatas” de negócio que umHashSet<Produto>semequals()/hashCode()sobrescritos, pois ambas as coleções dependem exclusivamente deequals()para decidir se dois elementos são o mesmo.
- □ Pelo mesmo contrato de
equals()/hashCode(), se dois objetosProdutotêm o mesmohashCode(), entãoequals()entre eles deve obrigatoriamente devolvertrue. - □ O
hashCode()pode usar atributos completamente diferentes dos usados peloequals(), sem risco. - □ Se um
Produtocomequals()/hashCode()mal implementados for usado apenas como VALOR (não como chave) dentro de umHashMap<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.