É correto que uma função modifique um parâmetro

17

Temos uma camada de dados que envolve o Linq To SQL. Nesta camada de dados, temos este método (simplificado)

int InsertReport(Report report)
{
    db.Reports.InsertOnSubmit(report);
    db.SubmitChanges();
    return report.ID; 
}

Ao enviar as alterações, o ID do relatório é atualizado com o valor no banco de dados que retornamos.

Do lado da chamada, fica assim (simplificado)

var report = new Report();
DataLayer.InsertReport(report);
// Do something with report.ID

Observando o código, o ID foi definido dentro da função InsertReport como um tipo de efeito colateral, e então estamos ignorando o valor de retorno.

Minha pergunta é: devo confiar no efeito colateral e fazer algo assim em seu lugar.

void InsertReport(Report report)
{
    db.Reports.InsertOnSubmit(report);
    db.SubmitChanges();
}

ou devemos impedi-lo

int InsertReport(Report report)
{
    var newReport = report.Clone();
    db.Reports.InsertOnSubmit(newReport);
    db.SubmitChanges();
    return newReport.ID; 
}

talvez até

Report InsertReport(Report report)
{
    var newReport = report.Clone();
    db.Reports.InsertOnSubmit(newReport);
    db.SubmitChanges();
    return newReport; 
}

Essa questão foi levantada quando criamos um teste de unidade e descobrimos que não está realmente claro que a propriedade ID dos parâmetros do relatório é atualizada e que, para zombar do comportamento do efeito colateral, parecia errado, um cheiro de código, se você quiser.

John Petrak
fonte
2
É para isso que serve a documentação da API.

Respostas:

16

Sim, tudo bem e bastante comum. Pode não ser óbvio, como você descobriu.

Em geral, costumo ter métodos do tipo persistência retornando a instância atualizada do objeto. Isso é:

Report InsertReport(Report report)
{        
    db.Reports.InsertOnSubmit(report);
    db.SubmitChanges();
    return report; 
}

Sim, você está retornando o mesmo objeto que passou, mas isso torna a API mais clara. Não há necessidade do clone - se alguma coisa causar confusão, se, como no código original, o chamador continuar usando o objeto pelo qual passou.

Outra opção é usar um DTO

Report InsertReport(ReportDTO dto)
{
    var newReport = Report.Create(dto);
    db.Reports.InsertOnSubmit(newReport);
    db.SubmitChanges();
    return newReport; 
}

Dessa forma, a API é muito óbvia e o chamador não pode tentar acidentalmente usar o objeto passado / modificado. Dependendo do que o seu código está fazendo, pode ser um pouco trabalhoso.

Ben Hughes
fonte
Eu não retornaria o objeto se Ira não fosse necessário. Isso parece processamento extra e uso de memória (excesso). Eu vou para um vazio ou exceção, se não for necessário. Senão, voto na sua amostra de DTO.
Independente
Todas essas respostas resumem nossas próprias discussões. Esta parece ser uma pergunta sem resposta real. Sua declaração "Sim, você está retornando o mesmo objeto que passou, mas isso torna a API mais clara". praticamente respondeu a nossa pergunta.
precisa
4

Na IMO, esse é um caso raro em que o efeito colateral da mudança é desejável - porque sua entidade de Relatório possui um ID, já é possível supor que ela tenha as preocupações de um DTO e, sem dúvida, existe a obrigação do ORM de garantir que o Relatório na memória A entidade é mantida em sincronia com o objeto de representação do banco de dados.

StuartLC
fonte
6
+1 - depois de inserir uma entidade no banco de dados, você esperaria que ele tivesse um ID. Seria mais surpreendente se a entidade voltasse sem ela.
precisa saber é o seguinte
2

O problema é que, sem a documentação, não está claro o que o método está fazendo e, principalmente, por que ele está retornando um número inteiro.

A solução mais fácil seria usar um nome diferente para o seu método. Algo como:

int GenerateIdAndInsert(Report report)

Ainda assim, isso não é suficientemente ambíguo: se, como em C #, a instância do reportobjeto for passada por referência, seria difícil saber se o reportobjeto original foi modificado ou se o método o clonou e modificou apenas o clone. Se você optar por modificar o objeto original, seria melhor nomear o método:

void ChangeIdAndInsert(Report report)

Uma solução mais complicada (e talvez menos otimizada) é refatorar fortemente o código. A respeito:

using (var transaction = new TransactionScope())
{
    var id = this.Data.GenerateReportId(); // We need to find an available ID...
    this.Data.AddReportWithId(id, report); // ... and use this ID to insert a report.
    transaction.Complete();
}
Arseni Mourzenko
fonte
2

Geralmente, os programadores esperam que apenas os métodos de instância de um objeto possam alterar seu estado. Em outras palavras, não fico surpreso se report.insert()alterar o ID do relatório e for fácil verificar. Não é fácil pensar em todos os métodos em todo o aplicativo se ele altera o ID do relatório ou não.

Eu também argumentaria que talvez IDnem devesse pertencer Report. Como ele não contém um ID válido por tanto tempo, você realmente tem dois objetos diferentes antes e depois da inserção, com comportamento diferente. O objeto "antes" pode ser inserido, mas não pode ser recuperado, atualizado ou excluído. O objeto "depois" é exatamente o oposto. Um tem um ID e o outro não. A maneira como eles são exibidos pode ser diferente. As listas em que aparecem podem ser diferentes. Permissões de usuário associadas podem ser diferentes. Ambos são "relatórios" no sentido inglês da palavra, mas são muito diferentes.

Por outro lado, seu código pode ser simples o suficiente para que um objeto seja suficiente, mas é algo a considerar se seu código estiver apimentado if (validId) {...} else {...}.

Karl Bielefeldt
fonte
0

Não, não está tudo bem! É adequado para um procedimento modificar um parâmetro apenas em linguagens procedurais, onde não há outra maneira; em idiomas OOP, chame o método de alteração no objeto, neste caso no relatório (algo como report.generateNewId ()).

Nesse caso, seu método faz duas coisas, portanto quebra o SRP: ele insere um registro no banco de dados e gera um novo ID. O chamador não pode saber que seu método também gera um novo ID, pois é chamado simplesmente insertRecord ().

m3th0dman
fonte
3
Ermm ... e se db.Reports.InsertOnSubmit(report)chama o método de alteração no objeto?
Stephen C
Está tudo bem ... deve ser evitado, mas, neste caso, o LINQ to SQL está fazendo a modificação de parâmetros, portanto, não é como se o OP pudesse evitar isso sem o efeito clone do hoop jumping (que é sua própria violação do SRP).
Telastyn
@StephenC Eu estava dizendo que você deveria chamar esse método no objeto de relatório; neste caso, não há sentido em passar o mesmo objeto que um parâmetro.
M3th0dman
@Telastyn eu estava falando no caso geral; boas práticas não podem ser respeitadas 100%. No seu caso particular, ninguém pode inferir qual é a melhor maneira de 5 linhas de código ...
m3th0dman