Si vous transmettez toujours le minimum de données nécessaire à une fonction dans des cas comme celui-ci

81

Disons que j'ai une fonction IsAdminqui vérifie si un utilisateur est un administrateur. Disons également que la vérification de l'administrateur est effectuée en faisant correspondre l'identifiant, le nom et le mot de passe de l'utilisateur à une sorte de règle (sans importance).

Dans ma tête, il y a alors deux signatures de fonction possibles pour cela:

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

Je choisis le plus souvent le deuxième type de signature, en pensant que:

  • La signature de fonction donne au lecteur beaucoup plus d'informations
  • La logique contenue dans la fonction n'a pas besoin de connaître la Userclasse
  • Il en résulte généralement un peu moins de code dans la fonction

Cependant, je remets parfois en cause cette approche et réalise également qu’à un moment donné, elle deviendrait difficile à manier. Si, par exemple, une fonction mappait entre dix champs d'objet différents dans un bool résultant, j'enverrais évidemment l'objet entier. Mais à part un exemple aussi frappant que celui-là, je ne vois aucune raison de transmettre l'objet réel.

J'apprécierais tous les arguments pour l'un ou l'autre style, ainsi que vos observations générales.

Je programme dans les styles à la fois orienté objet et fonctionnel, la question doit donc être considérée comme concernant tous les idiomes.

Anders Arpi
la source
40
Hors sujet: par curiosité, pourquoi le statut "est administrateur" d'un utilisateur dépend-il de son mot de passe?
Stakx
43
@ syb0rg Wrong. En C #, c'est la convention de nommage .
Magnattic
68
@ syb0rg C'est vrai, il n'a pas précisé le langage, alors je trouve assez étrange de lui dire qu'il viole la convention d'un certain langage.
Magnattic
31
@ syb0rg La déclaration générale selon laquelle "les noms de fonction ne doivent pas commencer par une lettre majuscule" est fausse, car certaines langues le devraient. Quoi qu'il en soit, je ne voulais pas vous offenser, je voulais juste clarifier cela. Cela est en train de devenir hors sujet par rapport à la question elle-même. Je ne vois pas la nécessité, mais si vous voulez continuer à faire avancer le débat permet à la discussion .
Magnattic
6
De manière générale, faire rouler votre propre sécurité est généralement une mauvaise chose (tm) . La plupart des langages et des frameworks ont des systèmes de sécurité / autorisation disponibles et il y a de bonnes raisons de les utiliser même quand vous voulez quelque chose de plus simple.
James Snell

Réponses:

123

Personnellement, je préfère la première méthode de juste IsAdmin(User user)

Il est beaucoup plus facile à utiliser et si vos critères pour IsAdmin changent ultérieurement (peut-être en fonction de rôles ou d'isActive), vous n'avez pas besoin de réécrire la signature de votre méthode partout.

Il est également probablement plus sûr de ne pas annoncer les propriétés qui déterminent si un utilisateur est un administrateur ou non, ou de transmettre la propriété de mot de passe partout. Et syrion insiste sur le fait que que se passe-t-il lorsque votre idne correspond pas à la name/ password?

La longueur du code dans une fonction ne devrait pas vraiment avoir d'importance, à condition que la méthode fasse son travail, et je préférerais de loin un code d'application plus court et plus simple que le code de la méthode auxiliaire.

Rachel
la source
3
Je pense que la question du refactoring est un très bon point - merci.
Anders Arpi
1
J'allais faire l'argument de signature de méthode si vous ne le faisiez pas. Ceci est particulièrement utile lorsqu'il existe de nombreuses dépendances sur votre méthode qui sont gérées par d'autres programmeurs. Cela aide à garder les gens à l'écart des autres. +1
jmort253
1
Je suis d’accord avec cette réponse, mais permettez-moi de signaler deux situations dans lesquelles l’autre approche ("données minimales") pourrait être préférable: (1) Vous souhaitez mettre en cache les résultats de la méthode. Il est préférable de ne pas utiliser un objet entier comme clé de cache ou de vérifier les propriétés d'un objet à chaque appel. (2) Vous souhaitez que la méthode / fonction s'exécute de manière asynchrone, ce qui peut impliquer de sérialiser les arguments et, par exemple, de les stocker dans une table. Plus facile et plus simple de rechercher un entier qu'un objet blob lors de la gestion des travaux en attente.
GladstoneKeep
L'anti-modèle obsessionnel primitif pourrait être horrible pour les tests unitaires.
Samuel
82

La première signature est supérieure car elle permet l'encapsulation de la logique liée à l'utilisateur dans l' Userobjet même. Il n’est pas avantageux d’avoir une logique pour la construction du tuple "id, name, password" sur votre base de code. De plus, cela complique la isAdminfonction: que se passe-t-il si quelqu'un passe dans un idfichier qui ne correspond pas au name, par exemple? Que faire si vous voulez vérifier si un utilisateur donné est un administrateur d’un contexte dans lequel vous ne devriez pas connaître son mot de passe?

En outre, comme une note sur votre troisième point en faveur du style avec des arguments supplémentaires; il peut en résulter "moins de code dans la fonction", mais où va cette logique? Ça ne peut pas disparaître! Au lieu de cela, il est réparti autour de chaque endroit où la fonction est appelée. Pour enregistrer cinq lignes de code au même endroit, vous avez payé cinq lignes par utilisation de la fonction!

asthasr
la source
45
Selon votre logique, ça ne serait pas user.IsAdmin()mieux?
Anders Arpi
13
Oui absolument.
Asthasr
6
@ AndersHolmström Cela dépend, testez-vous si l'utilisateur est un administrateur en général, ou simplement un administrateur pour la page / le formulaire actuel sur lequel vous vous trouvez. Si en général, alors oui, ce serait mieux, mais si vous testez uniquement IsAdminune forme ou une fonction spécifique, cela ne devrait pas être lié à l'utilisateur
Rachel
40
@ Anders: non, cela ne devrait pas. Un utilisateur ne devrait pas décider s'il s'agit d'un administrateur ou non. Le système de privilèges ou de sécurité devrait. Quelque chose est étroitement lié à une classe ne signifie pas que c'est une bonne idée de faire partie de cette classe
Marjan Venema
11
@MarjanVenema - l'idée que la classe User ne doit pas "décider" si une instance est un administrateur me semble être un peu culte du cargo. Vous ne pouvez vraiment pas faire cette généralisation si fortement. Si vos besoins sont complexes, il est peut-être avantageux de les intégrer dans un "système" séparé, mais en citant KISS + YAGNI, je voudrais prendre cette décision face à cette complexité, et non en règle générale :-). Et notez que même si la logique ne fait pas "partie" de User, il peut toujours être utile, pour des raisons pratiques, d’avoir une passerelle sur User qui ne fait que déléguer à un système plus complexe.
Eamon Nerbonne
65

Le premier, mais pas (seulement) pour les raisons que les autres ont données.

public bool IsAdmin(User user);

Ceci est le type sûr. D'autant plus que l'utilisateur est un type que vous avez défini vous-même, il y a peu ou pas d'occasions de transposer ou d'activer des arguments. Il fonctionne clairement sur les utilisateurs et n'est pas une fonction générique acceptant les entiers et les deux chaînes. C’est une grande partie de l’intérêt d’utiliser un langage sûr comme Java ou C # auquel votre code ressemble. Si vous posez des questions sur une langue spécifique, vous pouvez ajouter une étiquette pour cette langue à votre question.

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

Ici, tout int et deux chaînes feront l'affaire. Qu'est-ce qui vous empêche de transposer le nom et le mot de passe? La seconde demande plus de travail et permet plus d’erreurs.

Vraisemblablement, les deux fonctions requièrent que user.getId (), user.getName () et user.getPassword () soient appelées, soit à l'intérieur de la fonction (dans le premier cas), soit avant l'appel de la fonction (dans le second); la quantité de couplage est la même de toute façon. En fait, cette fonction n'étant valide que sur les utilisateurs, en Java, j'éliminerais tous les arguments et en ferais une méthode d'instance sur les objets utilisateur:

user.isAdmin();

Étant donné que cette fonction est déjà étroitement liée aux utilisateurs, il est logique d’en faire une partie de ce qu’un utilisateur est.

PS Je suis sûr que ce n'est qu'un exemple, mais il semble que vous stockiez un mot de passe. Vous devriez plutôt stocker uniquement un hachage de mot de passe cryptographiquement sécurisé. Lors de la connexion, le mot de passe fourni doit être haché de la même manière et comparé au hachage stocké. L'envoi de mots de passe en clair doit être évité. Si vous ne respectez pas cette règle et que isAdmin (int Str Str) devait enregistrer le nom d'utilisateur, puis si vous transposiez le nom et le mot de passe dans votre code, le mot de passe pourrait être enregistré à la place, ce qui crée un problème de sécurité (n'écrivez pas de mots de passe dans les journaux ) et constitue un autre argument en faveur du passage d’un objet à la place de ses composants (ou de l’utilisation d’une méthode de classe).

GlenPeterson
la source
11
Un utilisateur ne devrait pas décider s'il s'agit d'un administrateur ou non. Le système de privilèges ou de sécurité devrait. Quelque chose qui est étroitement associé à une classe ne signifie pas que c'est une bonne idée de l'intégrer à cette classe.
Marjan Venema
6
@MarjanVenema L'utilisateur ne décide rien. L'objet / la table utilisateur stocke des données sur chaque utilisateur. Les utilisateurs réels ne peuvent pas tout changer d'eux-mêmes. Même lorsque l'utilisateur modifie un élément auquel il est autorisé, des alertes de sécurité, telles qu'un courrier électronique, peuvent être déclenchées s'il modifie son adresse électronique, son identifiant utilisateur ou son mot de passe. Utilisez-vous un système où les utilisateurs sont autorisés par magie à tout changer concernant certains types d’objets? Je ne suis pas familier avec un tel système.
GlenPeterson
2
@GlenPeterson: me trompez-vous délibérément? Lorsque j'ai dit utilisateur, je voulais bien sûr parler de la classe d'utilisateurs.
Marjan Venema
2
@GlenPeterson Complètement hors sujet, mais plusieurs tours de fonctions de hachage et de cryptographie affaibliront les données.
Gusdor
3
@MarjanVenema: Je ne suis pas délibérément difficile. Je pense que nous avons simplement des perspectives très différentes et je voudrais mieux comprendre la vôtre. J'ai ouvert une nouvelle question où cela peut être mieux discuté: programmers.stackexchange.com/questions/216443/…
GlenPeterson
6

Si votre langue de choix ne passe pas les structures par valeur, alors (User user)variante semble meilleure. Et si un jour vous décidiez de supprimer le nom d'utilisateur et d'utiliser simplement l'ID / mot de passe pour identifier l'utilisateur?

En outre, cette approche conduit inévitablement à des appels de méthode longs et excessifs, ou à des incohérences. Passer 3 arguments est-il correct? Que diriez-vous de 5? Ou 6? Pourquoi penser à cela, si passer un objet est peu coûteux en termes de ressources (même moins cher que de passer 3 arguments, probablement)?

Et je suis (en quelque sorte) en désaccord avec le fait que la deuxième approche donne au lecteur plus d'informations - intuitivement, l'appel à la méthode demande "si l'utilisateur possède des droits d'administrateur", et non "si la combinaison donnée id / nom / mot de passe possède des droits d'administrateur".

Ainsi, à moins que le passage de l' Userobjet ne conduise à la copie d'une grande structure, la première approche est plus propre et plus logique.

Maciej Stachowski
la source
3

J'aurais probablement les deux. Le premier en tant qu'API externe, avec un espoir de stabilité dans le temps. Le second en tant qu'implémentation privée appelée par l'API externe.

Si, par la suite, je devais modifier la règle de vérification, il me suffirait d'écrire une nouvelle fonction privée avec une nouvelle signature appelée par l'externe.

Cela a l'avantage d'être facile à changer d'implémentation interne. En outre, il est très courant qu'à un moment donné, les deux fonctions soient disponibles simultanément et que vous appeliez l'une ou l'autre en fonction d'un changement de contexte externe (par exemple, d'anciens utilisateurs coexistants et de nouveaux utilisateurs possédant des champs différents peuvent coexister).

En ce qui concerne le titre de la question, dans un cas comme dans l'autre, vous fournissez le minimum d'informations nécessaires. Un utilisateur semble être la plus petite structure de données liée à l'application contenant les informations. Les trois champs id, password et name semblent être les plus petites données d'implémentation réellement nécessaires, mais ce ne sont pas vraiment des objets de niveau Application.

En d'autres termes, si vous utilisiez une base de données, un utilisateur serait un enregistrement, tandis que l'id, le mot de passe et le login seraient les champs de cet enregistrement.

kriss
la source
Je ne vois pas la raison de la méthode privée. Pourquoi maintenir les deux? Si la méthode privée a besoin d'arguments différents, vous devrez toujours modifier un argument public. Et vous allez le tester en public. Et non, ce n'est pas habituel que vous ayez besoin des deux à un moment donné. Vous devinez ici - YAGNI.
Piotr Perak
Vous supposez par exemple que la structure de données utilisateur ne comporte aucun autre champ depuis le début. Ce n'est généralement pas vrai. La deuxième fonction indique clairement quelle partie interne de l'utilisateur est vérifiée pour voir s'il s'agit d'un administrateur. Les cas d'utilisation typiques peuvent être du type: si l'heure de création de l'utilisateur est antérieure à une date donnée, appelez l'ancienne règle, sinon appelez la nouvelle règle (qui peut dépendre d'autres parties de l'utilisateur, ou dépend de la même chose mais en utilisant une autre règle). En divisant le code de cette manière, vous obtiendrez deux fonctions minimales: une API conceptuelle minimale et une fonction d'implémentation minimale.
Kriss
En d'autres termes, de cette manière, les parties de l'utilisateur utilisées ou non par la signature de la fonction interne seront immédiatement visibles. Dans le code de production, cela n’est pas toujours évident. Je travaille actuellement sur une base de code volumineuse avec un historique de maintenance de plusieurs années et ce type de scission est très utile pour la maintenance à long terme. Si vous n'avez pas l'intention de maintenir le code, il est en effet inutile (mais même fournir une API conceptuelle stable est probablement aussi inutile).
Kriss
Un autre mot: je ne testerai certainement pas la méthode interne par une méthode externe. C'est une mauvaise pratique de test.
Kriss
1
C'est une bonne pratique, et il y a un nom pour cela. C'est ce qu'on appelle le "principe de responsabilité unique". Il peut être appliqué aux classes et aux méthodes. Avoir des méthodes avec une seule responsabilité est un bon moyen de créer un code plus modulaire et maintenable. Dans ce cas, une tâche sélectionne un ensemble de données de base à partir de l'objet utilisateur, et une autre tâche vérifie cet ensemble de données de base pour voir si l'utilisateur est un administrateur. En suivant ce principe, vous bénéficiez de la possibilité de composer des méthodes et d’éviter la duplication de code. Cela signifie également, comme l'a déclaré @kriss, que vous obtenez de meilleurs tests unitaires.
diegoreymendez
3

Il existe un point négatif pour l'envoi de classes (types de référence) aux méthodes, à savoir la vulnérabilité d'affectation en masse . Imaginez qu'au lieu de IsAdmin(User user)méthode, j'ai UpdateUser(User user)méthode.

Si Userclasse a une propriété booléenne appelée IsAdminet que je ne la vérifie pas lors de l'exécution de ma méthode, je suis vulnérable à l'affectation en masse. GitHub a été attaqué par cette signature même en 2012.

Saeed Neamati
la source
2

J'irais avec cette approche:

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

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

public interface IUser
{
    string Username {get;}
    string Password {get;}
    IID Id {get;}
}
  • L'utilisation du type d'interface en tant que paramètre pour cette fonction vous permet de passer des objets utilisateur, de définir des objets utilisateur fictifs à tester, et vous offre davantage de flexibilité pour l'utilisation et la maintenance de cette fonction.

  • J'ai également ajouté le mot clé thispour que la fonction devienne une méthode d'extension de toutes les classes IUser; De cette façon, vous pouvez écrire du code de manière plus conviviale et utiliser cette fonction dans les requêtes Linq. Plus simple encore serait de définir cette fonction dans IUser et User, mais je suppose qu’il ya une raison pour laquelle vous avez décidé de placer cette méthode en dehors de cette classe?

  • J'utilise l'interface personnalisée à la IIDplace intcar cela vous permet de redéfinir les propriétés de votre identifiant; par exemple , si vous avez besoin jamais de changer de intà long, Guidou autre chose. C'est probablement exagéré, mais il est toujours bon d'essayer de rendre les choses flexibles de manière à ne pas être obligé de prendre des décisions précoces.

  • NB: Votre objet IUser peut se trouver dans un espace de nom et / ou un assemblage différent de la classe User. Vous avez donc la possibilité de garder votre code utilisateur complètement séparé de votre code IsAdmin, avec le partage d'une seule bibliothèque référencée. Exactement ce que vous faites il y a un appel pour savoir comment vous pensez que ces objets seront utilisés.

JohnLBevan
la source
3
IUser est inutile ici et ne fait qu'ajouter à la complexité. Je ne vois pas pourquoi vous ne pourriez pas utiliser le véritable utilisateur dans les tests.
Piotr Perak
1
Il ajoute une flexibilité future avec très peu d'effort. Par conséquent, même s'il n'apporte pas de valeur ajoutée, il ne fait pas mal. Personnellement, je préfère toujours le faire, car cela évite de penser à de futures possibilités pour tous les scénarios (ce qui est pratiquement impossible compte tenu de la nature imprévisible des exigences, et il faudra beaucoup plus de temps pour obtenir une réponse partiellement satisfaisante que pour coller dans une interface).
JohnLBevan
@Peri une vraie classe d'utilisateurs peut être complexe à créer, prendre une interface vous permet de la simuler afin que vous puissiez garder vos tests simples
jk.
@JohnLBevan: YAGNI !!!
Piotr Perak
@jk .: S'il est difficile de créer un utilisateur, alors quelque chose ne va pas
Piotr Perak
1

Avertissements:

Pour ma réponse, je vais me concentrer sur le problème du style et oublier si un identifiant, un identifiant et un mot de passe en clair constituent un bon jeu de paramètres à utiliser du point de vue de la sécurité. Vous devriez pouvoir extrapoler ma réponse à n'importe quel ensemble de données de base que vous utilisez pour connaître les privilèges de l'utilisateur ...

Je considère également user.isAdmin()être équivalent à IsAdmin(User user). C'est un choix à faire pour vous.

Répondre:

Ma recommandation est à l'un ou l'autre utilisateur seulement:

public bool IsAdmin(User user);

Ou utilisez les deux, comme suit:

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

Raisons de la méthode publique:

Lorsque vous écrivez des méthodes publiques, c'est généralement une bonne idée de penser à la façon dont elles seront utilisées.

Dans ce cas particulier, vous appelez généralement cette méthode pour déterminer si un utilisateur donné est un administrateur. C'est une méthode dont vous avez besoin. Vous ne voulez vraiment pas que chaque appelant doive choisir les bons paramètres à envoyer (et risquer des erreurs en raison de la duplication de code).

Raisons de la méthode privée:

La méthode privée ne recevant que le minimum de paramètres requis peut parfois être une bonne chose. Parfois pas tellement.

L'un des avantages de la scission de ces méthodes est que vous pouvez appeler la version privée à partir de plusieurs méthodes publiques de la même classe. Par exemple, si vous vouliez (pour une raison quelconque) avoir une logique légèrement différente pour Localuseret RemoteUser(peut-être un paramètre pour activer / désactiver les administrateurs distants?):

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);

En outre, si pour une raison quelconque vous devez rendre publique la méthode privée, vous le pouvez. C'est aussi simple que de changer privatepour public. Cela peut ne pas sembler être un gros avantage pour cette affaire, mais parfois, c'est vraiment le cas.

De plus, lors de tests unitaires, vous serez capable de faire beaucoup plus de tests atomiques. Il est généralement très agréable de ne pas avoir à créer un objet utilisateur complet pour exécuter des tests sur cette méthode. Et si vous souhaitez une couverture complète, vous pouvez tester les deux appels.

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

est mauvais pour les raisons énumérées. Cela ne veut pas dire

public bool IsAdmin(User user);

est automatiquement bon. En particulier , si l' utilisateur est un objet ayant des méthodes telles que : getOrders(), getFriends(), getAdresses(), ...

Vous pouvez envisager de refactoriser le domaine avec un type UserCredentials contenant uniquement l'ID, le nom, le mot de passe et de le transmettre à IsAdmin au lieu de toutes les données utilisateur inutiles.

tuse
la source