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)?
object-oriented
language-agnostic
encapsulation
Botond Balázs
fonte
fonte
Respostas:
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:
e
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:
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:
Isso diz alguma coisa sobre o que vai acontecer com o valor quando é dado ao levantador?
E se:
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.
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.
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.
Em casos muito raros, talvez. Em geral, provavelmente é melhor não. Esse é o tipo de coisa que é melhor deixar para outro método.
fonte
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.
fonte
IEnumerable<T>
que forçá-la a algo comoList<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.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.
fonte
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
UPDATE
em 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
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.
fonte
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
center
propriedade, mesmo que não haja um ivar que armazene o centro da exibição; A configuraçãocenter
causa alterações em outros ivars, comoorigin
ou o quetransform
quer que seja, mas os clientes da classe não sabem nem se importam com ocenter
armazenamento, desde que funcione corretamente. O que não deveria acontecer, porém, é que a configuraçãocenter
faz com que as coisas aconteçam além do necessário para salvar o novo valor, por mais que seja feito.fonte
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.
fonte
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.
fonte