Dovresti sempre passare i dati minimi necessari in una funzione in casi come questo


81

Diciamo che ho una funzione IsAdminche controlla se un utente è un amministratore. Diciamo anche che il controllo dell'amministratore viene eseguito abbinando ID utente, nome e password a una sorta di regola (non importante).

Nella mia testa ci sono quindi due possibili firme di funzione per questo:

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

Vado spesso per il secondo tipo di firma, pensando che:

  • La firma della funzione fornisce al lettore molte più informazioni
  • La logica contenuta all'interno della funzione non deve conoscere la Userclasse
  • Di solito risulta un po 'meno codice all'interno della funzione

Tuttavia a volte metto in dubbio questo approccio e mi rendo anche conto che a un certo punto diventerebbe ingombrante. Se per esempio una funzione mappasse tra dieci diversi campi oggetto in un bool risultante, invierei ovviamente l'intero oggetto. Ma a parte un esempio severo come quello non riesco a vedere un motivo per passare l'oggetto reale.

Gradirei qualsiasi argomento per entrambi gli stili, nonché eventuali osservazioni generali che potresti offrire.

Programmo in stili sia orientati agli oggetti che funzionali, quindi la domanda dovrebbe essere vista come relativa a tutti gli idiomi.


40
Fuori tema: per curiosità, perché lo stato "è admin" di un utente dipende dalla sua password?
stakx,

43
@ syb0rg Wrong. In C # questa è la convenzione di denominazione .
magnattico

68
@ syb0rg Giusto, non ha specificato la lingua, quindi penso che sia piuttosto strano dirgli che sta violando la convenzione di una determinata lingua.
magnattico

31
@ syb0rg L'affermazione generale che "i nomi delle funzioni non devono iniziare con una lettera maiuscola" è errata, perché ci sono lingue dove dovrebbero. Comunque, non intendevo offenderti, stavo solo cercando di chiarire. Questo sta diventando quasi fuori tema per la domanda stessa. Non vedo la necessità, ma se vuoi continuare lascia passare il dibattito alla chat .
magnattico

6
Proprio come un punto generale, far rotolare la propria sicurezza di solito è una cosa negativa (tm) . La maggior parte delle lingue e dei framework ha un sistema di sicurezza / autorizzazione disponibile e ci sono buoni motivi per usarli anche quando vuoi qualcosa di più semplice.
James Snell,

Risposte:


123

Personalmente preferisco il primo metodo di just IsAdmin(User user)

È molto più facile da usare e se i criteri per IsAdmin cambiano in un secondo momento (forse in base a ruoli o isActive), non è necessario riscrivere la firma del metodo ovunque.

Probabilmente è anche più sicuro in quanto non stai pubblicizzando quali proprietà determinano se un utente è un amministratore o meno, o passano intorno alla proprietà della password ovunque. E Syrion sottolinea che cosa succede quando il tuo idnon corrisponde a name/ password?

La lunghezza del codice all'interno di una funzione non dovrebbe davvero importare se il metodo fa il suo lavoro e preferirei avere un codice dell'applicazione più breve e più semplice rispetto al codice del metodo helper.


3
Penso che il problema del refactoring sia un ottimo punto - grazie.
Anders Arpi,

1
Se non lo avessi fatto, avrei discusso la firma del metodo. Questo è particolarmente utile quando ci sono molte dipendenze dal tuo metodo che sono gestite da altri programmatori. Questo aiuta a tenere le persone lontane l'una dall'altra. +1
jmort253,

1
Sono d'accordo con questa risposta, ma mi permetta di bandiera due situazioni in cui l'altro approccio ( "Dati minima") potrebbe essere preferibile: (1) che si desidera memorizzare nella cache i risultati di metodo. Meglio non usare un intero oggetto come chiave della cache o controllare le proprietà di un oggetto ad ogni chiamata. (2) Si desidera che il metodo / la funzione vengano eseguiti in modo asincrono, il che potrebbe comportare la serializzazione degli argomenti e, diciamo, la loro memorizzazione in una tabella. È sempre più semplice cercare un numero intero rispetto a un BLOB oggetto durante la gestione dei lavori in sospeso.
Gladstone

L'anti-schema dell'ossessione primitiva potrebbe essere orribile per i test unitari.
Samuel,

82

La prima firma è superiore perché consente l'incapsulamento della logica relativa all'utente Usernell'oggetto stesso. Non è utile avere una logica per la costruzione della tupla "id, nome, password" sparpagliata sulla tua base di codice. Inoltre, complica la isAdminfunzione: cosa succede se qualcuno passa in un caso idche non corrisponde al name, per esempio? Cosa succede se si desidera verificare se un determinato utente è un amministratore di un contesto in cui non si deve conoscere la propria password?

Inoltre, come nota sul terzo punto a favore dello stile con argomenti extra; può comportare "meno codice all'interno della funzione", ma dove va questa logica? Non può semplicemente svanire! Invece, si sviluppa in ogni singolo posto in cui viene chiamata la funzione. Per salvare cinque righe di codice in un unico posto, hai pagato cinque righe per ogni utilizzo della funzione!


45
Seguendo la tua logica, non user.IsAdmin()sarebbe molto meglio?
Anders Arpi,

13
Si assolutamente.
asthasr,

6
@ AndersHolmström Dipende, stai testando se l'utente è un amministratore in generale, o solo un amministratore per la pagina / modulo corrente in cui ti trovi. Se in generale, allora sì, sarebbe meglio, ma se stai solo testando IsAdminuna forma o una funzione specifica, ciò non dovrebbe essere correlato all'utente
Rachel,

40
@Anders: no, non dovrebbe. Un utente non dovrebbe decidere se si tratta di un amministratore o meno. I privilegi o il sistema di sicurezza dovrebbero. Qualcosa di strettamente associato a una classe non significa che sia una buona idea farne parte
Marjan Venema,

11
@MarjanVenema - l'idea che la classe User non debba "decidere" se un'istanza è un amministratore mi sembra un po 'cult-cult. Non puoi davvero fare questa generalizzazione così fortemente. Se le tue esigenze sono complesse, allora forse è utile incapsularle in un "sistema" separato, ma citando KISS + YAGNI mi piacerebbe prendere quella decisione effettivamente affrontata con quella complessità, e non come regola generale :-). E nota che anche se la logica non è "parte" dell'Utente, può comunque essere utile, in pratica, avere un passthrough sull'Utente che delega semplicemente a un sistema più complesso.
Eamon Nerbonne,

65

Il primo, ma non (solo) per i motivi che altri hanno indicato.

public bool IsAdmin(User user);

Questo è sicuro. Soprattutto perché l'Utente è un tipo che hai definito tu stesso, c'è poca o nessuna opportunità di trasporre o cambiare argomento. Funziona chiaramente sugli utenti e non è una funzione generica che accetta int e due stringhe. Questa è una parte importante dell'uso di un linguaggio sicuro come Java o C # a cui assomiglia il tuo codice. Se stai chiedendo una lingua specifica, potresti voler aggiungere un tag per quella lingua alla tua domanda.

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

Qui, qualsiasi int e due stringhe faranno. Cosa ti impedisce di trasporre il nome e la password? Il secondo è più lavoro da utilizzare e consente maggiori opportunità di errore.

Presumibilmente, entrambe le funzioni richiedono che user.getId (), user.getName () e user.getPassword () siano chiamati, sia all'interno della funzione (nel primo caso) o prima di chiamare la funzione (nel secondo), quindi il la quantità di accoppiamento è uguale in entrambi i modi. In realtà, poiché questa funzione è valida solo per gli utenti, in Java eliminerei tutti gli argomenti e lo renderei un metodo di istanza sugli oggetti utente:

user.isAdmin();

Poiché questa funzione è già strettamente associata agli Utenti, ha senso renderla parte di ciò che un utente è.

PS Sono sicuro che questo è solo un esempio, ma sembra che tu stia memorizzando una password. Dovresti invece archiviare solo un hash password crittograficamente sicuro. Durante l'accesso, la password fornita deve essere sottoposta a hash nello stesso modo e confrontata con l'hash memorizzato. È necessario evitare l'invio di password in chiaro. Se si viola questa regola e isAdmin (int Str Str) dovesse registrare il nome utente, quindi se si trasponevano il nome e la password nel codice, la password potrebbe essere registrata invece che crea un problema di sicurezza (non scrivere password nei registri ) ed è un altro argomento a favore del passaggio di un oggetto anziché delle sue parti componenti (o utilizzando un metodo di classe).


11
Un utente non dovrebbe decidere se si tratta di un amministratore o meno. I privilegi o il sistema di sicurezza dovrebbero. Qualcosa che è strettamente associato a una classe non significa che sia una buona idea farne parte.
Marjan Venema,

6
@MarjanVenema L'utente non sta decidendo nulla. L'oggetto / tabella utente memorizza i dati relativi a ciascun utente. Gli utenti reali non possono cambiare tutto di se stessi. Anche quando l'utente modifica qualcosa a cui è consentito, può attivare avvisi di sicurezza come un'e-mail se cambiano il loro indirizzo e-mail o il loro ID utente o password. Stai usando un sistema in cui gli utenti sono autorizzati magicamente a cambiare tutto su determinati tipi di oggetti? Non ho familiarità con un tale sistema.
GlenPeterson,

2
@GlenPeterson: mi stai fraintendendo deliberatamente? Quando ho detto utente, ovviamente intendevo la classe utente.
Marjan Venema,

2
@GlenPeterson Totalmente fuori tema, ma più cicli di hashing e funzioni crittografiche indeboliranno i dati.
Gusdor,

3
@MarjanVenema: non sono deliberatamente difficile. Penso che abbiamo solo prospettive molto diverse e vorrei capire meglio le tue. Ho aperto una nuova domanda in cui questo può essere meglio discusso: programmers.stackexchange.com/questions/216443/…
GlenPeterson

6

Se la tua lingua preferita non passa le strutture per valore, allora la (User user)variante sembra migliore. Cosa succederebbe se un giorno decidessi di eliminare il nome utente e utilizzare semplicemente l'ID / la password per identificare l'utente?

Inoltre, questo approccio porta inevitabilmente a chiamate di metodo lunghe e esagerate o incoerenze. Passare 3 argomenti va bene? Che ne dici di 5? O 6? Perché pensare a questo, se passare un oggetto costa poco in termini di risorse (anche più economico del passare 3 argomenti, probabilmente)?

E io (in un certo senso) non sono d'accordo sul fatto che il secondo approccio fornisca al lettore maggiori informazioni - intuitivamente, la chiamata del metodo chiede "se l'utente ha i diritti di amministratore", non "se la data combinazione id / nome / password ha diritti di amministratore".

Quindi, a meno che il passaggio Userdell'oggetto non comporterebbe la copia di una grande struttura, il primo approccio è più pulito e logico per me.


3

Probabilmente avrei entrambi. Il primo come API esterna, con qualche speranza di stabilità nel tempo. Il secondo come implementazione privata chiamata dall'API esterna.

Se in un secondo momento dovessi modificare la regola di controllo, scriverei semplicemente una nuova funzione privata con una nuova firma chiamata da quella esterna.

Questo ha il vantaggio della facilità nel cambiare l'implementazione interna. Inoltre è davvero normale che a volte sia necessario disporre di entrambe le funzioni disponibili contemporaneamente e chiamare l'una o l'altra a seconda di alcune modifiche al contesto esterno (ad esempio si potrebbero avere coesistenti vecchi utenti e nuovi utenti con campi diversi).

Per quanto riguarda il titolo della domanda, in entrambi i casi stai fornendo le informazioni minime necessarie. Un utente sembra essere la più piccola struttura di dati relativa all'applicazione contenente le informazioni. I tre campi id, password e nome sembrano essere i dati di implementazione più piccoli effettivamente necessari, ma questi non sono realmente oggetti a livello di applicazione.

In altre parole, se si trattasse di un database, un utente sarebbe un record, mentre id, password e login sarebbero i campi in quel record.


Non vedo il motivo del metodo privato. Perché mantenere entrambi? Se il metodo privato richiede argomenti diversi, dovrai comunque modificarne uno pubblico. E lo testerai attraverso quello pubblico. E no, non è normale che tu abbia bisogno di entrambi in un secondo momento. Stai indovinando qui - YAGNI.
Piotr Perak,

Ad esempio, si presuppone che la struttura dei dati dell'utente non abbia altri campi dall'inizio. Di solito non è vero. Le seconde funzioni chiariscono quale parte interna dell'utente viene controllata per vedere se si tratta di un amministratore. I casi d'uso tipici possono essere del tipo: se il tempo di creazione dell'utente è precedente a una data, chiama la vecchia regola, se non chiama una nuova regola (che può dipendere da altre parti dell'utente o dipende dalla stessa ma utilizzando un'altra regola). Dividendo il codice in questo modo otterrai due funzioni minime: un'API concettuale minima e una funzione di implementazione minima.
Kriss,

in altre parole in questo modo sarà immediatamente visibile dalla firma della funzione interna quali parti dell'utente sono utilizzate o meno. Nel codice di produzione ciò non è sempre evidente. Attualmente sto lavorando su una base di codice di grandi dimensioni con diversi anni di storia della manutenzione e questo tipo di suddivisione è molto utile per la manutenzione a lungo termine. Se non si intende mantenere il codice, è effettivamente inutile (ma anche fornire un'API concettuale stabile è probabilmente inutile).
Kriss,

Un'altra parola: certamente non testerò in unità il metodo interno tramite un metodo esterno. Questa è una brutta pratica di test.
Kriss,

1
Questa è una buona pratica e c'è un nome per questo. Si chiama "principio di responsabilità singola". Può essere applicato sia a classi che a metodi. Avere metodi con una sola responsabilità è un buon modo per rendere il codice più modulare e gestibile. In questo caso, un'attività sta selezionando un set di dati di base dall'oggetto utente e un'altra attività sta controllando quel set di dati di base per vedere se l'utente è un amministratore. Seguendo questo principio si ottiene il vantaggio di poter comporre metodi ed evitare la duplicazione del codice. Questo significa anche, come affermato da @kriss, che si ottengono test unitari migliori.
Diegoreymendez,

3

Esiste un punto negativo per inviare classi (tipi di riferimento) ai metodi, ovvero la vulnerabilità legata all'assegnazione di massa . Immagina che invece del IsAdmin(User user)metodo, io abbia il UpdateUser(User user)metodo.

Se la Userclasse ha una proprietà booleana chiamata IsAdmine se non la controllo durante l'esecuzione del mio metodo, allora sono vulnerabile all'assegnazione di massa. GitHub è stato attaccato da questa firma nel 2012.


2

Andrei con questo approccio:

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

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

public interface IUser
{
    string Username {get;}
    string Password {get;}
    IID Id {get;}
}
  • L'uso del tipo di interfaccia come parametro per questa funzione consente di passare oggetti utente, definire oggetti utente falsi per i test e offrire maggiore flessibilità sull'uso e sulla manutenzione di questa funzione.

  • Ho anche aggiunto la parola chiave thisper rendere la funzione un metodo di estensione di tutte le classi IUser; in questo modo è possibile scrivere codice in un modo più intuitivo per OO e utilizzare questa funzione nelle query Linq. Sarebbe ancora più semplice definire questa funzione in IUser e User, ma suppongo che ci sia qualche ragione per cui hai deciso di mettere questo metodo al di fuori di quella classe?

  • Uso l'interfaccia personalizzata IIDanziché in intquanto ciò consente di ridefinire le proprietà del proprio ID; ad esempio, si dovrebbe mai bisogno di cambiare da inta long, Guid, o qualcos'altro. Questo è probabilmente eccessivo, ma è sempre bene cercare di rendere le cose flessibili in modo da non essere bloccati nelle decisioni iniziali.

  • NB: l'oggetto IUser può trovarsi in uno spazio dei nomi e / o assembly diverso rispetto alla classe User, quindi hai la possibilità di mantenere il tuo codice User completamente separato dal tuo codice IsAdmin, condividendo solo una libreria di riferimento. Esattamente quello che fai c'è una richiesta per sospettare che questi oggetti vengano usati.


3
IUser è inutile qui e aggiunge solo complessità. Non vedo perché non è stato possibile utilizzare l'utente reale nei test.
Piotr Perak,

1
Aggiunge flessibilità futura con il minimo sforzo, quindi mentre non aggiunge valore al momento non fa male. Personalmente preferisco farlo sempre poiché evita di dover riflettere sulle possibilità future per tutti gli scenari (il che è praticamente impossibile data la natura imprevedibile dei requisiti e ci vorrà molto più tempo per ottenere anche una risposta parzialmente buona di quanto dovrebbe attaccare in un'interfaccia).
JohnLBevan,

@Peri una vera classe utente potrebbe essere complessa da creare, prendendo un'interfaccia ti permette di deriderla in modo da poter semplificare i test
jk.

@JohnLBevan: YAGNI !!!
Piotr Perak,

@jk .: se è difficile costruire un utente, allora qualcosa non va
Piotr Perak,

1

Avvertenze:

Per la mia risposta, mi concentrerò sul problema dello stile e dimenticherò se un ID, login e password semplice sono un buon insieme di parametri da utilizzare, dal punto di vista della sicurezza. Dovresti essere in grado di estrapolare la mia risposta a qualsiasi set di dati di base che stai utilizzando per capire i privilegi dell'utente ...

Considero anche user.isAdmin()equivalente a IsAdmin(User user). Questa è una scelta che devi fare.

Rispondere:

La mia raccomandazione è di entrambi gli utenti:

public bool IsAdmin(User user);

Oppure usa entrambi, sulla falsariga di:

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

Ragioni per il metodo pubblico:

Quando si scrivono metodi pubblici, di solito è una buona idea pensare a come verranno utilizzati.

Per questo caso particolare, il motivo per cui di solito si chiama questo metodo è capire se un determinato utente è un amministratore. Questo è un metodo che ti serve. Non vuoi davvero che ogni chiamante debba scegliere i parametri giusti da inviare (e rischiare errori dovuti alla duplicazione del codice).

Ragioni per il metodo privato:

Il metodo privato che riceve solo il set minimo di parametri richiesti può essere utile a volte. A volte non così tanto.

Una delle cose positive della divisione di questi metodi è che puoi chiamare la versione privata da diversi metodi pubblici nella stessa classe. Ad esempio, se volessi (per qualsiasi motivo) avere una logica leggermente diversa per Localusere RemoteUser(forse c'è un'impostazione per attivare / disattivare gli amministratori remoti?):

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

Inoltre, se per qualsiasi motivo è necessario rendere pubblico il metodo privato ... è possibile. È facile come passare privatea public. Potrebbe non sembrare un grande vantaggio per questo caso, ma a volte lo è davvero.

Inoltre, durante i test unitari sarai in grado di effettuare molti più test atomici. Di solito è molto bello non dover creare un oggetto utente completo per eseguire test su questo metodo. E se desideri una copertura completa, puoi testare entrambe le chiamate.


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

è male per i motivi elencati. Questo non significa

public bool IsAdmin(User user);

è automaticamente buono. In particolare, se l'utente è un oggetto con metodi come: getOrders(), getFriends(), getAdresses(), ...

È possibile considerare il refactoring del dominio con un tipo UserCredentials contenente solo ID, nome, password e passandolo a IsAdmin anziché tutti i dati utente non necessari.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.