O que deve ser permitido dentro de getters e setters?

45

Entrei em uma discussão interessante na Internet sobre métodos e encapsulamento getter e setter. Alguém disse que tudo o que eles deveriam fazer é uma atribuição (setters) ou um acesso variável (getters) para mantê-los "puros" e garantir o encapsulamento.

  • Estou certo de que isso anularia completamente o propósito de ter getters e setters em primeiro lugar e validação e outras lógicas (sem efeitos colaterais estranhos, é claro) deveriam ser permitidas?
  • Quando a validação deve acontecer?
    • Ao definir o valor, dentro do setter (para proteger o objeto de entrar em um estado inválido - minha opinião)
    • Antes de definir o valor, fora do setter
    • Dentro do objeto, antes de cada vez que o valor é usado
  • É permitido a um setter alterar o valor (talvez converter um valor válido em alguma representação interna canônica)?
Botond Balázs
fonte
18
A melhor coisa sobre getters e setters é a capacidade de não tê-los , ou seja, sair do setter e você tem uma propriedade somente leitura, deixar o getter e você tem a opção de configuração cujo valor atual não é da conta de ninguém.
precisa
7
@MichaelBorgwardt Deixe os dois de fora para ter uma interface limpa "diga, não pergunte". Propriedades = cheiro potencial do código.
Konrad Rudolph
2
Os setters também podem ser usados ​​para criar um evento .
John Isaiah Carmona
@KonradRudolph Concordo com sua afirmação, embora eu realmente queira enfatizar a palavra "potencial".
26412 Phil

Respostas:

37

Lembro-me de ter uma discussão semelhante com meu professor quando aprendi C ++ na universidade. Eu simplesmente não conseguia entender o motivo de usar getters e setters quando eu poderia tornar uma variável pública. Agora entendo melhor com anos de experiência e aprendi uma razão melhor do que simplesmente dizer "manter o encapsulamento".

Ao definir os getters e setters, você fornecerá uma interface consistente para que, se desejar alterar sua implementação, seja menos provável que você quebre o código dependente. Isso é especialmente importante quando as aulas são expostas por meio de uma API e usadas em outros aplicativos ou por terceiros. Então, e as coisas que entram no caçador ou caçador?

Geralmente, os getters são melhor implementados como uma passagem simplificada para acessar um valor, pois isso torna seu comportamento previsível. Eu digo geralmente, porque vi casos em que getters foram usados ​​para acessar valores manipulados por cálculo ou mesmo por código condicional. Geralmente não é tão bom se você estiver criando componentes visuais para uso em tempo de design, mas aparentemente útil em tempo de execução. Entretanto, não há diferença real entre isso e o uso de um método simples, exceto que, quando você usa um método, geralmente é mais provável que você nomeie um método de maneira mais apropriada, para que a funcionalidade do "getter" seja mais aparente ao ler o código.

Compare o seguinte:

int aValue = MyClass.Value;

e

int aValue = MyClass.CalculateValue();

A segunda opção deixa claro que o valor está sendo calculado, enquanto o primeiro exemplo informa que você está simplesmente retornando um valor sem saber nada sobre o valor em si.

Talvez você possa argumentar que o seguinte seria mais claro:

int aValue = MyClass.CalculatedValue;

O problema, porém, é que você está assumindo que o valor já foi manipulado em outro lugar. Portanto, no caso de um getter, embora você possa supor que algo mais possa estar acontecendo quando você retornar um valor, é difícil deixar essas coisas claras no contexto de uma propriedade, e os nomes das propriedades nunca devem conter verbos caso contrário, fica difícil entender rapidamente se o nome usado deve ser decorado com parênteses quando acessado.

Os setters são um caso ligeiramente diferente, no entanto. É inteiramente apropriado que um setter forneça algum processamento adicional para validar os dados que estão sendo enviados a uma propriedade, lançando uma exceção se a definição de um valor violar os limites definidos da propriedade. O problema que alguns desenvolvedores têm ao adicionar processamento aos setters, no entanto, é que sempre há uma tentação de fazer com que o setter faça um pouco mais, como executar um cálculo ou uma manipulação dos dados de alguma maneira. É aqui que você pode obter efeitos colaterais que, em alguns casos, podem ser imprevisíveis ou indesejáveis.

No caso de setters, eu sempre aplico uma regra simples, que é fazer o mínimo possível aos dados. Por exemplo, geralmente permitirei o teste de limite e o arredondamento para que eu possa criar exceções, se apropriado, ou evitar exceções desnecessárias, onde possam ser sensatamente evitadas. As propriedades de ponto flutuante são um bom exemplo em que você pode desejar arredondar casas decimais excessivas para evitar gerar uma exceção, enquanto ainda permite que os valores do intervalo sejam inseridos com algumas casas decimais adicionais.

Se você aplicar algum tipo de manipulação da entrada do setter, terá o mesmo problema do getter, que é difícil permitir que outras pessoas saibam o que o setter está fazendo simplesmente nomeando-o. Por exemplo:

MyClass.Value = 12345;

Isso diz alguma coisa sobre o que vai acontecer com o valor quando é dado ao levantador?

E se:

MyClass.RoundValueToNearestThousand(12345);

O segundo exemplo informa exatamente o que vai acontecer com seus dados, enquanto o primeiro não informa se o valor será modificado arbitrariamente. Ao ler o código, o segundo exemplo será muito mais claro em seu propósito e função.

Estou certo de que isso anularia completamente o propósito de ter getters e setters em primeiro lugar e validação e outras lógicas (sem efeitos colaterais estranhos, é claro) deveriam ser permitidas?

Ter getters e setters não é sobre encapsulamento por uma questão de "pureza", mas sobre encapsular para permitir que o código seja facilmente refatorado sem arriscar uma alteração na interface da classe que, de outra forma, quebraria a compatibilidade da classe com o código de chamada. A validação é totalmente apropriada em um setter, no entanto, existe um pequeno risco de que uma alteração na validação possa quebrar a compatibilidade com o código de chamada se o código de chamada se basear na validação que ocorre de uma maneira específica. Essa é uma situação geralmente rara e de risco relativamente baixo, mas deve ser observada por uma questão de integridade.

Quando a validação deve acontecer?

A validação deve ocorrer dentro do contexto do setter antes de realmente definir o valor. Isso garante que, se uma exceção for lançada, o estado do seu objeto não será alterado e potencialmente invalidará seus dados. Geralmente, acho melhor delegar a validação a um método separado, que seria a primeira coisa chamada no setter, a fim de manter o código do setter relativamente organizado.

É permitido a um setter alterar o valor (talvez converter um valor válido em alguma representação interna canônica)?

Em casos muito raros, talvez. Em geral, provavelmente é melhor não. Esse é o tipo de coisa que é melhor deixar para outro método.

S.Robins
fonte
re: altere o valor, pode ser razoável defini-lo como -1 ou algum token de flag NULL se o setter receber um valor ilegal
Martin Beckett
1
Existem alguns problemas com isso. A definição arbitrária de um valor cria um efeito colateral deliberado e pouco claro. Além disso, ele não permite que o código de chamada receba feedback que poderia ser usado para lidar melhor com dados ilegais. Isso é particularmente importante com valores no nível da interface do usuário. Para ser justo, porém, uma exceção que eu pensei poderia ser se permitisse vários formatos de data como entrada, enquanto armazenasse a data em um formato de data / hora padrão. Pode-se argumentar que esse "efeito colateral" em particular é uma normalização dos dados na validação, desde que os dados de entrada sejam legais.
S.Robins
Sim, uma das justificativas de um setter é que ele deve retornar verdadeiro / falso se o valor puder ser definido.
Martin Beckett
1
"As propriedades de ponto flutuante são um bom exemplo em que você pode desejar arredondar casas decimais excessivas para evitar gerar uma exceção" Em que circunstância ter muitas casas decimais gera uma exceção?
Mark263:
2
Vejo alterar o valor para uma representação canônica como uma forma de validação. A falta de valores padrão (por exemplo, a data atual se um carimbo de data / hora contiver apenas a hora) ou a escalabilidade de um valor (0,93275 -> 93,28%) para garantir que a consistência interna seja boa, mas essas manipulações devem ser explicitamente mencionadas na documentação da API , especialmente se eles forem um idioma incomum na API.
TMN
20

Se o getter / setter simplesmente espelha o valor, não faz sentido tê-los ou tornar o valor privado. Não há nada errado em tornar públicas algumas variáveis ​​de membro se você tiver um bom motivo. Se você estiver escrevendo uma classe de pontos 3d, ter o público .x, .y, .z faz todo sentido.

Como Ralph Waldo Emerson disse: "Uma consistência tola é o duende de mentes pequenas, adorada por pequenos estadistas, filósofos e designers de Java".

Os getters / setters são úteis onde podem haver efeitos colaterais, nos quais é necessário atualizar outras variáveis ​​internas, recalcular valores em cache e proteger a classe de entradas inválidas.

A justificativa usual para eles, de que ocultam a estrutura interna, é geralmente a menos útil. por exemplo. Eu tenho esses pontos armazenados como 3 carros alegóricos, posso decidir armazená-los como seqüências de caracteres em um banco de dados remoto, para que os getters / setters os ocultem, como se você pudesse fazer isso sem ter nenhum outro efeito no código do chamador.

Martin Beckett
fonte
1
Uau, os designers de Java são divinos? </jk>
@delnan - obviamente não - ou eles iriam rima melhor ;-)
Martin Beckett
Seria válido criar um ponto 3d no qual x, y, z são todos Float.NAN?
Andrew T Finnell
4
Não concordo com o seu argumento de que "a justificativa usual" (ocultar a estrutura interna) é a propriedade menos útil para getters e setters. Em C #, é definitivamente útil tornar a propriedade uma interface cuja estrutura subjacente pode ser alterada - por exemplo, se você deseja apenas uma propriedade disponível para enumeração, é muito melhor dizer do IEnumerable<T>que forçá-la a algo como List<T>. Seu exemplo de acesso ao banco de dados, eu diria, está violando a responsabilidade única - misturando a representação do modelo com a forma como ele é persistido.
Brandon Linton
@AndrewFinnell - que poderia ser uma boa maneira de sinalizar pontos como impossível / inválido / Suprimido etc
Martin Beckett
8

Princípio de acesso uniforme de Meyer: "Todos os serviços oferecidos por um módulo devem estar disponíveis por meio de uma notação uniforme, que não trai se eles foram implementados por meio de armazenamento ou computação". é a principal razão por trás dos getters / setters, também conhecidos como Propriedades.

Se você decidir armazenar em cache ou calcular preguiçosamente um campo de uma classe, poderá alterá-lo a qualquer momento se tiver exposto apenas acessadores de propriedades e não os dados concretos.

Objetos de valor, estruturas simples não precisam dessa abstração, mas uma classe de pleno direito, na minha opinião.

Karl
fonte
2

Uma estratégia comum para o design de classes, introduzida pela linguagem Eiffel, é a Separação de Consulta de Comando . A idéia é que um método deve quer dizer algo sobre o objeto ou dizer o objeto para fazer algo, mas não para fazer ambos.

Isso está relacionado apenas à interface pública da classe, não à representação interna. Considere um objeto de modelo de dados apoiado por uma linha no banco de dados. Você pode criar o objeto sem carregar os dados e, na primeira vez em que chama um getter, ele realmente faz o SELECT. Tudo bem, você pode estar alterando alguns detalhes internos sobre como o objeto é representado, mas não está alterando a aparência dos clientes desse objeto. Você deve poder chamar getters várias vezes e ainda obter os mesmos resultados, mesmo se eles fizerem um trabalho diferente para retornar esses resultados.

Da mesma forma, um levantador parece, contratualmente, apenas mudar o estado de um objeto. Isso pode ser feito de alguma maneira complicada - escrevendo um UPDATEem um banco de dados ou passando o parâmetro para algum objeto interno. Tudo bem, mas fazer algo não relacionado à definição do estado seria surpreendente.

Meyer (o criador de Eiffel) também tinha coisas a dizer sobre validação. Essencialmente, sempre que um objeto está inativo, ele deve estar em um estado válido. Portanto, logo após a conclusão do construtor, antes e depois (mas não necessariamente durante) de toda chamada de método externo, o estado do objeto deve ser consistente.

É interessante notar que, nesse idioma, a sintaxe para chamar um procedimento e para ler um atributo exposto parece a mesma. Em outras palavras, um chamador não pode dizer se está usando algum método ou se está trabalhando diretamente com uma variável de instância. É apenas em idiomas que não ocultam esses detalhes de implementação que a pergunta ainda se coloca - se os chamadores não souberem, você pode alternar entre um ivar público e os acessadores sem vazar essa alteração no código do cliente.


fonte
1

Outra coisa aceitável a se fazer é a clonagem. Em alguns casos, você precisa ter certeza de que, após alguém dar a sua aula, por exemplo. uma lista de algo, ele não pode alterá-lo dentro da sua classe (nem alterar o objeto nessa lista). Portanto, você faz uma cópia profunda do parâmetro no setter e retorna uma cópia profunda no getter. (Usar tipos imutáveis ​​como parâmetros é outra opção, mas acima pressupõe-se que não é possível) Mas não clone nos acessadores se não for necessário. É fácil pensar (mas não apropriadamente) sobre apropriações / levantadores e levantadores como operações de custo constante; portanto, este é o desempenho das minas terrestres à espera do usuário da API.

user470365
fonte
1

Nem sempre existe um mapeamento 1-1 entre os acessadores de propriedades e os ivars que armazenam os dados.

Por exemplo, uma classe de exibição pode fornecer uma centerpropriedade, mesmo que não haja um ivar que armazene o centro da exibição; A configuração centercausa alterações em outros ivars, como originou o que transformquer que seja, mas os clientes da classe não sabem nem se importam com o center armazenamento, desde que funcione corretamente. O que não deveria acontecer, porém, é que a configuração centerfaz com que as coisas aconteçam além do necessário para salvar o novo valor, por mais que seja feito.

Caleb
fonte
0

A melhor parte dos setters e getters é que eles facilitam a alteração das regras de uma API sem alterar a API. Se você detectar um bug, é muito mais provável que você possa corrigi-lo na biblioteca e nem todos os consumidores atualizem sua base de códigos.

AlexanderBrevig
fonte
-4

Costumo acreditar que setters e getters são maus e só devem ser usados ​​em classes gerenciadas por um framework / container. Um design de classe adequado não deve gerar getters e setters.

Edit: um artigo bem escrito sobre este assunto .

Edit2: campos públicos são um absurdo em uma abordagem OOP; ao dizer que os caçadores e caçadores são maus, não quero dizer que devam ser substituídos por campos públicos.

m3th0dman
fonte
1
Acho que você está perdendo o objetivo do artigo, que sugere o uso de getters / setters com moderação e para evitar a exposição de dados de classe, a menos que seja necessário. Este IMHO é um princípio de design sensato que deve ser aplicado deliberadamente pelo desenvolvedor. Em termos de criação de propriedades em uma classe, você pode simplesmente expor uma variável, mas isso torna mais difícil fornecer validação quando a variável é definida ou extrair o valor de uma fonte alternativa quando "got", violando essencialmente o encapsulamento e potencialmente bloqueando você em um design de interface difícil de manter.
S.Robins
@ S.Robins Na minha opinião, getters e setters públicos estão errados; campos públicos estão mais errados (se isso for comparável).
M3th0dman
@methodman Sim, eu concordo que um campo público está errado, no entanto, uma propriedade pública pode ser útil. Essa propriedade pode ser usada para fornecer um local para validação ou eventos relacionados à configuração ou retorno dos dados, dependendo do requisito específico no momento. Os próprios getters e setters não estão errados por si só. Por outro lado, como e quando são usados ​​ou abusados ​​pode ser visto como ruim em termos de design e manutenção, dependendo das circunstâncias. :)
S.Robins
@ S.Robins Pense nos conceitos básicos de OOP e modelagem; objetos devem copiar entidades da vida real. Existem entidades da vida real que possuem esses tipos de operações / propriedades, como getters / setters?
M3th0dman 27/11/2012
Para evitar muitos comentários aqui, levarei essa conversa para este bate-papo e abordarei seu comentário lá.
27512 Novell