Você sempre deve passar os dados mínimos necessários para uma função em casos como este

81

Digamos que eu tenho uma função IsAdminque verifica se um usuário é um administrador. Digamos também que a verificação do administrador seja feita combinando a ID do usuário, o nome e a senha com algum tipo de regra (não importante).

Na minha cabeça, existem duas possíveis assinaturas de funções para isso:

public bool IsAdmin(User user);
public bool IsAdmin(int id, string name, string password);

Costumo escolher o segundo tipo de assinatura, pensando que:

  • A assinatura da função fornece ao leitor muito mais informações
  • A lógica contida na função não precisa saber sobre a Userclasse
  • Geralmente resulta em um pouco menos de código dentro da função

No entanto, às vezes questiono essa abordagem e também percebo que em algum momento ela se tornaria pesada. Se, por exemplo, uma função mapeasse entre dez campos de objetos diferentes em um bool resultante, eu obviamente enviaria o objeto inteiro. Mas, além de um exemplo gritante como esse, não vejo uma razão para passar no objeto real.

Eu apreciaria quaisquer argumentos para qualquer estilo, bem como quaisquer observações gerais que você possa oferecer.

Como programa em estilos orientados a objetos e funcionais, a pergunta deve ser vista em relação a todo e qualquer idioma.

Anders Arpi
fonte
40
Fora do tópico: Por curiosidade, por que o status "é administrador" de um usuário depende de sua senha?
stakx
43
@ syb0rg Errado. Em C #, essa é a convenção de nomenclatura .
magnattic
68
@ syb0rg Certo, ele não especificou o idioma, então acho estranho dizer que ele está violando a convenção de um determinado idioma.
magnattic
31
@ syb0rg A afirmação geral de que "os nomes das funções não devem começar com uma letra maiúscula" está errada, porque existem idiomas onde deveriam. De qualquer forma, não quis ofendê-lo, estava apenas tentando esclarecer isso. Isso está ficando praticamente fora do tópico para a pergunta em si. Não vejo a necessidade, mas se você quiser continuar, vamos passar o debate para o chat .
#
6
Como ponto geral, rolar sua própria segurança geralmente é uma coisa ruim (tm) . A maioria das linguagens e estruturas possui sistemas de segurança / permissão disponíveis e existem boas razões para usá-los, mesmo quando você deseja algo mais simples.
James Snell

Respostas:

123

Eu pessoalmente prefiro o primeiro método de apenas IsAdmin(User user)

É muito mais fácil de usar e, se seus critérios para o IsAdmin forem alterados posteriormente (talvez com base em funções ou isActive), você não precisará reescrever a assinatura do método em todos os lugares.

Também é provavelmente mais seguro, pois você não anuncia quais propriedades determinam se um usuário é um administrador ou não, ou está passando a propriedade da senha em qualquer lugar. E syrion enfatiza que o que acontece quando o seu idnão corresponde ao name/ password?

O comprimento do código dentro de uma função não deve realmente importar, desde que o método faça seu trabalho, e eu prefiro ter um código de aplicativo mais curto e mais simples do que o código do método auxiliar.

Rachel
fonte
3
Eu acho que a questão da refatoração é um ponto muito bom - obrigado.
precisa
1
Eu ia criar o argumento de assinatura do método, se não o fizesse. Isso é especialmente útil quando há muitas dependências no seu método que são mantidas por outros programadores. Isso ajuda a manter as pessoas afastadas umas das outras. 1
jmort253
1
Concordo com esta resposta, mas deixe-me sinalizar duas situações em que a outra abordagem ("dados mínimos") pode ser preferível: (1) Você deseja armazenar em cache os resultados do método. Melhor não usar um objeto inteiro como chave de cache ou verificar as propriedades de um objeto em cada chamada. (2) Você deseja que o método / função seja executado de forma assíncrona, o que pode envolver a serialização dos argumentos e, por exemplo, armazená-los em uma tabela. Mais fácil e mais simples procurar um número inteiro que um blob de objeto ao gerenciar trabalhos pendentes.
precisa
O antipadrão de obsessão primitiva pode ser horrível para testes de unidade.
Samuel
82

A primeira assinatura é superior porque permite o encapsulamento da lógica relacionada ao usuário no Userpróprio objeto. Não é benéfico ter lógica para a construção da tupla "id, nome, senha" espalhada sobre sua base de código. Além disso, complica a isAdminfunção: o que acontece se alguém passa em um idque não corresponde ao name, por exemplo? E se você quiser verificar se um determinado usuário é um administrador de um contexto em que você não deve saber sua senha?

Além disso, como uma observação no seu terceiro ponto em favor do estilo com argumentos extras; pode resultar em "menos código dentro da função", mas para onde vai essa lógica? Não pode simplesmente desaparecer! Em vez disso, ele se espalha por todos os lugares onde a função é chamada. Para salvar cinco linhas de código em um único local, você pagou cinco linhas por uso da função!

asthasr
fonte
45
Seguindo sua lógica, não user.IsAdmin()seria muito melhor?
precisa
13
Sim absolutamente.
asthasr
6
@ AndersHolmström Depende, você está testando se o usuário é um administrador em geral ou apenas um administrador da página / formulário atual em que está. Se, em geral, então sim que seria melhor, mas se você está apenas testando IsAdminpara uma forma ou função específica, que deve estar relacionado com o usuário
Rachel
40
@ Anders: não, não deveria. Um usuário não deve decidir se é um administrador ou não. Os privilégios ou sistema de segurança devem. Algo a ser fortemente acoplados a uma classe não significa que ele é uma boa idéia para torná-lo parte dessa classe
Marjan Venema
11
@MarjanVenema - a idéia de que a classe User não deve "decidir" se uma instância é um administrador me parece um pouco culto à carga. Você realmente não pode fazer essa generalização tão fortemente. Se suas necessidades são complexas, talvez seja benéfico encapsulá-las em um "sistema" separado, mas citando o KISS + YAGNI, eu gostaria de tomar essa decisão realmente diante dessa complexidade, e não como regra geral :-). E observe que, mesmo que a lógica não seja "parte" do Usuário, ainda pode ser útil, por uma questão de praticidade, ter um repasse no Usuário que apenas delega para um sistema mais complexo.
Eamon Nerbonne
65

A primeira, mas não (apenas) pelas razões que outros deram.

public bool IsAdmin(User user);

Esse é o tipo seguro. Especialmente porque User é um tipo que você definiu, há pouca ou nenhuma oportunidade de transpor ou alternar argumentos. Ele opera claramente em Usuários e não é uma função genérica que aceita int e duas strings. Essa é uma grande parte do ponto de usar uma linguagem de segurança de tipo como Java ou C # com a qual seu código se parece. Se você estiver perguntando sobre um idioma específico, poderá adicionar uma tag para esse idioma à sua pergunta.

public bool IsAdmin(int id, string name, string password);

Aqui, qualquer int e duas strings servirão. O que impede você de transpor o nome e a senha? O segundo é mais trabalho a ser usado e permite mais oportunidades de erro.

Presumivelmente, ambas as funções exigem que user.getId (), user.getName () e user.getPassword () sejam chamados, dentro da função (no primeiro caso) ou antes de chamar a função (no segundo), portanto, o A quantidade de acoplamento é a mesma. Na verdade, como essa função é válida apenas em Usuários, em Java eu ​​eliminaria todos os argumentos e tornaria isso um método de instância nos objetos Usuário:

user.isAdmin();

Como essa função já está fortemente acoplada aos Usuários, faz sentido torná-la parte do que é um usuário.

PS Tenho certeza de que este é apenas um exemplo, mas parece que você está armazenando uma senha. Em vez disso, você deve armazenar apenas um hash de senha criptograficamente segura. Durante o login, a senha fornecida deve ser hash da mesma maneira e comparada com o hash armazenado. O envio de senhas em texto sem formatação deve ser evitado. Se você violar essa regra e o isAdmin (int Str Str) tiver que registrar o nome de usuário, se você transpôs o nome e a senha no seu código, a senha poderá ser registrada, o que cria um problema de segurança (não escreva senhas nos logs ) e é outro argumento a favor de passar um objeto em vez de suas partes componentes (ou usar um método de classe).

GlenPeterson
fonte
11
Um usuário não deve decidir se é um administrador ou não. Os privilégios ou sistema de segurança devem. Algo que está sendo fortemente associado a uma classe não significa que é uma boa ideia fazer parte dela.
Marjan Venema
6
@MarjanVenema O usuário não está decidindo nada. O objeto / tabela Usuário armazena dados sobre cada usuário. Usuários reais não conseguem mudar tudo sobre si mesmos. Mesmo quando o usuário altera algo a que está autorizado, ele pode acionar alertas de segurança como um email, se alterar o endereço de email, o ID ou a senha do usuário. Você está usando um sistema em que os usuários podem magicamente alterar tudo sobre determinados tipos de objetos? Eu não estou familiarizado com esse sistema.
GlenPeterson
2
@GlenPeterson: você está deliberadamente me entendendo mal? Quando eu disse usuário, claro que quis dizer a classe de usuário.
Marjan Venema #
2
@GlenPeterson Totalmente fora de tópico, mas várias rodadas de hash e funções criptográficas enfraquecerão os dados.
Gusdor
3
@MarjanVenema: Não estou sendo deliberadamente difícil. Acho que só temos perspectivas muito diferentes e gostaria de entender melhor a sua. Eu abri uma nova pergunta onde isto pode ser melhor discutido: programmers.stackexchange.com/questions/216443/...
GlenPeterson
6

Se o seu idioma de escolha não passa estruturas por valor, a (User user)variante parece melhor. E se algum dia você decidir descartar o nome de usuário e apenas usar o ID / senha para identificar o usuário?

Além disso, essa abordagem leva inevitavelmente a chamadas de método longas e exageradas, ou inconsistências. Passar 3 argumentos é bom? Que tal 5? Ou 6? Por que pensar nisso, se passar um objeto é barato em termos de recursos (ainda mais barato do que passar 3 argumentos, provavelmente)?

E eu (meio que) discordo que a segunda abordagem fornece ao leitor mais informações - intuitivamente, a chamada do método está perguntando "se o usuário tem direitos de administrador", e não "se a combinação de identificação / nome / senha fornecida tem direitos de administrador".

Portanto, a menos que a passagem do Userobjeto resulte na cópia de uma estrutura grande, a primeira abordagem é mais limpa e mais lógica para mim.

Maciej Stachowski
fonte
3

Eu provavelmente teria os dois. O primeiro como uma API externa, com alguma esperança de estabilidade no tempo. O segundo como uma implementação privada chamada pela API externa.

Se, em algum momento posterior, for necessário alterar a regra de verificação, escreveria apenas uma nova função privada com uma nova assinatura chamada pela externa.

Isso tem a vantagem da facilidade de alterar a implementação interna. Também é realmente comum que, em algum momento, você possa precisar das duas funções disponíveis ao mesmo tempo e chamar uma ou outra, dependendo de alguma mudança de contexto externa (por exemplo, você pode ter usuários antigos coexistentes e novos usuários com campos diferentes).

Em relação ao título da pergunta, de certa forma, nos dois casos, você está fornecendo as informações mínimas necessárias. Um usuário parece ser a menor estrutura de dados relacionados a aplicativos que contém as informações. Os três campos id, senha e nome parecem ser os menores dados de implementação realmente necessários, mas esses não são realmente o objeto no nível do aplicativo.

Em outras palavras, se você estivesse lidando com um banco de dados, um Usuário seria um registro, enquanto identificação, senha e login seriam campos nesse registro.

kriss
fonte
Não vejo a razão do método privado. Por que manter os dois? Se o método privado precisar de argumentos diferentes, você ainda precisará modificar o público. E você irá testá-lo através de um público. E não, não é habitual que você precise dos dois algum tempo depois. Você está adivinhando aqui - YAGNI.
Piotr Perak #
Você está assumindo, por exemplo, que a estrutura de dados do usuário não possui nenhum outro campo desde o início. Geralmente não é verdade. As segundas funções deixam claro qual parte interna do usuário é verificada para ver se é um administrador. Os casos de uso típicos podem ser do tipo: se a hora de criação do usuário for anterior a alguma data, chame a regra antiga, se não chamar a nova regra (que pode depender de outras partes do Usuário ou depende da mesma, mas usando outra regra). Ao dividir o código dessa maneira, você terá duas funções mínimas: uma API conceitual mínima e uma função de implementação mínima.
kriss
em outras palavras, dessa forma, será imediatamente visível na assinatura da função interna quais partes do Usuário são usadas ou não. No código de produção, isso nem sempre é óbvio. Atualmente, estou trabalhando em uma grande base de código com vários anos de histórico de manutenção e esse tipo de divisão é muito útil para manutenção de longo prazo. Se você não pretende manter o código, é realmente inútil (mas mesmo fornecendo uma API conceitual estável também é provavelmente inútil).
quer
Outra palavra: certamente não testarei o método interno por meio de método externo. Essa é uma prática ruim de teste.
kriss
1
Essa é uma boa prática e existe um nome para ela. É chamado de "princípio de responsabilidade única". Pode ser aplicado a classes e métodos. Ter métodos com uma única responsabilidade é uma boa maneira de criar código mais modular e sustentável. Nesse caso, uma tarefa está escolhendo um conjunto básico de dados do objeto usuário, e outra tarefa está verificando esse conjunto básico de dados para ver se o usuário é um administrador. Seguindo esse princípio, você obtém o bônus de poder compor métodos e evitar a duplicação de código. Isso também significa, como afirma o @kriss, que você obtém melhores testes de unidade.
Diegoreymendez # 10/14
3

Há um ponto negativo para enviar classes (tipos de referência) para métodos, ou seja, Vulnerabilidade de atribuição em massa . Imagine que, em vez de IsAdmin(User user)método, eu tenho UpdateUser(User user)método.

Se a Userclasse tiver uma propriedade booleana chamada IsAdmine se eu não a verificar durante a execução do meu método, estou vulnerável à atribuição em massa. O GitHub foi atacado por essa mesma assinatura em 2012.

Saeed Neamati
fonte
2

Eu iria com esta abordagem:

public bool IsAdmin(this IUser user) { /* ... */ }

public class User: IUser { /* ... */ }

public interface IUser
{
    string Username {get;}
    string Password {get;}
    IID Id {get;}
}
  • O uso do tipo de interface como parâmetro para esta função permite passar objetos de Usuário, definir objetos de Usuário simulados para teste e oferece mais flexibilidade em relação ao uso e manutenção dessa função.

  • Também adicionei a palavra this- chave para tornar a função um método de extensão de todas as classes IUser; Dessa forma, você pode escrever o código de maneira mais amigável ao OO e usar essa função nas consultas do Linq. Ainda mais simples seria definir essa função em IUser e User, mas suponho que haja algum motivo para você ter decidido colocar esse método fora dessa classe?

  • Eu uso a interface personalizada em IIDvez de, intpois isso permite que você redefina as propriedades do seu ID; por exemplo, você deve precisar alterar a partir intde long, Guidou outra coisa. Provavelmente isso é um exagero, mas sempre é bom tentar flexibilizar as coisas para que você não fique preso às decisões iniciais.

  • NB: O objeto IUser pode estar em um namespace e / ou assembly diferente da classe User, portanto, você tem a opção de manter o código do usuário completamente separado do código IsAdmin, pois eles compartilham apenas uma biblioteca referenciada. Exatamente o que você faz, há uma chamada para saber como você suspeita que esses itens sejam usados.

JohnLBevan
fonte
3
O IUser é inútil aqui e apenas adiciona complexidade. Não vejo por que você não pôde usar um usuário real nos testes.
Piotr Perak #
1
Ele adiciona flexibilidade futura com muito pouco esforço, portanto, embora não agregue valor atualmente, não faz mal. Pessoalmente, prefiro sempre fazer isso, pois evita pensar em possibilidades futuras para todos os cenários (o que é praticamente impossível, dada a natureza imprevisível dos requisitos, e levará muito mais tempo para obter uma resposta parcialmente boa do que seria possível. ficar em uma interface).
21414 John LBevan
@Peri uma classe de usuário real pode ser complexa de criar, usar uma interface permite que você faça uma simulação, para que você possa manter seus testes simples
jk.
@JohnLBevan: YAGNI !!!
Piotr Perak
@jk .: se é difícil de construir Usuário então algo está errado
Piotr Perak
1

Isenções de responsabilidade:

Para minha resposta, vou me concentrar na questão do estilo e esquecer se um ID, login e senha simples são um bom conjunto de parâmetros a serem usados, do ponto de vista da segurança. Você deve conseguir extrapolar minha resposta para qualquer conjunto de dados básico que estiver usando para descobrir os privilégios do usuário ...

Eu também considero user.isAdmin()ser equivalente a IsAdmin(User user). Essa é uma escolha para você fazer.

Resposta:

Minha recomendação é apenas para o usuário:

public bool IsAdmin(User user);

Ou use ambos, ao longo das linhas de:

public bool IsAdmin(User user); // calls the private method below
private bool IsAdmin(int id, string name, string password);

Razões para o método público:

Ao escrever métodos públicos, geralmente é uma boa idéia pensar em como eles serão usados.

Nesse caso em particular, o motivo pelo qual você normalmente chama esse método é descobrir se um determinado usuário é um administrador. Esse é um método que você precisa. Você realmente não quer que cada chamador tenha que escolher os parâmetros certos para enviar (e arriscar erros devido à duplicação de código).

Razões para o método privado:

O método privado que recebe apenas o conjunto mínimo de parâmetros necessários pode ser uma coisa boa às vezes. Às vezes nem tanto.

Uma das coisas boas da divisão desses métodos é que você pode chamar a versão privada de vários métodos públicos da mesma classe. Por exemplo, se você deseja (por qualquer motivo) ter uma lógica ligeiramente diferente Localusere RemoteUser(talvez haja uma configuração para ativar / desativar administradores remotos?):

public bool IsAdmin(Localuser user); // Just call private method
public bool IsAdmin(RemoteUser user); // Check if setting is on, then call private method
private bool IsAdmin(int id, string name, string password);

Além disso, se por algum motivo você precisar tornar público o método privado ... você pode. É tão fácil quanto mudar privatepara public. Pode não parecer uma grande vantagem para este caso, mas às vezes é realmente.

Além disso, ao testar a unidade, você poderá fazer muito mais testes atômicos. Geralmente, é muito bom não ter que criar um objeto de usuário completo para executar testes nesse método. E se você quiser cobertura total, poderá testar as duas chamadas.

diegoreymendez
fonte
0
public bool IsAdmin(int id, string name, string password);

é ruim pelos motivos listados. Isto não significa

public bool IsAdmin(User user);

é automaticamente bom. Em particular se o usuário é um objeto com métodos como: getOrders(), getFriends(), getAdresses(), ...

Você pode refatorar o domínio com um tipo de UserCredentials contendo apenas ID, nome, senha e passá-lo para o IsAdmin, em vez de todos os dados desnecessários do usuário.

tkruse
fonte