Prática recomendada - Encapsulando se estiver em torno da chamada da função vs Adicionando saída antecipada se a função estiver protegida

9

Sei que isso pode ser muito específico para casos de uso, mas me pergunto isso com muita frequência. Existe uma sintaxe geralmente preferida.

Não estou perguntando qual é a melhor abordagem quando em uma função, estou perguntando se devo sair mais cedo ou simplesmente não devo chamar a função.

Envoltório se chamada de função ao redor


if (shouldThisRun) {
  runFunction();
}

Ter se ( guarda ) em função

runFunction() {
  if (!shouldThisRun) return;
}

A última opção obviamente tem o potencial de reduzir a duplicação de código se essa função for chamada várias vezes, mas às vezes parece errado adicioná-la aqui, pois você pode estar perdendo a responsabilidade única da função.


Heres um exemplo

Se eu tiver uma função updateStatus () que simplesmente atualize o status de algo. Quero apenas o status atualizado se o status tiver sido alterado. Conheço os lugares no meu código em que o status tem o potencial de mudar e conheço outros lugares nos quais ele desafiadoramente mudou.

Não tenho certeza se sou apenas eu, mas parece um pouco sujo verificar essa função interna, pois quero manter essa função o mais pura possível - se eu chamá-lo, espero que o status seja atualizado. Mas não sei dizer se é melhor encerrar a ligação com um cheque nos poucos lugares em que sei que tem o potencial de não ter mudado.

Matthew Mullin
fonte
3
@gnat Não, essa questão é essencialmente 'O que é a sintaxe preferencial em uma saída precoce' enquanto o meu é 'Devo saída precoce ou devo simplesmente não chamar a função'
Matthew Mullin
4
você não pode confiar nos desenvolvedores, nem em você , para verificar corretamente as pré-condições da função em todos os lugares em que é chamada. por esse motivo, sugiro que a função valide internamente todas as condições necessárias, se tiver a capacidade de fazê-lo.
TZHX 11/03/19
"Quero apenas o status atualizado se o status for alterado" - você deseja um status atualizado (= alterado) se o mesmo status for alterado? Soa bem circular. Você pode esclarecer o que quer dizer com isso, para que eu possa adicionar um exemplo significativo à minha resposta sobre isso?
Doc Brown
@DocBrown Vamos, por exemplo, dizer que quero manter duas propriedades de status de objetos diferentes sincronizadas. Quando qualquer um dos objetos muda, chamo syncStatuses () - mas isso pode ser acionado para muitas alterações de campo diferentes (não apenas o campo de status).
Matthew Mullin

Respostas:

15

Encerrando um if em torno de uma chamada de função:
decide se a função deve ser chamada e faz parte do processo de tomada de decisão do seu programa.

Cláusula de guarda na função (retorno antecipado):
serve para proteger contra chamadas com parâmetros inválidos

Uma cláusula de guarda usada dessa maneira mantém a função "pura" (seu termo). Existe apenas para garantir que a função não seja interrompida por dados de entrada incorretos.

A lógica sobre a possibilidade de chamar a função está em um nível mais alto de abstração, mesmo que apenas. Deve existir fora da própria função. Como o DocBrown diz, você pode ter uma função intermediária que executa essa verificação, para simplificar o código.

Essa é uma boa pergunta, e se enquadra no conjunto de perguntas que levam ao reconhecimento de níveis de abstração. Cada função deve operar em um único nível de abstração, e ter a lógica do programa e a lógica da função na função parece errada para você - isso ocorre porque elas estão em diferentes níveis de abstração. A função em si é um nível mais baixo.

Ao mantê-los separados, você garante que seu código será mais fácil de escrever, ler e manter.

Baldrickk
fonte
Resposta incrível. Eu gosto do fato de que isso me dá uma maneira clara de pensar sobre isso. O if deve estar fora, pois é 'parte do processo de tomada de decisão' se a função deve ser chamada em primeiro lugar. E inerentemente não tem nada a ver com a função em si. É estranho marcar uma resposta de opinião como correta, mas voltarei a olhar em algumas horas e o faço.
Matthew Mullin
Se ajudar, não considero isso uma resposta de "opinião". Observo que "parece" errado, mas isso ocorre porque os diferentes níveis de abstração não estão separados. O que recebi da sua pergunta é que você pode ver que não está 'certo', mas como você não está pensando nos níveis de abstração, é difícil quantificá-lo; portanto, é algo que você luta para colocar em palavras.
precisa
7

Você pode ter os dois - uma função que não verifica os parâmetros e outra que faz isso, assim (talvez retornando algumas informações sobre se a chamada foi feita):

bool tryRunFunction(...)
{
    bool shouldThisRun = /* some logic using data not available inside "runFunction"*/;
    if (shouldThisRun)
        runFunction();
    return shouldThisRun;
}

Dessa forma, você pode evitar lógica duplicada fornecendo uma função reutilizável tryRunFunctione ainda ter sua função original (talvez pura) que não faz a verificação interna.

Observe que algumas vezes você precisará de uma função como tryRunFunctionuma verificação integrada exclusivamente, para poder integrar a verificação runFunction. Ou você não precisa reutilizar a verificação em nenhum lugar do seu programa novamente; nesse caso, você pode deixá-la permanecer na função de chamada.

No entanto, tente tornar transparente ao chamador o que acontece, dando nomes adequados às suas funções. Portanto, os chamadores não precisam adivinhar ou analisar a implementação se tiverem que fazer as verificações sozinhos ou se a função chamada já o fizer. Um prefixo simples como trygeralmente pode ser suficiente para isso.

Doc Brown
fonte
11
Devo admitir que o idioma "tryXXX ()" sempre parecia um pouco errado e é inadequado aqui. Você não está tentando fazer algo esperando um erro provável. Você está atualizando se estiver sujo.
user949300
@ user949300: escolher um bom nome ou esquema de nomenclatura depende do caso de uso real, dos nomes reais das funções, e não de algum nome artificial runFunction. Uma função como updateStatus()pode ser acompanhada por outra função como updateIfStatusHasChanged(). Mas isso depende 100% do caso, não há uma solução "tamanho único" para isso, então sim, eu concordo, o idioma "try" nem sempre é uma boa escolha.
Doc Brown)
Não há algo chamado "dryRun"? Mais ou menos passa a ser uma execução regular, sem efeitos colaterais. Como desativar os efeitos colaterais é outra história
LAIV
3

Quanto a quem decide se candidatar, a resposta é, do GRASP , quem é o "especialista em informações" que sabe.

Depois de decidir, considere renomear a função para maior clareza.

Algo assim, se a função decidir:

 ensureUpdated()
 updateIfDirty()

Ou, se o chamador decidir:

 writeStatus()
user949300
fonte
2

Eu gostaria de expandir a resposta de @ Baldrickk.

Não há resposta geral para sua pergunta. Depende do significado (contrato) da função a ser chamada e da natureza da condição.

Então, vamos discutir isso no contexto da sua chamada de exemplo updateStatus(). Provavelmente, o contrato é atualizar algum status, porque algo com influência no status aconteceu. Eu esperaria que as chamadas para esse método fossem permitidas, mesmo que não houvesse mudança de status real, e seriam necessárias se houver uma mudança real.

Portanto, um site de chamada pode pular uma chamada updateStatus()se souber que (dentro do horizonte de seu domínio) nada relevante foi alterado. Essas são as situações em que a chamada deve ser cercada por uma ifconstrução apropriada .

Dentro da updateStatus()função, pode haver situações em que essa função detecta (a partir de dados dentro de seu horizonte de domínio) que não há nada a fazer, e é para isso que ela deve retornar mais cedo.

Então, as perguntas são:

  • Do ponto de vista externo, quando é permitido / necessário chamar a função, levando em consideração o contrato da função?
  • Dentro da função, há situações em que ela pode retornar mais cedo sem nenhum trabalho real?
  • A condição de chamar a função / retornar com antecedência pertence ao domínio interno da função ou ao exterior?

Com uma updateStatus()função, eu esperaria ver os dois, chamando sites que nada sabem que mudou, ignorando a chamada e a implementação verificando situações "alteradas por nada" mais cedo, mesmo que dessa maneira a mesma condição seja verificada duas vezes, ambos dentro e fora.

Ralf Kleberhoff
fonte
2

Há muitas boas explicações. Mas eu quero parecer incomum: Suponha que você use dessa maneira:

if (shouldThisRun) {
   runFunction();
}

runFunction() {
   if (!shouldThisRun) return;
}

E você precisa chamar outra função no runFunctionmétodo como este:

runFunction() {
   if (!shouldThisRun) return;
   someOtherfunction();
}

O que você vai fazer? Você copia todas as validações de cima para baixo?

someOtherfunction() {
   if (!shouldThisRun) return;
}

Acho que não. Portanto, costumo fazer a mesma abordagem: valide entradas e verifique condições no publicmétodo. Os métodos públicos devem fazer suas próprias validações e verificar as condições necessárias, mesmo as chamadas. Mas deixe que os métodos privados façam seus próprios negócios . Alguma outra função pode chamar runFunctionsem fazer nenhuma validação ou verificar qualquer condição.

public someFunction() {
   if (shouldThisRun) {
      runFunction();
   }
}

private runFunction() {
 // do your business.
}
Engineert
fonte