Meus colegas de trabalho me dizem que deve haver o mínimo de lógica possível em getters e setters.
No entanto, estou convencido de que muitas coisas podem ser ocultadas nos getters e setters para proteger os usuários / programadores dos detalhes da implementação.
Um exemplo do que faço:
public List<Stuff> getStuff()
{
if (stuff == null || cacheInvalid())
{
stuff = getStuffFromDatabase();
}
return stuff;
}
Um exemplo de como o trabalho me diz para fazer as coisas (elas citam 'Código Limpo' do tio Bob):
public List<Stuff> getStuff()
{
return stuff;
}
public void loadStuff()
{
stuff = getStuffFromDatabase();
}
Quanta lógica é apropriada em um setter / getter? Qual é a utilidade de getters e setters vazios, exceto uma violação da ocultação de dados?
public List<Stuff> getStuff() { return stuff; }
StuffGetter
interface, implemente umaStuffComputer
que faça os cálculos e envolva-a em um objeto deStuffCacher
, responsável por acessar o cache ou encaminhar chamadas para oStuffComputer
que ele envolve.Respostas:
A maneira como o trabalho diz para você fazer as coisas é esfarrapada.
Como regra geral, a maneira como eu faço as coisas é a seguinte: se pegar as coisas é computacionalmente barato ((ou se é mais provável que seja encontrado no cache)), então seu estilo de getStuff () é bom. Se conseguir que as coisas sejam conhecidas por serem computacionalmente caras, tão caras que a publicidade de seu custo é necessária na interface, então eu não chamaria de getStuff (), chamaria de calculStuff () ou algo assim, para indicar que haverá algum trabalho a fazer.
Em qualquer um dos casos, a maneira como o trabalho diz para você fazer as coisas é esfarrapada, porque getStuff () explodirá se loadStuff () não tiver sido chamado com antecedência, então eles essencialmente querem que você complique sua interface introduzindo a complexidade da ordem de operações para isso. A ordem das operações é praticamente o pior tipo de complexidade em que consigo pensar.
fonte
loadStuff()
função seja chamada antes dagetStuff()
função também significa que a classe não está abstraindo adequadamente o que está acontecendo sob o capô.A lógica nos getters está perfeitamente bem.
Mas obter dados de um banco de dados é muito mais que "lógica". Envolve uma série de operações muito caras, nas quais muitas coisas podem dar errado e de maneira não determinística. Eu hesitaria em fazê-lo implicitamente em um caso.
Por outro lado, a maioria dos ORMs suporta o carregamento lento de coleções, que é basicamente exatamente o que você está fazendo.
fonte
Eu acho que, de acordo com o 'Código Limpo', ele deve ser dividido o máximo possível, em algo como:
Obviamente, isso é um absurdo completo, já que a bela forma que você escreveu faz a coisa certa com uma fração de código que qualquer um entende de relance:
Não deve ser a dor de cabeça do chamador de como as coisas ficam escondidas, e particularmente não deve ser a dor de cabeça do chamador lembrar-se de chamar as coisas em alguma "ordem correta" arbitrária.
fonte
List<Stuff>
, só há uma maneira de obtê-lo.hasStuff
é o oposto do código limpo.É preciso haver toda a lógica necessária para atender às necessidades da classe. Minha preferência pessoal é o mínimo possível, mas ao manter o código, você geralmente precisa deixar a interface original com os getters / setters existentes, mas colocar muita lógica neles para corrigir a lógica de negócios mais recente (por exemplo, "clientes "o getter em um ambiente pós-911 precisa atender ao" conheça seu cliente "e aos regulamentos da OFAC , combinados com uma política da empresa que proíbe a aparência de clientes de certos países (como Cuba ou Irã).
No seu exemplo, eu prefiro o seu e não gosto da amostra "tio bob", pois a versão "tio bob" exige que os usuários / mantenedores lembrem-se de ligar
loadStuff()
antes de ligargetStuff()
- esta é uma receita para o desastre se qualquer um de seus mantenedores esquecer (ou pior, nunca soube). A maioria dos lugares em que trabalhei na última década ainda está usando códigos com mais de uma década; portanto, a facilidade de manutenção é um fator crítico a ser considerado.fonte
Você está certo, seus colegas estão errados.
Esqueça as regras práticas de todos sobre o que um método get deve ou não fazer. Uma classe deve apresentar uma abstração de algo. Sua classe tem legibilidade
stuff
. Em Java, é convencional usar métodos 'get' para ler propriedades. Bilhões de linhas de estruturas foram escritas esperando lerstuff
por telefonegetStuff
. Se você nomear sua funçãofetchStuff
ou algo diferentegetStuff
, sua classe será incompatível com todas essas estruturas.Você pode apontá-los para o Hibernate, onde 'getStuff ()' pode fazer algumas coisas muito complicadas e lança uma RuntimeException em falha.
fonte
stuff
. Ambos ocultam detalhes para facilitar a gravação do código de chamada.Parece que isso pode ser um debate purista em relação a aplicativos que pode ser afetado pela maneira como você prefere controlar os nomes das funções. Do ponto de vista aplicado, prefiro ver:
Ao contrário de:
Ou ainda pior:
O que tende a tornar outro código muito mais redundante e mais difícil de ler, porque você precisa começar a percorrer todas as chamadas semelhantes. Além disso, chamar funções do carregador ou similares interrompe todo o objetivo de usar o OOP, pois você não está mais sendo abstraído dos detalhes de implementação do objeto com o qual está trabalhando. Se você tem um
clientRoster
objeto, não precisa se preocupar com o modo comogetNames
funciona, como faria se tivesse que chamar umloadNames
, você deve saber apenas o quegetNames
lhe dá umList<String>
com os nomes dos clientes.Assim, parece que a questão é mais sobre semântica e o melhor nome para a função obter os dados. Se a empresa (e outras pessoas) tem um problema com o prefixo
get
eset
, que tal chamar a função de algo parecidoretrieveNames
? Ele diz o que está acontecendo, mas não implica que a operação seria instantânea, como seria de esperar de umget
método.Em termos de lógica em um método acessador, mantenha-o no mínimo, pois geralmente é implícito ser instantâneo, com apenas a interação nominal ocorrendo com a variável. No entanto, isso geralmente também se aplica apenas a tipos simples, tipos de dados complexos (ie
List
), acho mais difícil encapsular adequadamente uma propriedade e geralmente uso outros métodos para interagir com eles, em oposição a um mutador e um acessador estritos.fonte
Chamar um getter deve exibir o mesmo comportamento que a leitura de um campo:
fonte
foo.setAngle(361); bar = foo.getAngle()
.bar
poderia ser361
, mas também pode ser legitimamente1
se os ângulos estão vinculados a um intervalo.stuff
, o getter irá devolver o mesmo valor. (3) O carregamento lento, como mostrado no exemplo, não produz efeitos colaterais "visíveis". (4) é discutível, talvez um ponto válido, uma vez que a introdução do "carregamento lento" posteriormente pode alterar o contrato anterior da API - mas é preciso examinar esse contrato para tomar uma decisão.Um getter que chama outras propriedades e métodos para calcular seu próprio valor também implica uma dependência. Por exemplo, se sua propriedade precisa ser capaz de se calcular e, ao fazer isso, exige que outro membro seja definido, você precisa se preocupar com referências nulas acidentais se sua propriedade for acessada no código de inicialização, onde todos os membros não estão necessariamente definidos.
Isso não significa 'nunca acessar outro membro que não seja o campo de apoio das propriedades no getter', apenas significa prestar atenção ao que você está implicando sobre qual é o estado exigido do objeto e se isso corresponde ao contexto que você espera esta propriedade para ser acessada.
No entanto, nos dois exemplos concretos que você deu, a razão pela qual eu escolheria um sobre o outro é totalmente diferente. Seu getter é inicializado no primeiro acesso, por exemplo, Lazy Initialization . O segundo exemplo é assumido como inicializado em algum momento anterior, por exemplo, Inicialização Explícita .
Quando ocorre exatamente a inicialização, pode ou não ser importante.
Por exemplo, pode ser muito lento e precisa ser feito durante uma etapa de carregamento em que o usuário espera um atraso, em vez de apresentar um desempenho inesperado quando o usuário aciona o acesso pela primeira vez (ou seja, cliques com o botão direito do mouse, menu de contexto aparece, usuário já clicou com o botão direito novamente).
Além disso, às vezes há um ponto óbvio na execução em que tudo o que pode afetar / sujar o valor da propriedade em cache ocorre. Você pode até verificar se nenhuma das dependências muda e lança exceções posteriormente. Nessa situação, faz sentido também armazenar em cache o valor nesse ponto, mesmo que não seja particularmente caro calcular, apenas para evitar tornar a execução do código mais complexa e mais difícil de seguir mentalmente.
Dito isto, a Lazy Initialization faz muito sentido em muitas outras situações. Assim, como muitas vezes acontece na programação, é difícil resumir-se a uma regra, mas tudo se resume ao código concreto.
fonte
Basta fazê-lo como o @MikeNakis disse ... Se você acabou de receber as coisas, tudo bem ... Se você fizer outra coisa, crie uma nova função que faça o trabalho e torne-a pública.
Se sua propriedade / função está fazendo apenas o que o nome diz, não resta muito espaço para complicações. A coesão é a chave IMO
fonte
loadFoo()
oupreloadDummyReferences()
oucreateDefaultValuesForUninitializedFields()
apenas porque a implementação inicial da sua classe precisava deles.Pessoalmente, eu exporia o requisito de Stuff por meio de um parâmetro no construtor e permitiria que qualquer classe que instanciava o material fizesse o trabalho de descobrir de onde deveria vir. Se o material for nulo, ele deve retornar nulo. Prefiro não tentar soluções inteligentes como o original do OP, porque é uma maneira fácil de ocultar bugs profundamente na sua implementação, onde não é óbvio o que pode estar errado quando algo quebra.
fonte
Existem questões mais importantes do que apenas "adequação" aqui e você deve basear sua decisão nessas questões . Principalmente, a grande decisão aqui é se você deseja permitir que as pessoas ignorem o cache ou não.
Primeiro, pense se existe uma maneira de reorganizar seu código para que todas as chamadas de carga e gerenciamento de cache necessários sejam feitos no construtor / inicializador. Se isso for possível, você pode criar uma classe cuja invariante permite que você faça o getter simples da parte 2 com a segurança do getter complexo da parte 1. (Um cenário em que todos saem ganhando)
Se você não pode criar essa classe, decida se possui uma troca e precisa decidir se deseja permitir que o consumidor ignore o código de verificação de cache ou não.
Se for importante que o consumidor nunca pule a verificação de cache e você não se importe com as penalidades de desempenho, junte a verificação dentro do getter e torne impossível que o consumidor faça a coisa errada.
Se não houver problema em ignorar a verificação de cache ou for muito importante que você tenha desempenho garantido de O (1) no getter, use chamadas separadas.
Como você já deve ter notado, não sou um grande fã da filosofia "código limpo", "divida tudo em pequenas funções". Se você tiver várias funções ortogonais que podem ser chamadas em qualquer ordem, dividi-las fornecerá um poder mais expressivo a um custo baixo. No entanto, se suas funções tiverem dependências de ordem (ou forem realmente úteis em uma ordem específica), dividi-las apenas aumentará o número de maneiras pelas quais você pode fazer coisas erradas, além de adicionar poucos benefícios.
fonte
Na minha opinião, Getters não deve ter muita lógica neles. Eles não devem ter efeitos colaterais e você nunca deve receber uma exceção deles. A menos, claro, você sabe o que está fazendo. A maioria dos meus getters não tem lógica neles e apenas vai para um campo. Mas a exceção notável a isso foi com uma API pública que precisava ser o mais simples possível de usar. Então, eu tinha um getter que falharia se outro getter não tivesse sido chamado. A solução? Uma linha de código como
var throwaway=MyGetter;
no getter que dependia dela. Não tenho orgulho disso, mas ainda não vejo uma maneira mais limpa de fazê-lofonte
Parece uma leitura do cache com carregamento lento. Como outros observaram, a verificação e o carregamento podem pertencer a outros métodos. O carregamento pode precisar ser sincronizado para que você não receba vinte threads ao mesmo tempo.
Pode ser apropriado usar um nome
getCachedStuff()
para o getter, pois não terá um tempo de execução consistente.Dependendo de como a
cacheInvalid()
rotina funciona, a verificação de nulo pode não ser necessária. Eu não esperaria que o cache fosse válido, a menos questuff
tivesse sido preenchido no banco de dados.fonte
A principal lógica que eu esperaria ver nos getters que retornam uma lista é lógica para garantir que a lista não seja modificável. Tal como está, seu exemplo pode quebrar o encapsulamento.
algo como:
quanto ao armazenamento em cache no getter, acho que isso seria bom, mas posso ficar tentado a mudar a lógica do cache se a criação do cache demorar um tempo significativo. isto depende.
fonte
Dependendo do caso de uso exato, eu gosto de separar meu cache em uma classe separada. Faça uma
StuffGetter
interface, implemente umaStuffComputer
que faça os cálculos e envolva-a em um objeto deStuffCacher
, responsável por acessar o cache ou encaminhar chamadas para oStuffComputer
que ele envolve.Esse design permite adicionar facilmente cache, remover cache, alterar a lógica de derivação subjacente (por exemplo, acessar um banco de dados versus retornar dados simulados) etc. É um pouco prolixo, mas vale a pena em projetos suficientemente avançados.
fonte
IMHO é muito simples se você usar um design por contrato. Decida o que o getter deve fornecer e apenas codifique de acordo (código simples ou alguma lógica complexa que possa estar envolvida ou delegada em algum lugar).
fonte