La modifica di un oggetto passato per riferimento è una cattiva pratica?


12

In passato, in genere ho fatto la maggior parte della mia manipolazione di un oggetto all'interno del metodo principale che viene creato / aggiornato, ma ultimamente mi sono ritrovato ad adottare un approccio diverso e sono curioso di sapere se è una cattiva pratica.

Ecco un esempio Diciamo che ho un repository che accetta Userun'entità, ma prima di inserire l'entità, chiamiamo alcuni metodi per assicurarci che tutti i suoi campi siano impostati su ciò che vogliamo. Ora, piuttosto che chiamare metodi e impostare i valori di campo all'interno del metodo Inserisci, chiamo una serie di metodi di preparazione che modellano l'oggetto prima del suo inserimento.

Vecchio metodo:

public void InsertUser(User user) {
    user.Username = GenerateUsername(user);
    user.Password = GeneratePassword(user);

    context.Users.Add(user);
}

Nuovi metodi:

public void InsertUser(User user) {
    SetUsername(user);
    SetPassword(user);

    context.Users.Add(user);
}

private void SetUsername(User user) {
    var username = "random business logic";

    user.Username = username;
}

private void SetPassword(User user) {
    var password = "more business logic";

    user.Password = password;
}

Fondamentalmente, la pratica di impostare il valore di una proprietà da un altro metodo è una cattiva pratica?


6
Solo così si dice ... non hai passato nulla per riferimento qui. Passare riferimenti per valore non è affatto la stessa cosa.
cHao,

4
@cHao: questa è una distinzione senza differenze. Il comportamento del codice è lo stesso a prescindere.
Robert Harvey,

2
@JDDavis: Fondamentalmente, l'unica vera differenza tra i tuoi due esempi è che hai dato un nome significativo al nome utente impostato e impostato le azioni della password.
Robert Harvey,

1
Tutte le tue chiamate sono di riferimento qui. Devi usare un tipo di valore in C # per passare per valore.
Frank Hileman,

2
@FrankHileman: tutte le chiamate hanno valore qui. Questo è il valore predefinito in C #. Passare un riferimento e passare per riferimento sono bestie diverse, e la distinzione conta. Se usersono stati passati per riferimento, il codice potrebbe tirare fuori delle mani del chiamante e sostituirlo semplicemente dicendo, per esempio, user = null;.
cHao,

Risposte:


10

Il problema qui è che a Userpuò effettivamente contenere due cose diverse:

  1. Un'entità utente completa, che può essere passata al tuo archivio dati.

  2. L'insieme di elementi di dati richiesti dal chiamante per iniziare il processo di creazione di un'entità utente. Il sistema deve aggiungere un nome utente e una password prima che sia veramente un utente valido come nel numero 1 sopra.

Ciò comprende una sfumatura non documentata per il tuo modello di oggetto che non è affatto espressa nel tuo sistema di tipi. Devi solo "conoscerlo" come sviluppatore. Non è eccezionale e porta a strani schemi di codice come quello che stai incontrando.

Suggerirei che hai bisogno di due entità, ad esempio una Userclasse e una EnrollRequestclasse. Quest'ultimo può contenere tutto ciò che è necessario sapere per creare un utente. Sembrerebbe la tua classe utente, ma senza il nome utente e la password. Quindi potresti farlo:

public User InsertUser(EnrollRequest request) {
    var userName = GenerateUserName();
    var password = GeneratePassword();

    //You might want to replace this with a factory call, but "new" works here as an example
    var newUser = new User
    (
        request.Name, 
        request.Email, 
        userName, 
        password
    );
    context.Users.Add(user);
    return newUser;
}

Il chiamante inizia con solo le informazioni di iscrizione e recupera l'utente completato dopo che è stato inserito. In questo modo eviti di mutare qualsiasi classe e hai anche una distinzione sicura tra un utente che è inserito e uno che non lo è.


2
È InsertUserprobabile che un metodo con un nome simile abbia l'effetto collaterale dell'inserimento. Non sono sicuro di cosa significhi "icky" in questo contesto. Per quanto riguarda l'utilizzo di un "utente" che non è stato aggiunto, sembra che tu abbia perso il punto. Se non è stato aggiunto, non è un utente, ma solo una richiesta per creare un utente. Sei libero di lavorare con la richiesta, ovviamente.
John Wu,

@candiedorange Questi non sono effetti collaterali, ma gli effetti desiderati della chiamata al metodo. Se non si desidera aggiungere immediatamente, si crea un altro metodo che soddisfa il caso d'uso. Ma non è questo il caso d'uso passato nella domanda, quindi si dice che la preoccupazione non sia importante.
Andy,

@candiedorange Se l'accoppiamento facilita il codice client direi che va bene. Ciò che gli sviluppatori o le pos potrebbero pensare in futuro è irrilevante. Se arriva quel momento, si cambia il codice. Niente è più dispendioso del tentativo di creare codice in grado di gestire qualsiasi possibile caso di utilizzo futuro.
Andy,

5
@CandiedOrange Ti suggerisco di non accoppiare strettamente il sistema di tipi ben definito dell'implementazione con le entità concettuali e semplici dell'impresa. Un concetto aziendale di "utente" può certamente essere implementato in due classi, se non di più, per rappresentare varianti funzionalmente significative. A un utente che non è persistito non è garantito un nome utente univoco, non ha una chiave primaria a cui le transazioni possano essere collegate e non possa nemmeno accedere, quindi direi che comprende una variante significativa. Per un laico, naturalmente, sia EnrollRequest che l'Utente sono "utenti" nella loro lingua vaga.
John Wu,

5

Gli effetti collaterali sono ok purché non vengano inaspettati. Quindi non c'è nulla di sbagliato in generale quando un repository ha un metodo che accetta un utente e cambia lo stato interno dell'utente. Ma IMHO un nome di metodo come InsertUser non comunica chiaramente questo , ed è ciò che lo rende soggetto a errori. Quando si utilizza il repository, mi aspetto una chiamata come

 repo.InsertUser(user);

per modificare lo stato interno dei repository, non lo stato dell'oggetto utente. Questo problema esiste in entrambe le implementazioni, in che modo InsertUserquesto internamente è completamente irrilevante.

Per risolvere questo, potresti farlo

  • inizializzazione separata dall'inserimento (quindi il chiamante InsertUserdeve fornire un Useroggetto completamente inizializzato , oppure

  • costruire l'inizializzazione nel processo di costruzione dell'oggetto utente (come suggerito da alcune delle altre risposte), oppure

  • prova semplicemente a trovare un nome migliore per il metodo che esprime più chiaramente ciò che fa.

In modo da scegliere un nome metodo come PrepareAndInsertUser, OrchestrateUserInsertion, InsertUserWithNewNameAndPasswordo quello che preferite per rendere l'effetto collaterale più chiara.

Naturalmente, un nome di metodo così lungo indica che il metodo sta forse facendo "troppo" (violazione di SRP), ma a volte non si desidera o non si può risolvere facilmente questo problema, e questa è la soluzione meno invadente e pragmatica.


3

Sto esaminando le due opzioni che hai scelto e devo dire che preferisco di gran lunga il vecchio metodo piuttosto che il nuovo metodo proposto. Ci sono alcune ragioni per questo, anche se fondamentalmente fanno la stessa cosa.

In entrambi i casi si sta impostando il tasto user.UserNamee user.Password. Ho delle riserve sulla voce password, ma quelle riserve non sono pertinenti all'argomento in questione.

Implicazioni della modifica di oggetti di riferimento

  • Rende più difficile la programmazione concorrente, ma non tutte le applicazioni sono multithread
  • Tali modifiche possono essere sorprendenti, in particolare se nulla del metodo suggerisce che ciò accadrà
  • Queste sorprese possono rendere più difficile la manutenzione

Vecchio metodo vs. nuovo metodo

Il vecchio metodo ha reso le cose più facili da testare:

  • GenerateUserName()è testabile indipendentemente. È possibile scrivere test con quel metodo e assicurarsi che i nomi siano generati correttamente
  • Se il nome richiede informazioni dall'oggetto utente, è possibile modificare la firma GenerateUserName(User user)e mantenerla

Il nuovo metodo nasconde le mutazioni:

  • Non sai che l' Useroggetto sta cambiando fino a quando non vai in profondità 2 strati
  • Le modifiche Userall'oggetto sono più sorprendenti in quel caso
  • SetUserName()fa più che impostare un nome utente. Questa non è la verità nella pubblicità che rende più difficile per i nuovi sviluppatori scoprire come funzionano le cose nella tua applicazione

1

Sono pienamente d'accordo con la risposta di John Wu. Il suo suggerimento è buono. Ma manca leggermente la tua domanda diretta.

Fondamentalmente, la pratica di impostare il valore di una proprietà da un altro metodo è una cattiva pratica?

Non intrinsecamente.

Non puoi andare troppo lontano, poiché ti imbatterai in comportamenti inaspettati. Ad esempio PrintName(myPerson), non dovrebbe cambiare l'oggetto persona, poiché il metodo implica che è interessato solo a leggere i valori esistenti. Ma questo è un argomento diverso rispetto al tuo caso, poiché SetUsername(user)implica fortemente che imposterà valori.


Questo è in realtà un approccio che uso spesso per i test unit / integrazione, in cui creo un metodo specifico per modificare un oggetto al fine di impostare i suoi valori su una particolare situazione che voglio testare.

Per esempio:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

Mi aspetto esplicitamente che il ArrrangeContractDeletedStatusmetodo cambi lo stato myContractdell'oggetto.

Il vantaggio principale è che il metodo mi consente di testare la cancellazione del contratto con diversi contratti iniziali; ad esempio un contratto con una cronologia di stato lunga o uno che non ha una cronologia di stato precedente, un contratto con una cronologia di stato intenzionalmente errata, un contratto che il mio utente di prova non è autorizzato a eliminare.

Se mi fossi unito CreateEmptyContracte ArrrangeContractDeletedStatusin un unico metodo; Dovrei creare più varianti di questo metodo per ogni contratto diverso che vorrei testare in uno stato cancellato.

E mentre potrei fare qualcosa del genere:

myContract = ArrrangeContractDeletedStatus(myContract);

Questo è o ridondante (dal momento che sto cambiando l'oggetto comunque), o ora sto forzando me stesso a creare un clone profondo myContractdell'oggetto; il che è eccessivamente difficile se vuoi coprire ogni caso (immagina se voglio proprietà di navigazione di diversi livelli. Devo clonarle tutte? Solo l'entità di livello superiore? ... Tante domande, tante aspettative implicite )

Cambiare l'oggetto è il modo più semplice per ottenere ciò che voglio senza dover fare più lavoro solo per evitare di cambiare l'oggetto per principio.


Quindi la risposta diretta alla tua domanda è che non è intrinsecamente una cattiva pratica, a patto che tu non offuschi che il metodo è suscettibile di cambiare l'oggetto passato. Per la tua situazione attuale, questo è reso ampiamente chiaro attraverso il nome del metodo.


0

Trovo il vecchio e il nuovo problematico.

Vecchio modo:

1) InsertUser(User user)

Quando leggo questo nome di metodo che appare nel mio IDE, la prima cosa che mi viene in mente è

Dove viene inserito l'utente?

Il nome del metodo dovrebbe essere letto AddUserToContext. Almeno, questo è ciò che il metodo fa alla fine.

2) InsertUser(User user)

Ciò viola chiaramente il principio della minima sorpresa. Ad esempio, ho fatto il mio lavoro finora e ho creato un'istanza di recente di a Usere gli ho dato un namee impostato un password, sarei colpito dalla sorpresa:

a) questo non solo inserisce un utente in qualcosa

b) inoltre sovverte la mia intenzione di inserire l'utente così com'è; il nome e la password sono stati sovrascritti.

c) ciò indica che si tratta anche di una violazione del principio della responsabilità unica.

Nuovo modo:

1) InsertUser(User user)

Ancora violazione di SRP e principio della minima sorpresa

2) SetUsername(User user)

Domanda

Impostare il nome utente? A cosa?

Meglio: SetRandomNameo qualcosa che riflette l'intenzione.

3) SetPassword(User user)

Domanda

Impostare la password? A cosa?

Meglio: SetRandomPasswordo qualcosa che riflette l'intenzione.


Suggerimento:

Quello che preferirei leggere sarebbe qualcosa del tipo:

public User GenerateRandomUser(UserFactory Uf){
    User u = Uf.GetUser();
    u.Name = GenerateRandomUserName();
    u.Password = GenerateRandomUserPassword();
    return u;
}

...

public void AddUserToContext(User u){
    this.context.Users.Add(u);
}

Per quanto riguarda la tua domanda iniziale:

La modifica di un oggetto passato per riferimento è una cattiva pratica?

No. È più una questione di gusti.

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.