Passare un oggetto in un metodo che cambia l'oggetto, è un modello comune (anti-)?


17

Sto leggendo degli odori di codice comuni nel libro Refactoring di Martin Fowler . In quel contesto, mi chiedevo uno schema che sto vedendo in una base di codice e se uno potesse obiettivamente considerarlo un anti-schema.

Il modello è quello in cui un oggetto viene passato come argomento a uno o più metodi, tutti i quali cambiano lo stato dell'oggetto, ma nessuno dei quali restituisce l'oggetto. Quindi si basa sul passaggio per natura di riferimento di (in questo caso) C # /. NET.

var something = new Thing();
// ...
Foo(something);
int result = Bar(something, 42);
Baz(something);

Trovo che (specialmente quando i metodi non sono nominati in modo appropriato) ho bisogno di esaminare tali metodi per capire se lo stato dell'oggetto è cambiato. Rende la comprensione del codice più complessa, poiché devo tenere traccia di più livelli dello stack di chiamate.

Vorrei proporre di migliorare tale codice per restituire un altro oggetto (clonato) con il nuovo stato, o tutto ciò che è necessario per modificare l'oggetto nel sito di chiamata.

var something1 =  new Thing();
// ...

// Let's return a new instance of Thing
var something2 = Foo(something1);

// Let's use out param to 'return' other info about the operation
int result;
var something3 = Bar(something2, out result);

// If necessary, let's capture and make explicit complex changes
var changes = Baz(something3)
something3.Apply(changes);

A me sembra che il primo modello sia stato scelto in base alle ipotesi

  • che è meno lavoro o richiede meno righe di codice
  • che ci consente sia di cambiare l'oggetto, sia di restituire qualche altra informazione
  • che è più efficiente poiché abbiamo meno casi di Thing.

Illustro un'alternativa, ma per proporlo bisogna avere argomenti contro la soluzione originale. Quali eventuali argomenti possono essere fatti per sostenere che la soluzione originale è un anti-pattern?

E cosa c'è di sbagliato nella mia soluzione alternativa?



1
@DaveHillier Grazie, conoscevo bene il termine, ma non avevo stabilito la connessione.
Michiel van Oosterhout,

Risposte:


9

Sì, la soluzione originale è un anti-pattern per i motivi che descrivi: rende difficile ragionare su ciò che sta accadendo, l'oggetto non è responsabile del proprio stato / implementazione (interruzione dell'incapsulamento). Vorrei anche aggiungere che tutti questi cambiamenti di stato sono contratti impliciti del metodo, il che rende questo metodo fragile di fronte alle mutevoli esigenze.

Detto questo, la tua soluzione ha alcuni suoi aspetti negativi, il più ovvio dei quali è che la clonazione di oggetti non è eccezionale. Può essere lento per oggetti di grandi dimensioni. Può portare a errori in cui altre parti del codice si aggrappano ai vecchi riferimenti (che è probabilmente il caso nella base di codice che descrivi). Rendere questi oggetti esplicitamente immutabili risolve almeno alcuni di questi problemi, ma è un cambiamento più drastico.

A meno che gli oggetti non siano piccoli e in qualche modo transitori (il che li rende buoni candidati per l'immutabilità), sarei propenso a spostare semplicemente più della transizione di stato negli oggetti stessi. Ciò consente di nascondere i dettagli di implementazione di queste transizioni e impostare requisiti più rigorosi su chi / cosa / dove si verificano tali transizioni di stato.


1
Ad esempio, se avessi un oggetto "File", non proverei a spostare alcun metodo di modifica dello stato in quell'oggetto - ciò violerebbe l'SRP. Ciò rimane valido anche quando si hanno le proprie classi invece di una classe di libreria come "File": inserire ogni logica di transizione di stato nella classe dell'oggetto non ha davvero senso.
Doc Brown,

@Tetastyn So che questa è una vecchia risposta, ma ho problemi a visualizzare i tuoi suggerimenti nell'ultimo paragrafo in termini concreti. Potresti elaborare o fare un esempio?
AaronLS,

@AaronLS - Invece di Bar(something)(e modificando lo stato di something), crea Barun membro del somethingtipo. something.Bar(42)è più probabile che muti something, consentendo anche di utilizzare strumenti OO (stato privato, interfacce, ecc.) per proteggere lo somethingstato
Telastyn,

14

quando i metodi non sono nominati in modo appropriato

In realtà, questo è il vero odore del codice. Se si dispone di un oggetto mutabile, fornisce metodi per modificarne lo stato. Se si dispone di una chiamata a tale metodo incorporato in un'attività di alcune altre istruzioni, è opportuno fare il refactoring di tale attività in un metodo a sé stante, il che lascia in esattezza la situazione descritta. Ma se non hai nomi di metodi come Fooe Bar, ma metodi che chiariscono che cambiano l'oggetto, non vedo un problema qui. Pensa a

void AddMessageToLog(Logger logger, string msg)
{
    //...
}

o

void StripInvalidCharsFromName(Person p)
{
// ...
}

o

void AddValueToRepo(Repository repo,int val)
{
// ...
}

o

void TransferMoneyBetweenAccounts(Account source, Account destination, decimal amount)
{
// ...
}

o qualcosa del genere: qui non vedo alcun motivo per restituire un oggetto clonato per quei metodi, e non c'è nemmeno motivo di esaminare l'implementazione per capire che cambieranno lo stato dell'oggetto passato.

Se non vuoi effetti collaterali, rendi immutabili i tuoi oggetti, imporrà metodi come quelli sopra per restituire un oggetto modificato (clonato) senza cambiare quello originale.


Hai ragione, il refactoring del metodo di ridenominazione può migliorare la situazione rendendo chiari gli effetti collaterali. Può diventare difficile, tuttavia, se le modifiche sono tali da rendere impossibile un nome di metodo conciso.
Michiel van Oosterhout,

2
@michielvoo: se la denominazione del metodo consise sembra non essere possibile, il tuo metodo raggruppa le cose sbagliate invece di costruire un'astrazione funzionale per l'attività che svolge (e questo è vero con o senza effetti collaterali).
Doc Brown,

4

Sì, vedi http://codebetter.com/matthewpodwysocki/2008/04/30/side-effecting-functions-are-code-smells/ per uno dei molti esempi di persone che sottolineano che gli effetti collaterali imprevisti sono negativi.

In generale, il principio fondamentale è che il software è costruito in strati e ogni strato dovrebbe presentare l'astrazione più pulita possibile a quello successivo. E un'astrazione pulita è quella in cui devi tenere a mente il meno possibile per usarla. Si chiama modularità e si applica a tutto, dalle singole funzioni ai protocolli di rete.


Definirei ciò che l'OP sta descrivendo come "effetti collaterali attesi". Ad esempio, un delegato è possibile passare a un motore di qualche tipo che opera su ciascun elemento in un elenco. Questo è fondamentalmente ciò che ForEach<T>fa.
Robert Harvey,

@RobertHarvey Le lamentele sui metodi che non sono stati nominati correttamente e sulla necessità di leggere il codice per capire gli effetti collaterali, non li rendono sicuramente effetti collaterali previsti.
btilly

Ti concedo questo. Ma il corollario è che un metodo documentato con un nome appropriato con effetti previsti sul sito potrebbe non essere un anti-schema dopo tutto.
Robert Harvey,

@RobertHarvey Sono d'accordo. La chiave è che gli effetti collaterali significativi sono molto importanti da conoscere e devono essere documentati attentamente (preferibilmente nel nome del metodo).
btilly

Direi che è un mix di effetti collaterali inaspettati e non ovvi. Grazie per il link
Michiel van Oosterhout,

3

Prima di tutto, questo non dipende dal "passaggio per natura di riferimento di", dipende dal fatto che gli oggetti sono tipi di riferimento mutabili. Nei linguaggi non funzionali è quasi sempre così.

In secondo luogo, se questo è un problema o meno, dipende sia dall'oggetto sia da quanto strettamente i cambiamenti nelle diverse procedure sono collegati insieme: se non si riesce a apportare una modifica a Foo e ciò causa l'arresto anomalo di Bar, allora è un problema. Non necessariamente un odore di codice, ma è un problema con Foo o Bar o Something (probabilmente Bar come dovrebbe controllare il suo input, ma potrebbe essere qualcosa che viene messo in uno stato non valido che dovrebbe impedire).

Non direi che sale al livello di un anti-schema, ma piuttosto qualcosa da tenere presente.


2

Direi che c'è poca differenza tra la A.Do(Something)modifica somethinge la something.Do()modifica something. In entrambi i casi, dovrebbe essere chiaro dal nome del metodo invocato che somethingverrà modificato. Se non è chiaro dal nome del metodo, indipendentemente dal fatto che somethingsi tratti di un parametro thiso di una parte dell'ambiente, non deve essere modificato.


1

Penso che vada bene cambiare lo stato dell'oggetto in alcuni scenari. Ad esempio, ho un elenco di utenti e desidero applicare diversi filtri all'elenco prima di restituirlo al client.

var users = Dependency.Resolve<IGetUsersQuery>().GetAll();

var excludeAdminUsersFilter = new ExcludeAdminUsersFilter();
var filterByAnotherCriteria = new AnotherCriteriaFilter();

excludeAdminUsersFilter.Apply(users);
filterByAnotherCriteria.Apply(users); 

E sì, puoi renderlo carino spostando il filtro in un altro metodo, quindi finirai con qualcosa sulle linee di:

var users = Dependency.Resolve<IGetUsersQuery>().GetAll();
Filter(users);

Dove Filter(users)eseguire i filtri sopra.

Non ricordo dove esattamente mi sono imbattuto in questo prima, ma penso che sia stato definito pipeline di filtraggio.


0

Non sono sicuro che la nuova soluzione proposta (di copiare oggetti) sia un modello. Il problema, come hai sottolineato, è la cattiva nomenclatura delle funzioni.

Diciamo che scrivo un'operazione matematica complessa come funzione f () . Documento che f () è una funzione mappata NXNa N, e l'algoritmo dietro di essa. Se la funzione viene denominata in modo inappropriato e non è documentata, e non ha casi di test associati e sarà necessario comprendere il codice, nel qual caso il codice è inutile.

Sulla tua soluzione, alcune osservazioni:

  • Le applicazioni sono progettate da diversi aspetti: quando un oggetto viene utilizzato solo per contenere valori o viene passato attraverso i confini dei componenti, è consigliabile modificare le interiora dell'oggetto esternamente anziché riempirlo con i dettagli su come cambiare.
  • La clonazione di oggetti porta a requisiti di memoria rigonfia, e in molti casi porta all'esistenza di oggetti equivalenti in stati incompatibili ( Xdiventati Ydopo f(), ma in Xrealtà è Y) e possibilmente incoerenza temporale.

Il problema che stai cercando di risolvere è valido; tuttavia, anche con un'enorme ingegnerizzazione eccessiva, il problema viene eluso, non risolto.


2
Questa sarebbe una risposta migliore se mettessi in relazione la tua osservazione con la domanda del PO. Come è, è più un commento che una risposta.
Robert Harvey,

1
@RobertHarvey +1, buona osservazione, sono d'accordo, lo modificherò.
CMR,
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.