Eu tenho um método que atualiza os dados dos funcionários no banco de dados. A Employee
classe é imutável; portanto, "atualizar" o objeto significa realmente instanciar um novo objeto.
Quero que o Update
método retorne uma nova instância Employee
com os dados atualizados, mas como agora posso dizer que a responsabilidade do método é atualizar os dados dos funcionários e buscar no banco de dados um novo Employee
objeto , isso viola o Princípio de Responsabilidade Única ?
O próprio registro do banco de dados é atualizado. Em seguida, um novo objeto é instanciado para representar esse registro.
Compare-and-swap
etest-and-set
são operações fundamentais na teoria da programação multithread. Ambos são métodos de atualização com um tipo de retorno e não poderiam funcionar de outra maneira. Isso quebra conceitos como separação de consulta de comando ou princípio de responsabilidade única? Sim, e esse é o ponto . O SRP não é uma coisa universalmente boa e pode de fato ser ativamente prejudicial.Respostas:
Como em qualquer regra, acho que o importante aqui é considerar o objetivo da regra, o espírito, e não se envolver na análise exata de como a regra foi redigida em algum livro e como aplicá-la a esse caso. Não precisamos abordar isso como advogados. O objetivo das regras é ajudar-nos a escrever programas melhores. Não é como se o objetivo de escrever programas fosse manter as regras.
O objetivo da regra de responsabilidade única é tornar os programas mais fáceis de entender e manter, fazendo com que cada função faça uma coisa coerente e independente.
Por exemplo, uma vez eu escrevi uma função que chamei de algo como "checkOrderStatus", que determinava se um pedido estava pendente, enviado, com pedido pendente, o que fosse, e retornava um código indicando qual. Em seguida, outro programador apareceu e modificou essa função para também atualizar a quantidade disponível quando o pedido foi enviado. Isso violou severamente o princípio da responsabilidade única. Outro programador que lesse esse código posteriormente veria o nome da função, veria como o valor de retorno foi usado e pode muito bem nunca suspeitar que fez uma atualização do banco de dados. Alguém que precisasse obter o status do pedido sem atualizar a quantidade disponível estaria em uma posição embaraçosa: ele deveria escrever uma nova função que duplique a parte do status do pedido? Adicione um sinalizador para informar se é necessário fazer a atualização do banco de dados? Etc.
Por outro lado, eu não escolheria o que constitui "duas coisas". Recentemente, escrevi uma função que envia informações do cliente do nosso sistema para o sistema do cliente. Essa função faz alguma reformatação dos dados para atender aos seus requisitos. Por exemplo, temos alguns campos que podem ser nulos em nosso banco de dados, mas eles não permitem nulos; portanto, precisamos preencher algum texto fictício "não especificado" ou esqueci as palavras exatas. Indiscutivelmente, essa função está fazendo duas coisas: reformate os dados E envie-os. Mas eu deliberadamente coloquei isso em uma única função, em vez de "reformatar" e "enviar", porque eu não quero nunca, nunca envie sem reformatar. Não quero que alguém escreva uma nova ligação e não perceba que ele precisa ligar para reformatar e depois enviar.
No seu caso, atualize o banco de dados e retorne uma imagem do registro gravado, como duas coisas que podem muito bem logicamente e inevitavelmente. Como não conheço os detalhes do seu aplicativo, não posso dizer definitivamente se essa é uma boa ideia ou não, mas parece plausível.
Se você estiver criando um objeto na memória que armazena todos os dados do registro, fazendo as chamadas do banco de dados para gravar isso e depois retornando o objeto, isso faz muito sentido. Você tem o objeto em suas mãos. Por que não devolvê-lo? Se você não retornou o objeto, como o chamador o conseguiu? Ele teria que ler o banco de dados para obter o objeto que você acabou de escrever? Isso parece bastante ineficiente. Como ele encontraria o registro? Você conhece a chave primária? Se alguém declarar que é "legal" a função de gravação retornar a chave primária para que você possa reler o registro, por que não retornar apenas o registro inteiro para que você não precise? Qual é a diferença?
Por outro lado, se a criação do objeto é um trabalho bastante distinto da gravação do registro do banco de dados, e um chamador pode querer fazer a gravação, mas não criar o objeto, isso pode ser um desperdício. Se um chamador desejar o objeto, mas não gravar, será necessário fornecer outra maneira de obtê-lo, o que pode significar a escrita de código redundante.
Mas acho que o cenário 1 é mais provável, então eu diria que provavelmente não há problema.
fonte
public function sendDataInClientFormat() { formatDataForClient(); sendDataToClient(); } private function formatDataForClient() {...} private function sendDataToClient() {...}
O conceito do SRP é impedir que os módulos façam 2 coisas diferentes ao fazê-los individualmente, para uma melhor manutenção e menos espaguete no tempo. Como o SRP diz "uma razão para mudar".
No seu caso, se você tiver uma rotina que "atualize e retorne o objeto atualizado", você ainda estará alterando o objeto uma vez - dando 1 motivo para a alteração. Como você devolve o objeto de volta não está aqui nem ali, você ainda está operando nesse único objeto. O código é responsável por uma e apenas uma coisa.
O SRP não é sobre tentar reduzir todas as operações a uma única chamada, mas reduzir o que você está operando e como está operando. Portanto, uma única atualização que retorna o objeto atualizado é boa.
fonte
Como sempre, isso é uma questão de grau. O SRP deve impedir você de escrever um método que recupera um registro de um banco de dados externo, executa uma transformação rápida de Fourier nele e atualiza um registro de estatísticas globais com o resultado. Eu acho que quase todo mundo concorda que essas coisas devem ser feitas por métodos diferentes. Postular uma responsabilidade única para cada método é simplesmente a maneira mais econômica e memorável de fazer isso.
No outro extremo do espectro, existem métodos que fornecem informações sobre o estado de um objeto. O típico
isActive
fornece essas informações como uma responsabilidade única. Provavelmente todos concordam que está tudo bem.Agora, alguns estendem o princípio até o ponto em que consideram o retorno de uma bandeira de sucesso uma responsabilidade diferente da execução da ação cujo sucesso está sendo relatado. Sob uma interpretação extremamente rigorosa, isso é verdade, mas como a alternativa seria chamar um segundo método para obter o status de sucesso, o que complica o chamador, muitos programadores estão perfeitamente bem em retornar códigos de sucesso de um método com efeitos colaterais.
Retornar o novo objeto é um passo adiante no caminho para várias responsabilidades. Exigir que o chamador faça uma segunda chamada para um objeto inteiro é um pouco mais razoável do que exigir uma segunda chamada apenas para verificar se a primeira foi bem-sucedida ou não. Ainda assim, muitos programadores considerariam perfeitamente o retorno do resultado de uma atualização. Embora isso possa ser interpretado como duas responsabilidades ligeiramente diferentes, certamente não é um dos abusos flagrantes que inspirou o princípio.
fonte
Viola o princípio da responsabilidade única?
Não necessariamente. Se alguma coisa, isso viola o princípio da separação entre comando e consulta .
A responsabilidade é
com o entendimento implícito de que essa responsabilidade inclui o status da operação; por exemplo, se a operação falhar, uma exceção será lançada, se for bem-sucedida, o funcionário da atualização será retornado etc.
Novamente, tudo isso é uma questão de grau e julgamento subjetivo.
Então, o que acontece com a separação de comando e consulta?
Bem, esse princípio existe, mas retornar o resultado de uma atualização é de fato muito comum.
(1) Java's
Set#add(E)
adiciona o elemento e retorna sua inclusão anterior no conjunto.Isso é mais eficiente que a alternativa CQS, que pode precisar executar duas pesquisas.
(2) Comparar e trocar , buscar e adicionar e testar e definir são primitivas comuns que permitem a existência de programação simultânea. Esses padrões aparecem frequentemente, desde instruções de CPU de baixo nível até coleções simultâneas de alto nível.
fonte
A única responsabilidade é sobre uma classe que não precisa mudar por mais de um motivo.
Como exemplo, um funcionário tem uma lista de números de telefone. Quando a maneira como você lida com os números de telefone muda (você pode adicionar códigos de chamada do país) que não devem alterar a classe dos funcionários.
Eu não teria a classe de funcionários precisando saber como ela se salva no banco de dados, porque ela mudaria para alterações nos funcionários e alterações na maneira como os dados são armazenados.
Semelhante, não deve haver um método CalculateSalary na classe empregado. Uma propriedade salarial está ok, mas o cálculo do imposto etc. deve ser feito em outro lugar.
Mas o método Update retorna o que acabou de ser atualizado.
fonte
Wrt. o caso específico:
Ele parece o
Update
método leva um já existenteEmployee
como o primeiro parâmetro para identificar (?) O funcionário existente e um conjunto de parâmetros para alterar este empregado.Eu não acho que isso poderia ser feito mais limpo. Eu vejo dois casos:
(a)
Employee
realmente contém um banco de dados / ID exclusivo pelo qual ele sempre pode ser identificado no banco de dados. (Ou seja, você não precisa de todos os valores do registro definidos para encontrá-lo no banco de dados.Nesse caso, eu preferiria um
void Update(Employee obj)
método, que apenas encontre o registro existente por ID e atualize os campos do objeto passado. Ou talvez umvoid Update(ID, EmployeeBuilder values)
Uma variação disso que achei útil é ter apenas um
void Put(Employee obj)
método que insira ou atualize, dependendo da existência do registro (por ID).(b) O registro completo existente é necessário para a pesquisa DB, nesse caso, ele ainda pode fazer mais sentido para ter:
void Update(Employee existing, Employee newData)
.Até onde posso ver aqui, eu diria que a responsabilidade de construir um novo objeto (ou valores de subobjeto) para armazenar e realmente armazená-lo é ortogonal, então eu os separaria.
Os requisitos simultâneos mencionados em outras respostas (configuração e recuperação atômica / troca de comparação, etc.) não foram um problema no código do banco de dados em que trabalhei até agora. Ao conversar com um banco de dados, acho que isso deve ser tratado no nível da transação normalmente, não no nível da instrução individual. (Isso não quer dizer que pode não haver um design em que "atômico"
Employee[existing data] Update(ID, Employee newData)
não possa fazer sentido, mas, com o DB, ele não é algo que normalmente vejo.)fonte
Até agora, todo mundo aqui está falando sobre aulas. Mas pense sobre isso da perspectiva da interface.
Se um método de interface declara um tipo de retorno e / ou uma promessa implícita sobre o valor de retorno, toda implementação precisa retornar o objeto atualizado.
Então você pode perguntar:
Pense também em objetos simulados para testes de unidade. Obviamente, será mais fácil zombar sem o valor de retorno. E os componentes dependentes são mais fáceis de testar, se suas dependências injetadas tiverem interfaces mais simples.
Com base nessas considerações, você pode tomar uma decisão.
E se você se arrepender mais tarde, ainda poderá introduzir uma segunda interface, com adaptadores entre os dois. É claro que essas interfaces adicionais aumentam a complexidade da composição, portanto é tudo uma troca.
fonte
Sua abordagem está bem. A imutabilidade é uma forte afirmação. A única coisa que gostaria de perguntar é: Existe algum outro lugar onde você constrói o objeto. Se o seu objeto não for imutável, você terá que responder a perguntas adicionais porque "Estado" é introduzido. E a mudança de estado de um objeto pode ocorrer por diferentes razões. Então você deve conhecer seus casos e eles não devem ser redundantes ou divididos.
fonte