No passado, eu normalmente fazia a maior parte da minha manipulação de um objeto no método principal que está sendo criado / atualizado, mas me peguei adotando uma abordagem diferente recentemente e estou curioso para saber se é uma prática ruim.
Aqui está um exemplo. Digamos que eu tenha um repositório que aceite uma User
entidade, mas antes de inseri-la, chamamos alguns métodos para garantir que todos os seus campos estejam configurados para o que queremos. Agora, em vez de chamar métodos e definir os valores dos campos no método Insert, chamo uma série de métodos de preparação que modelam o objeto antes de sua inserção.
Método antigo:
public void InsertUser(User user) {
user.Username = GenerateUsername(user);
user.Password = GeneratePassword(user);
context.Users.Add(user);
}
Novos métodos:
public void InsertUser(User user) {
SetUsername(user);
SetPassword(user);
context.Users.Add(user);
}
private void SetUsername(User user) {
var username = "random business logic";
user.Username = username;
}
private void SetPassword(User user) {
var password = "more business logic";
user.Password = password;
}
Basicamente, a prática de definir o valor de uma propriedade de outro método é uma prática ruim?
fonte
user
passado por referência, o código poderia arrancá-lo das mãos do chamador e substituí-lo simplesmente dizendo, digamosuser = null;
,.Respostas:
O problema aqui é que um
User
pode realmente conter duas coisas diferentes:Uma entidade de usuário completa, que pode ser transmitida ao seu armazenamento de dados.
O conjunto de elementos de dados necessários ao chamador para iniciar o processo de criação de uma entidade Usuário. O sistema deve adicionar um nome de usuário e senha antes de ser realmente um usuário válido, como no item 1 acima.
Isso inclui uma nuance não documentada no seu modelo de objeto que não é expressa no seu sistema de tipos. Você só precisa "conhecê-lo" como desenvolvedor. Isso não é ótimo e leva a padrões de código estranhos como o que você está encontrando.
Eu sugiro que você precise de duas entidades, por exemplo, uma
User
classe e umaEnrollRequest
classe. O último pode conter tudo o que você precisa saber para criar um usuário. Seria parecido com sua classe de usuário, mas sem o nome de usuário e a senha. Então você pode fazer isso:O chamador começa apenas com as informações de inscrição e recupera o usuário completo depois que ele é inserido. Dessa forma, você evita a mutação de qualquer classe e também tem uma distinção de tipo seguro entre um usuário inserido e um que não é.
fonte
InsertUser
provavelmente terá o efeito colateral da inserção. Não tenho certeza do que "nojento" significa neste contexto. Quanto ao uso de um "usuário" que não foi adicionado, parece que você perdeu o objetivo. Se não foi adicionado, não é um usuário, apenas uma solicitação para criar um usuário. Você é livre para trabalhar com a solicitação, é claro.Os efeitos colaterais são aceitáveis, desde que não sejam inesperados. Portanto, não há nada errado em geral quando um repositório possui um método para aceitar um usuário e altera o estado interno do usuário. Mas IMHO, como um nome de método
InsertUser
, não comunica isso claramente , e é isso que o torna propenso a erros. Ao usar seu repo, eu esperaria uma ligação comopara alterar o estado interno dos repositórios, não o estado do objeto do usuário. Esse problema existe em ambas as implementações, como
InsertUser
isso internamente é completamente irrelevante.Para resolver isso, você pode
separar a inicialização da inserção (para que o chamador
InsertUser
precise fornecer umUser
objeto totalmente inicializado ou,crie a inicialização no processo de construção do objeto de usuário (conforme sugerido por algumas das outras respostas) ou
tente simplesmente encontrar um nome melhor para o método que expressa mais claramente o que ele faz.
Portanto, escolha um nome de método como
PrepareAndInsertUser
,OrchestrateUserInsertion
,InsertUserWithNewNameAndPassword
ou o que você preferir fazer o efeito colateral mais clara.Obviamente, um nome de método tão longo indica que o método talvez esteja fazendo "muito" (violação do SRP), mas às vezes você não quer ou não pode consertar isso facilmente, e essa é a solução menos intrusiva e pragmática.
fonte
Estou analisando as duas opções que você escolheu e devo dizer que prefiro o método antigo em vez do novo método proposto. Existem algumas razões para isso, mesmo que eles façam a mesma coisa.
Nos dois casos, você está definindo o
user.UserName
euser.Password
. Tenho reservas sobre o item de senha, mas essas reservas não são pertinentes ao tópico em questão.Implicações da modificação de objetos de referência
Método Antigo vs. Novo Método
O método antigo tornou as coisas mais fáceis de testar:
GenerateUserName()
é testável independentemente. Você pode escrever testes nesse método e garantir que os nomes sejam gerados corretamenteGenerateUserName(User user)
e manter essa testabilidadeO novo método oculta as mutações:
User
objeto está mudando até que você tenha 2 camadas de profundidadeUser
objeto são mais surpreendentes nesse casoSetUserName()
faz mais do que definir um nome de usuário. Isso não é verdade na publicidade, o que torna mais difícil para os novos desenvolvedores descobrirem como as coisas funcionam no seu aplicativofonte
Concordo plenamente com a resposta de John Wu. Sua sugestão é boa. Mas falta um pouco a sua pergunta direta.
Não inerentemente.
Você não pode levar isso muito longe, pois terá um comportamento inesperado. Por exemplo
PrintName(myPerson)
, não deve alterar o objeto person, pois o método implica que ele esteja interessado apenas na leitura dos valores existentes. Mas esse é um argumento diferente do seu caso, poisSetUsername(user)
implica fortemente que ele definirá valores.Essa é realmente uma abordagem que costumo usar para testes de unidade / integração, onde crio um método especificamente para alterar um objeto, a fim de definir seus valores para uma situação específica que quero testar.
Por exemplo:
Eu espero explicitamente que o
ArrrangeContractDeletedStatus
método altere o estado domyContract
objeto.O principal benefício é que o método permite testar a exclusão de contratos com diferentes contratos iniciais; por exemplo, um contrato que tenha um longo histórico de status ou um que não tenha um histórico anterior, um contrato que tenha um histórico de status intencionalmente errôneo, um contrato que meu usuário de teste não tem permissão para excluir.
Se eu tivesse mesclado
CreateEmptyContract
eArrrangeContractDeletedStatus
em um único método; Eu precisaria criar várias variantes desse método para cada contrato diferente que gostaria de testar em um estado excluído.E enquanto eu poderia fazer algo como:
Isso é redundante (já que estou alterando o objeto de qualquer maneira) ou agora estou me forçando a criar um clone profundo do
myContract
objeto; o que é excessivamente difícil se você deseja cobrir todos os casos (imagine se eu quero propriedades de navegação em vários níveis. Preciso clonar todas elas? Somente a entidade de nível superior? ... Tantas perguntas, tantas expectativas implícitas )Alterar o objeto é a maneira mais fácil de obter o que quero, sem ter que fazer mais trabalho apenas para evitar não alterar o objeto por uma questão de princípio.
Portanto, a resposta direta à sua pergunta é que não é uma prática inerentemente ruim, desde que você não ofusque que o método possa alterar o objeto passado. Para a sua situação atual, isso é esclarecido abundantemente pelo nome do método.
fonte
Acho o antigo e o novo problemático.
À moda antiga:
1)
InsertUser(User user)
Ao ler esse nome de método aparecendo no meu IDE, a primeira coisa que me vem à cabeça é
O nome do método deve ser lido
AddUserToContext
. Pelo menos, é o que o método faz no final.2)
InsertUser(User user)
Isso viola claramente o princípio da menor surpresa. Digamos, eu fiz meu trabalho até agora e criei uma instância recente de ae
User
dei a elename
e defini umpassword
, ficaria surpreso:a) isso não apenas insere um usuário em algo
b) também subverte minha intenção de inserir o usuário como está; o nome e a senha foram substituídos.
c) isso indica que também é uma violação do princípio da responsabilidade única.
Nova maneira:
1)
InsertUser(User user)
Ainda violação do SRP e princípio da menor surpresa
2)
SetUsername(User user)
Questão
Melhor:
SetRandomName
ou algo que reflete a intenção.3)
SetPassword(User user)
Questão
Melhor:
SetRandomPassword
ou algo que reflete a intenção.Sugestão:
O que eu preferiria ler seria algo como:
Em relação à sua pergunta inicial:
Não. É mais uma questão de gosto.
fonte