Um getter deve lançar uma exceção se seu objeto tiver um estado inválido?

55

Costumo encontrar esse problema, especialmente em Java, mesmo que eu ache que seja um problema geral de POO. Ou seja: criar uma exceção revela um problema de design.

Suponha que eu tenha uma classe que tenha um String namecampo e um String surnamecampo.

Em seguida, ele usa esses campos para compor o nome completo de uma pessoa, a fim de exibi-lo em algum tipo de documento, digamos uma fatura.

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

Agora, quero reforçar o comportamento da minha classe lançando um erro se displayCompleteNameOnInvoicefor chamado antes que o nome seja atribuído. Parece uma boa ideia, não é?

Eu posso adicionar um código de exceção ao getCompleteNamemétodo. Mas dessa maneira estou violando um contrato "implícito" com o usuário da classe; em geral, os getters não devem lançar exceções se seus valores não estiverem definidos. Ok, este não é um getter padrão, pois não retorna um único campo, mas, do ponto de vista do usuário, a distinção pode ser sutil demais para pensar sobre isso.

Ou posso lançar a exceção de dentro do displayCompleteNameOnInvoice. Mas, para fazer isso, eu deveria testar diretamente os campos nameou surname, violando a abstração representada por getCompleteName. É responsabilidade deste método verificar e criar o nome completo. Pode até decidir, baseando a decisão em outros dados, que, em alguns casos, é suficiente surname.

Portanto, a única possibilidade parece alterar a semântica do método getCompleteNamepara composeCompleteName, o que sugere um comportamento mais "ativo" e, com ele, a capacidade de gerar uma exceção.

Essa é a melhor solução de design? Estou sempre procurando o melhor equilíbrio entre simplicidade e correção. Existe uma referência de design para esse problema?

AgostinoX
fonte
8
"em geral, os getters não devem lançar exceções se seus valores não estiverem definidos." - O que eles deveriam fazer então?
Rotem
2
@scriptin Então, seguindo essa lógica, displayCompleteNameOnInvoicebasta lançar uma exceção se getCompleteNameretornar null, não é?
Rotem
2
@ Rotem pode. A questão é se deveria.
Script #
22
Sobre o assunto, os programadores de falsidades acreditam em nomes são uma leitura muito boa sobre por que 'primeiro nome' e 'sobrenome' são conceitos muito restritos.
Lars Viklund #
9
Se o seu objeto pode ter um estado inválido, você já está errado.
precisa saber é o seguinte

Respostas:

151

Não permita que sua turma seja construída sem atribuir um nome.

DeadMG
fonte
9
É uma solução interessante, mas bastante radical. Muitas vezes, suas aulas precisam ser mais fluidas, permitindo que os estados se tornem um problema apenas se e quando certos métodos forem chamados; caso contrário, você terminará com uma proliferação de classes pequenas que exigem muita explicação para serem usadas.
AgostinoX
71
Este não é um comentário. Se ele aplicar o conselho na resposta, ele não terá mais o problema descrito. É uma resposta.
58580 DeadMG #
14
@AgostinoX: Você só deve tornar suas aulas fluidas se for absolutamente necessário. Caso contrário, eles devem ser o mais invariáveis ​​possível.
58580 DeadMG #
25
@gnat: você faz um ótimo trabalho aqui, questionando a qualidade de todas as perguntas e respostas no PSE, mas às vezes você é IMHO um pouco confuso demais.
Doc Brown)
9
Se eles podem ser validamente opcionais, não há razão para ele jogar em primeiro lugar.
DeadMG
75

A resposta fornecida pelo DeadMG praticamente se destaca, mas deixe-me expressá-la de maneira um pouco diferente: evite ter objetos com estado inválido. Um objeto deve ser "inteiro" no contexto da tarefa que ele realiza. Ou não deveria existir.

Portanto, se você quiser fluidez, use algo como o Padrão do Construtor . Ou qualquer outra coisa que seja uma reificação separada de um objeto em construção, em oposição à "coisa real", onde é garantido que este último possui um estado válido e é aquele que realmente expõe as operações definidas nesse estado (por exemplo getCompleteName).

back2dos
fonte
7
So if you want fluidity, use something like the builder pattern- fluidez ou imutabilidade (a menos que seja isso que você queira dizer). Se alguém deseja criar objetos imutáveis, mas precisa adiar a definição de alguns valores, o padrão a seguir é defini-los um por um em um objeto construtor e criar apenas um imutável quando reunirmos todos os valores necessários para a correta estado ...
Konrad Morawski
11
@KonradMorawski: Estou confuso. Você está esclarecendo ou contradizendo minha afirmação? ;)
back2dos
3
Tentei complementá-lo ...
Konrad Morawski
40

Não tem um objeto com um estado inválido.

O objetivo de um construtor ou construtor é definir o estado do objeto como algo consistente e utilizável. Sem essa garantia, surgem problemas com o estado inicializado incorretamente nos objetos. Esse problema se agrava se você estiver lidando com simultaneidade e algo puder acessar o objeto antes de concluir a configuração.

Portanto, a pergunta para esta parte é "por que você está permitindo que o nome ou sobrenome seja nulo?" Se isso não é algo válido na classe para que funcione corretamente, não permita. Tenha um construtor ou construtor que valide adequadamente a criação do objeto e, se não estiver certo, levante o problema nesse ponto. Você também pode usar uma das @NotNullanotações existentes para ajudar a comunicar que "isso não pode ser nulo" e aplicá-la na codificação e análise.

Com essa garantia, fica muito mais fácil argumentar sobre o código, o que ele faz e não precisar lançar exceções em locais estranhos ou fazer verificações excessivas nas funções getter.

Getters que fazem mais.

Há um pouco por aí sobre esse assunto. Você tem Quanto Logic em Getters e que deve ser permitido dentro getters e setters? daqui e getters e setters executando lógica adicional no Stack Overflow. Esse é um problema que surge repetidamente no design da classe.

O núcleo disso vem de:

em geral, os getters não devem lançar exceções se seus valores não estiverem definidos

E você está certo. Eles não deveriam. Eles devem parecer "burros" para o resto do mundo e fazer o que é esperado. Colocar muita lógica aí leva a questões em que a lei de menor espanto é violada. E vamos ser sinceros, você realmente não quer envolver os getters com um try catchporque sabemos o quanto amamos fazer isso em geral.

Também existem situações em que você deve usar um getFoométodo como a estrutura JavaBeans e quando você tem algo da chamada EL esperando obter um bean (para que <% bar.foo %>realmente chame getFoo()na classe - deixando de lado o 'caso o bean esteja fazendo a composição ou deveria que são deixados à vista? ', porque é possível chegar facilmente a situações em que um ou outro pode ser claramente a resposta certa)

Perceba também que é possível que um determinado getter seja especificado em uma interface ou faça parte da API pública exposta anteriormente para a classe que está sendo refatorada (uma versão anterior tinha apenas 'completeName' que foi retornado e uma refatoração foi interrompida em dois campos).

No fim do dia...

Faça o que é mais fácil de raciocinar. Você gastará mais tempo mantendo esse código do que gastando projetando-o (embora, quanto menos tempo você gaste projetando, mais tempo você gastará mantendo). A escolha ideal é projetá-lo de tal maneira que não demore muito tempo para mantê-lo, mas também não fique lá pensando por dias - a menos que essa seja realmente uma escolha de design que terá dias de implicações posteriormente .

Tentar manter a pureza semântica do ser getter private T foo; T getFoo() { return foo; }causará problemas em algum momento. Às vezes, o código simplesmente não se encaixa nesse modelo e as contorções que se passam para tentar mantê-lo dessa maneira simplesmente não fazem sentido ... e, finalmente, tornam mais difícil o design.

Às vezes, aceite que os ideais do design não podem ser realizados da maneira que você deseja no código. Diretrizes são diretrizes - não grilhões.

Comunidade
fonte
2
Obviamente, meu exemplo foi apenas uma desculpa para melhorar o estilo de codificação, fundamentando-o, não um problema de bloqueio. Mas estou bastante perplexo com a importância que você e outros parecem dar aos construtores. Em teoria, eles cumprem sua promessa. Na prática, eles se tornam difíceis de manter com herança, não utilizáveis ​​quando você extrai uma interface de uma classe, não adotada por javabeans e outros frameworks como spring, se não estiver errado. Sob o guarda-chuva de uma idéia de 'classe', viva muitas categorias diferentes de objetos. Um objeto de valor pode muito bem ter um construtor de validação, mas esse é apenas um tipo.
AgostinoX
2
Ser capaz de raciocinar sobre o código é essencial para escrever código usando uma API ou manter o código. Se uma classe tem um construtor que permite que ela esteja em um estado inválido, fica muito mais difícil pensar em "bem, o que isso faz?" porque agora você precisa considerar mais situações do que o projetista da classe considerada ou pretendida. Essa carga conceitual extra vem com a penalidade de mais bugs e mais tempo para escrever o código. Por outro lado, se você pode olhar o código e dizer "nome e sobrenome nunca serão nulos", fica muito mais fácil escrever código usando-os.
2
Incompleto não significa necessariamente inválido. Uma fatura sem um recibo pode estar em andamento, e a API pública que ela apresenta refletiria que esse é um estado válido. Se surnameou nameser nulo for um estado válido para o objeto, trate-o de acordo. Mas se não for um estado válido - fazendo com que os métodos dentro da classe gerem exceções, deve-se evitar que ele esteja nesse estado a partir do ponto em que foi inicializado. Você pode passar objetos incompletos, mas passar objetos inválidos é problemático. Se um sobrenome nulo for excepcional, tente evitá-lo mais cedo ou mais tarde.
2
Uma postagem de blog relacionada para ler: Designing Object Initialization e observe as quatro abordagens para inicializar uma variável de instância. Uma delas é permitir que ela esteja em um estado inválido, embora observe que isso gera uma exceção. Se você está tentando evitar isso, essa abordagem não é válida e precisará trabalhar com as outras abordagens - que envolvem garantir um objeto válido o tempo todo. No blog: "Objetos que podem ter estados inválidos são mais difíceis de usar e mais difíceis de entender do que aqueles que sempre são válidos". e isso é fundamental.
5
"Faça o que é mais fácil de raciocinar." Isso
Ben
5

Eu não sou um cara de Java, mas isso parece aderir às duas restrições que você apresentou.

getCompleteNamenão gera uma exceção se os nomes não forem inicializados e displayCompleteNameOnInvoicegera.

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}
Rotem
fonte
Para expandir por que essa é uma solução correta: Dessa forma, o responsável pela chamada getCompleteName()não precisa se preocupar se a propriedade está realmente armazenada ou se é computada em tempo real.
Bart van Ingen Schenau 02/11/2014
18
Para expandir o motivo pelo qual essa solução é insana, é necessário verificar a nulidade em todas as chamadas getCompleteName, o que é hediondo.
58580 DeadMG #
Não, @DeadMG, você só precisa verificá-la nos casos em que uma exceção deve ser lançada se getCompleteName retornar nulo. Qual é o caminho que deve ser. Esta solução está correta.
Dawood diz que restabelece Monica
11
@DeadMG Sim, mas não sabemos do que OUTROS usos existem getCompleteName()no restante da classe ou mesmo em outras partes do programa. Em alguns casos, o retorno nullpode ser exatamente o que é necessário. Porém, embora exista UM método cuja especificação seja "lance uma exceção neste caso", é EXATAMENTE o que devemos codificar.
Dawood diz que restabelece Monica
4
Pode haver situações em que essa é a melhor solução, mas não consigo imaginar nenhuma. Na maioria dos casos, isso parece muito complicado: um método lança com dados incompletos, outro retorna nulo. Receio que seja provável que isso seja usado incorretamente pelos chamadores.
sleske
4

Parece que ninguém está respondendo sua pergunta.
Ao contrário do que as pessoas gostam de acreditar, "evitar objetos inválidos" não costuma ser uma solução prática.

A resposta é:

Sim deveria.

Exemplo fácil: FileStream.Lengthem C # /. NET , que lança NotSupportedExceptionse o arquivo não é procurável.

Mehrdad
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
maple_shaft
1

Você tem todo o nome do nome de cabeça para baixo.

getCompleteName()não precisa saber quais funções o utilizarão. Portanto, não ter um namequando ligar displayCompleteNameOnInvoice()não getCompleteName()é responsabilidade.

Mas, nameé um pré-requisito claro para displayCompleteNameOnInvoice(). Portanto, deve assumir a responsabilidade de verificar o estado (ou delegar a responsabilidade de verificar para outro método, se você preferir).

sampathsris
fonte