Você não pode escrever um bom código sem getters.
A razão disso não é porque os getters não quebram o encapsulamento, eles o fazem. Não é porque os getters não tentam as pessoas a não se incomodarem em seguir o POO, o que os levaria a colocar métodos com os dados em que atuam. Eles fazem. Não, você precisa de getters por causa dos limites.
As idéias de encapsulamento e manutenção de métodos juntamente com os dados em que atuam simplesmente não funcionam quando você se depara com um limite que impede você de mover um método e, portanto, obriga a mover dados.
É realmente assim tão simples. Se você usa getters quando não há limites, acaba sem objetos reais. Tudo começa a tender para o procedimento. O que funciona tão bem quanto nunca.
OOP verdadeiro não é algo que você pode espalhar por toda parte. Funciona apenas dentro desses limites.
Esses limites não são muito finos. Eles têm código neles. Esse código não pode ser OOP. Também não pode ser funcional. Nenhum código tem nossos ideais retirados para que ele possa lidar com a dura realidade.
Michael Fetters chamou esse código de fáscia, depois daquele tecido conjuntivo branco que mantém seções de uma laranja juntas.
Esta é uma maneira maravilhosa de pensar sobre isso. Ele explica por que não há problema em ter os dois tipos de código na mesma base de código. Sem essa perspectiva, muitos novos programadores se apegam a seus ideais com força, então partem seus corações e desistem desses ideais quando atingem seu primeiro limite.
Os ideais funcionam apenas em seu devido lugar. Não desista deles só porque eles não funcionam em todos os lugares. Use-os onde eles trabalham. Esse lugar é a parte suculenta que a fáscia protege.
Um exemplo simples de limite é uma coleção. Isso contém algo e não faz ideia do que é. Como um designer de coleção poderia mover a funcionalidade comportamental do objeto em espera para a coleção quando não tem idéia do que ele estará segurando? Você não pode. Você está enfrentando um limite. É por isso que as coleções têm getters.
Agora, se você soubesse, poderia mudar esse comportamento e evitar o estado de mudança. Quando você sabe, você deveria. Você nem sempre sabe.
Algumas pessoas chamam isso de pragmático. E isso é. Mas é bom saber por que temos que ser pragmáticos.
Você expressou que não deseja ouvir argumentos semânticos e parece estar defendendo a colocação de "apanhadores sensatos" em todos os lugares. Você está pedindo que essa ideia seja contestada. Acho que posso mostrar que a ideia tem problemas com a maneira como você a estruturou. Mas também acho que sei de onde você vem, porque eu estive lá.
Se você quiser getters em todos os lugares, veja Python. Não há palavra-chave privada. No entanto, o Python faz POO muito bem. Quão? Eles usam um truque semântico. Eles nomeiam qualquer coisa que seja privada com um sublinhado principal. Você pode ler o conteúdo, desde que seja responsável por isso. "Somos todos adultos aqui", costumam dizer.
Então, qual é a diferença entre isso e apenas colocar getters em tudo em Java ou C #? Desculpe, mas é semântica. A convenção sublinhada de Pythons indica claramente a você que você está bisbilhotando atrás da única porta dos funcionários. Tapa getters em tudo e você perde esse sinal. Com a reflexão, você poderia ter retirado o privado de qualquer maneira e ainda não ter perdido o sinal semântico. Simplesmente não há um argumento estrutural a ser feito aqui.
Então, o que nos resta é o trabalho de decidir onde pendurar a placa "apenas funcionários". O que deve ser considerado privado? Você chama isso de "receptores sensatos". Como eu disse, a melhor justificativa para um getter é um limite que nos afasta de nossos ideais. Isso não deve resultar em getters sobre tudo. Quando isso resulta em um getter, você deve considerar mover o comportamento ainda mais para a parte interessante, onde você pode protegê-lo.
Essa separação deu origem a alguns termos. Um objeto de transferência de dados ou DTO não possui comportamento. Os únicos métodos são getters e às vezes setters, às vezes um construtor. Esse nome é lamentável, porque não é um objeto verdadeiro. Os getters e setters são realmente apenas códigos de depuração que oferecem um local para definir um ponto de interrupção. Se não fosse por essa necessidade, eles seriam apenas uma pilha de campos públicos. Em C ++, costumávamos chamá-los de estruturas. A única diferença que eles tinham de uma classe C ++ era o padrão para o público.
Os DTOs são bons porque você pode jogá-los sobre um muro de fronteira e manter seus outros métodos com segurança em um bom objeto de comportamento interessante. Um verdadeiro objeto. Sem getters para violar, é encapsulamento. Meus objetos de comportamento podem comer DTOs usando-os como Objetos de Parâmetro . Às vezes, tenho que fazer uma cópia defensiva para impedir o estado mutável compartilhado . Eu não espalhe DTOs mutáveis por dentro da parte suculenta dentro dos limites. Eu os encapsulo. Eu os escondo. E quando finalmente encontro um novo limite, lancei um novo DTO e o joguei por cima do muro, tornando-o o problema de outra pessoa.
Mas você deseja fornecer getters que expressam identidade. Bem, parabéns, você encontrou um limite. As entidades têm uma identidade que vai além de sua referência. Ou seja, além do endereço de memória. Portanto, tem que ser armazenado em algum lugar. E algo tem que ser capaz de se referir a isso por sua identidade. Uma pessoa que expressa identidade é perfeitamente razoável. Uma pilha de código que usa esse getter para tomar decisões que a Entidade poderia ter tomado por si mesma não é.
No final, não é a existência de getters que está errada. Eles são muito melhores que os campos públicos. O que é ruim é quando eles são usados para fingir que você está sendo orientado a objetos quando não está. Getters são bons. Ser orientado a objetos é bom. Os getters não são orientados a objetos. Use getters para criar um local seguro para se orientar a objetos.