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.