A adição de um tipo de retorno a um método de atualização viola o "Princípio de responsabilidade única"?

37

Eu tenho um método que atualiza os dados dos funcionários no banco de dados. A Employeeclasse é imutável; portanto, "atualizar" o objeto significa realmente instanciar um novo objeto.

Quero que o Updatemétodo retorne uma nova instância Employeecom 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 Employeeobjeto , 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.

Sipo
fonte
5
Um pouco de pseudocódigo pode percorrer um longo caminho aqui: existe realmente um novo registro de banco de dados criado ou o próprio registro de banco de dados é atualizado, mas no código "cliente" um novo objeto é criado porque a classe é modelada como imutável?
Martin Ba
Além do que Martin Bra perguntou, por que você está retornando uma instância de Employee ao atualizar o banco de dados? Os valores da instância Employee não foram alterados no método Update, então, por que retorná-lo (os chamadores já têm acesso à instância ...). Ou o método de atualização também recupera valores (potencialmente diferentes) do banco de dados?
Thorsal 05/07
11
@Thorsal: se suas entidades de dados são imutáveis, retornar uma instância com dados atualizados é praticamente SOP, pois caso contrário você precisaria fazer a instanciação dos dados modificados.
mikołak
21
Compare-and-swape test-and-setsã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.
MSalters
3
@MSalters: Exatamente. A separação de comando / consulta diz que deve ser possível emitir consultas que podem ser reconhecidas como idempotentes e emitir comandos sem ter que esperar por uma resposta, mas a leitura atômica, modificação e gravação atômica deve ser reconhecida como uma terceira categoria de operação.
21816

Respostas:

16

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.

Jay
fonte
Obrigado por todas as respostas, mas esta realmente me ajudou muito.
Sipo 07/07
Se você não deseja enviar sem reformatar, e não há utilidade para reformatar, além de enviar os dados posteriormente, eles são uma coisa, não duas.
Joker_vD 07/07
public function sendDataInClientFormat() { formatDataForClient(); sendDataToClient(); } private function formatDataForClient() {...} private function sendDataToClient() {...}
CJ Dennis
@CJDennis Sure. E, de fato, foi assim que eu fiz: Uma função para fazer a formatação, uma função para fazer o envio real e havia duas outras funções nas quais não vou entrar aqui. Em seguida, uma função de nível superior para chamá-los todos na seqüência correta. Você poderia dizer que "formatar e enviar" é uma operação lógica e, portanto, pode ser combinada adequadamente em uma função. Se você insistir que são duas, ok, ainda assim a coisa racional a fazer é ter uma função de nível superior que faça tudo.
Jay
67

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.

gbjbaanb
fonte
7
Como alternativa, não se trata "de tentar reduzir todas as operações para uma única chamada", mas de reduzir em quantos casos diferentes você deve pensar ao usar o item em questão.
Jpmc26
46

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 isActivefornece 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.

Kilian Foth
fonte
15
Se você precisar chamar um segundo método para verificar se o primeiro foi bem-sucedido, não será necessário chamar um terceiro método para obter o resultado do segundo? Então se você realmente quer o resultado, bem ...
3
Posso acrescentar que a aplicação excessivamente zelosa do SRP leva a uma miríade de pequenas classes, o que é um fardo em si. Como grande um fardo depende de seu ambiente, compilador, ferramentas de IDE / auxiliares etc.
Erik Alapää
8

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 é

atualizar os dados do funcionário

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.

if (visited.add(nodeToVisit)) {
    // perform operation once
}

Isso é mais eficiente que a alternativa CQS, que pode precisar executar duas pesquisas.

if (!visited.contains(nodeToVisit)) {
    visited.add(nodeToVisit);
    // perform operation once
}

(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.

Paul Draper
fonte
2

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.

Bent
fonte
2

Wrt. o caso específico:

Employee Update(Employee, name, password, etc) (na verdade, usando um construtor, pois tenho muitos parâmetros).

Ele parece o Updatemétodo leva um já existente Employeecomo 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.)

Martin Ba
fonte
1

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:

  • Você consegue pensar em possíveis implementações que não desejam incomodar o retorno de um novo objeto?
  • Você consegue pensar em componentes que dependem da interface (injetando uma instância), que não precisam do funcionário atualizado como valor de retorno?

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.

Don Quixote
fonte
0

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.

oopexpert
fonte