È un anti-pattern se una proprietà di classe crea e restituisce una nuova istanza di una classe?


24

Ho una classe chiamata Headingche fa alcune cose, ma dovrebbe anche essere in grado di restituire l'opposto dell'attuale valore dell'intestazione, che alla fine deve essere usato creando una nuova istanza della Headingclasse stessa.

Posso avere una proprietà semplice chiamata reciprocalper restituire l'intestazione opposta del valore corrente e quindi creare manualmente una nuova istanza della classe Heading, oppure posso creare un metodo come createReciprocalHeading()creare automaticamente una nuova istanza della classe Heading e restituirla alla utente.

Tuttavia, uno dei miei colleghi mi ha consigliato di creare semplicemente una proprietà di classe chiamata reciprocalche restituisce una nuova istanza della classe stessa tramite il suo metodo getter.

La mia domanda è: non è un anti-schema per una proprietà di una classe comportarsi così?

Lo trovo particolarmente meno intuitivo perché:

  1. Nella mia mente, una proprietà di una classe non dovrebbe restituire una nuova istanza di una classe e
  2. Il nome della proprietà, ovvero reciprocal, non aiuta lo sviluppatore a comprenderne appieno il comportamento, senza ottenere aiuto dall'IDE o controllare la firma getter.

Sono troppo severo su cosa dovrebbe fare una proprietà di classe o è una preoccupazione valida? Ho sempre cercato di gestire lo stato della classe attraverso i suoi campi e proprietà e il suo comportamento attraverso i suoi metodi, e non riesco a vedere come questo si adatta alla definizione della proprietà della classe.


6
Come dice @Ewan nell'ultimo paragrafo della sua risposta, lungi dall'essere un anti-schema, avere Headingcome tipo immutabile e reciprocalrestituire un nuovo Headingè una buona pratica "pozzo del successo". (con l'avvertenza alle due chiamate a reciprocaldovrebbe restituire "la stessa cosa", cioè dovrebbero superare il test di uguaglianza.)
David Arno,

20
"la mia classe non è immutabile". C'è il tuo vero anti-pattern, proprio lì. ;)
David Arno,

3
@DavidArno Beh, a volte c'è un'enorme penalità prestazionale per aver reso certe cose immutabili, specialmente se il linguaggio non lo supporta in modo nativo, ma per il resto sono d'accordo che l'immutabilità è una buona pratica in molti casi.
53777A

2
@dcorking la classe è implementata sia in C # che in TypeScript ma preferisco mantenere questo linguaggio agnostico.
53777A

2
@Kaiserludi sembra una risposta (un'ottima risposta) - i commenti sono per richiedere chiarezza
dcorking

Risposte:


22

Non è sconosciuto avere cose come Copy () o Clone () ma sì, penso che tu abbia ragione a essere preoccupato per questo.

prendiamo ad esempio:

h2 = h1.reciprocal
h2.Name = "hello world"
h1.reciprocal.Name = ?

Sarebbe bello avere qualche avvertimento che la proprietà era ogni volta un nuovo oggetto.

potresti anche supporre che:

h2.reciprocal == h1

Tuttavia. Se la tua classe di titolo fosse un tipo di valore immutabile, allora saresti in grado di implementare queste relazioni e il reciproco potrebbe essere un buon nome per l'operazione


1
Basta notare che Copy()e Clone()sono metodi, non proprietà. Credo che l'OP si riferisca ai getter di proprietà che restituiscono nuove istanze.
Lynn,

6
lo so. ma le proprietà sono (in qualche modo) anche metodi in c #
Ewan

4
In questo caso, C # ha molto più da offrire: String.Substring(), Array.Reverse(), Int32.GetHashCode(), ecc
Lynn

If your heading class was an immutable value type- Penso che dovresti aggiungere che i setter di proprietà su oggetti immutabili dovrebbero restituire sempre le nuove istanze (o almeno se il nuovo valore non è lo stesso del vecchio valore), perché questa è la definizione di oggetti immutabili. :)
Ivan Kolmychek,

17

Un'interfaccia di una classe porta l'utente della classe a fare ipotesi su come funziona.

Se molte di queste ipotesi sono giuste e poche sono sbagliate, l'interfaccia è buona.

Se molte di queste ipotesi sono sbagliate e poche sono giuste, l'interfaccia è spazzatura.

Un presupposto comune su Proprietà è che chiamare la funzione get è economico. Un altro presupposto comune sulle proprietà è che chiamare la funzione get due volte di seguito restituirà la stessa cosa.


Puoi aggirare questo problema usando la coerenza, al fine di cambiare le aspettative. Ad esempio, per una piccola libreria 3D di cui hai bisogno Vector, Ray Matrixecc. Puoi renderla tale che getter come Vector.normale Matrix.inversesono praticamente sempre costosi. Sfortunatamente anche se le tue interfacce usano costantemente proprietà costose, le interfacce saranno nella migliore delle ipotesi intuitive come quelle che usano Vector.create_normal()e Matrix.create_inverse()- eppure non conosco argomenti forti che si possano affermare che l'uso delle proprietà crea un'interfaccia più intuitiva, anche dopo aver cambiato le aspettative.


10

Le proprietà dovrebbero restituire molto rapidamente, restituire lo stesso valore con chiamate ripetute e ottenere il loro valore non dovrebbe avere effetti collaterali. Quello che descrivi qui non dovrebbe essere implementato come proprietà.

La tua intuizione è ampiamente corretta. Un metodo di fabbrica non restituisce lo stesso valore con chiamate ripetute. Restituisce una nuova istanza ogni volta. Questo praticamente lo squalifica come proprietà. Inoltre, non è veloce se lo sviluppo successivo aggiunge peso alla creazione dell'istanza, ad es. Dipendenze di rete.

Le proprietà dovrebbero generalmente essere operazioni molto semplici che ottengono / impostano una parte dello stato corrente della classe. Le operazioni di tipo industriale non soddisfano questi criteri.


1
La DateTime.Nowproprietà è comunemente usata e fa riferimento a un nuovo oggetto. Penso che il nome di una proprietà possa aiutare a rimuovere l'ambiguità circa la restituzione o meno dello stesso oggetto ogni volta. È tutto nel nome e come viene utilizzato.
Greg Burghardt,

3
DateTime.Now è considerato un errore. Vedi stackoverflow.com/questions/5437972/…
Brad Thomas

@GregBurghardt L'effetto collaterale di un nuovo oggetto potrebbe non essere un problema in sé e per sé, poiché DateTimeè un tipo di valore e non ha un concetto di identità dell'oggetto. Se ho capito bene, il problema è che non è deterministico e restituisce un nuovo valore ogni volta.
Zev Spitz,

1
@GregBurghardt DateTime.Newè statico e quindi non puoi aspettarti che rappresenti lo stato di un oggetto.
Tsahi Asher,

5

Non credo che ci sia una risposta agnostica al linguaggio in quanto ciò che costituisce una "proprietà" è una domanda specifica della lingua, e ciò che un chiamante di una "proprietà" si aspetta è anche una domanda specifica della lingua. Penso che il modo più fruttuoso di pensare a questo sia quello di pensare a come appare dal punto di vista del chiamante.

In C #, le proprietà sono distintive in quanto sono (convenzionalmente) maiuscole (come i metodi) ma mancano di parentesi (come le variabili di istanza pubblica). Se vedi il seguente codice, documentazione assente, cosa ti aspetti?

var reciprocalHeading = myHeading.Reciprocal;

Come un principiante in C #, ma uno che ha letto le Linee guida per l'utilizzo delle proprietà di Microsoft , mi aspetto Reciprocal, tra le altre cose:

  1. essere un membro di dati logici della Headingclasse
  2. essere economico da chiamare, in modo tale che non sia necessario per me memorizzare nella cache il valore
  3. mancano effetti collaterali osservabili
  4. produce lo stesso risultato se chiamato due volte in successione
  5. (forse) offrire un ReciprocalChangedevento

Di questi presupposti, (3) e (4) sono probabilmente corretti (supponendo che Headingsia un tipo di valore immutabile, come nella risposta di Ewan ), (1) è discutibile, (2) è sconosciuto ma anche discutibile e (5) è improbabile che ha un senso semantico (anche se qualunque cosa abbia un titolo dovrebbe forse avere un HeadingChangedevento). Questo mi suggerisce che in un'API C #, "ottenere o calcolare il reciproco" non dovrebbe essere implementato come una proprietà, ma soprattutto se il calcolo è economico eHeading è immutabile, è un caso limite.

(Si noti, tuttavia, che nessuna di queste preoccupazioni ha nulla a che fare con la chiamata alla proprietà crea una nuova istanza , nemmeno (2). La creazione di oggetti nel CLR, di per sé, è piuttosto economica.)

In Java, le proprietà sono una convenzione di denominazione dei metodi. Se vedo

Heading reciprocalHeading = myHeading.getReciprocal();

le mie aspettative sono simili a quelle sopra (se espressamente meno esplicite): mi aspetto che la chiamata sia economica, idempotente e priva di effetti collaterali. Tuttavia, al di fuori del framework JavaBeans, il concetto di "proprietà" non è poi così significativo in Java, e in particolare quando si considera una proprietà immutabile senza corrispondenti setReciprocal(), la getXXX()convenzione è ora in qualche modo antiquata. Da Effective Java , seconda edizione (ormai più di otto anni):

I metodi che restituiscono una non booleanfunzione o un attributo dell'oggetto su cui sono invocati sono generalmente denominati con un nome, una frase di nome o una frase di verbo che inizia con il verbo get…. Esiste un contingente vocale che afferma che solo la terza forma (a partire da get) è accettabile, ma ci sono poche basi per questa affermazione. Le prime due forme di solito portano a un codice più leggibile ... (p. 239)

In un'API contemporanea e più fluida, quindi, mi aspetterei di vedere

Heading reciprocalHeading = myHeading.reciprocal();

- che suggerirebbe ancora una volta che la chiamata è economica, idempotente e priva di effetti collaterali, ma non direbbe nulla sull'esecuzione di un nuovo calcolo o sulla creazione di un nuovo oggetto. Questo va bene; in una buona API, non mi dovrebbe importare.

In Ruby non esiste una proprietà. Ci sono "attributi", ma se vedo

reciprocalHeading = my_heading.reciprocal

Non ho modo immediato di sapere se sto accedendo a una variabile di istanza @reciprocaltramite un attr_readero un metodo di accesso semplice o se sto chiamando un metodo che esegue un calcolo costoso. Il fatto che il nome del metodo sia un nome semplice, invece di direcalcReciprocal , suggerisce, ancora una volta, che la chiamata è almeno economica e probabilmente non ha effetti collaterali.

In Scala, la convenzione di denominazione è che i metodi con effetti collaterali prendono parentesi e metodi senza di loro no, ma

val reciprocal = heading.reciprocal

potrebbe essere uno dei seguenti:

// immutable public value initialized at creation time
val reciprocal: Heading = … 

// immutable public value initialized on first use
lazy val reciprocal: Heading = … 

// public method, probably recalculating on each invocation
def reciprocal: Heading = …

// as above, with parentheses that, by convention, the caller
// should only omit if they know the method has no side effects
def reciprocal(): Heading = …

(Nota che Scala consente varie cose che sono comunque scoraggiate dalla guida di stile . Questo è uno dei miei principali fastidi con Scala.)

La mancanza di parentesi mi dice che la chiamata non ha effetti collaterali; il nome, ancora una volta, suggerisce che la chiamata dovrebbe essere relativamente economica. Oltre a ciò, non mi interessa come mi dia il valore.

In breve: Conosci la lingua che stai usando e conosci quali aspettative altri programmatori porteranno alla tua API. Tutto il resto è un dettaglio di implementazione.


1
+1 a una delle mie risposte preferite, grazie per aver dedicato del tempo a scrivere questo.
53777A

2

Come altri hanno già detto, è un modello piuttosto comune restituire istanze della stessa classe.

La denominazione dovrebbe andare di pari passo con le convenzioni di denominazione della lingua.

Ad esempio, in Java probabilmente mi aspetto che venga chiamato getReciprocal();

Detto questo, considererei oggetti mutabili vs immutabili .

Con immutabile quelli , le cose sono abbastanza facili e non farà male se restituisci lo stesso oggetto o no.

Con quelli mutevoli , questo può diventare molto spaventoso.

b = a.reciprocal
a += 1
b = what?

A cosa si riferisce b? Il reciproco del valore originale di a? O il reciproco di quello cambiato? Questo potrebbe essere un esempio troppo breve, ma ottieni il punto.

In quei casi cercare una denominazione migliore che comunichi ciò che accade, ad esempio createReciprocal()potrebbe essere una scelta migliore.

Ma dipende anche dal contesto.


Intendevi mutevole ?
Carsten S

@CarstenS Sì, certo, grazie. Non ho idea di dove fosse la mia testa. :)
Eiko

In disaccordo un po ' getReciprocal(), dal momento che "suona" come un "normale" getter in stile JavaBeans. Preferisci "createXXX ()" o eventualmente "calcoliXXX ()" che a indicare o suggerire ad altri programmatori che sta succedendo qualcosa di diverso
user949300

@ user949300 Concordo in qualche modo con le tue preoccupazioni - e ho citato il nome ... più avanti. Ci sono anche degli aspetti negativi, però. crea ... implica nuove istanze che potrebbero non essere sempre il caso (pensate a singoli oggetti dedicati per alcuni casi speciali), calcola ... una sorta di dettagli di implementazione delle perdite - che potrebbero incoraggiare ipotesi errate sul prezzo.
Eiko,

1

Nell'interesse della responsabilità individuale e della chiarezza avrei una ReverseHeadingFactory che ha preso l'oggetto e restituito il suo contrario. Ciò renderebbe più chiaro il ritorno di un nuovo oggetto e significherebbe che il codice per produrre il contrario è stato incapsulato lontano da altro codice.


1
+1 per la chiarezza del nome, ma cosa c'è di sbagliato con SRP in altre soluzioni?
53777A

3
Perché consigli una classe di fabbrica rispetto a un metodo di fabbrica? (Secondo me, un'utile responsabilità di un oggetto è spesso quella di emettere oggetti che sono come se stessi, come iterator.next. Questa lieve violazione di SRP causa altri problemi di ingegneria del software?)
dcorking

2
@dcorking Una classe di fabbrica rispetto a un metodo (secondo me) di solito è lo stesso tipo di domanda di "L'oggetto è comparabile o dovrei fare un comparatore?" Va sempre bene fare un comparatore, ma va bene solo renderli comparabili se è un modo abbastanza universale di confrontarli. (Ad esempio, i numeri più grandi sono maggiori di quelli più piccoli, questo è buono per comparabili, ma potresti dire che tutti i pari sono maggiori di tutte le probabilità, lo trasformerei in un comparatore) - Quindi, per questo, penso che ce ne sia solo uno modo di invertire un'intestazione, quindi mi spingerei a fare un metodo per evitare una classe in più.
Captain Man,

0

Dipende. Di solito mi aspetto che le proprietà restituiscano elementi che fanno parte di un'istanza, quindi restituire un'istanza diversa della stessa classe sarebbe un po 'insolito.

Ad esempio, una classe di stringhe potrebbe avere proprietà "firstWord", "lastWord" o se gestisce Unicode "firstLetter", "lastLetter" che sarebbero oggetti stringa completi, in genere solo quelli più piccoli.


0

Poiché le altre risposte riguardano la parte Proprietà, ti rivolgerò solo al tuo numero 2 su reciprocal:

Non usare altro che reciprocal. È l'unico termine corretto per quello che stai descrivendo (ed è il termine formale). Non scrivere software errato per salvare i tuoi sviluppatori dall'apprendimento del dominio in cui stanno lavorando. Nella navigazione, i termini hanno significati molto specifici e l'utilizzo di qualcosa di apparentemente innocuo come reverseo oppositepotrebbe creare confusione in determinati contesti.


0

Logicamente, no, non è una proprietà di un'intestazione. Se lo fosse, si potrebbe anche dire che il negativo di un numero è proprietà di quel numero, e francamente ciò farebbe perdere "significato" alla "proprietà", quasi tutto sarebbe una proprietà.

Il reciproco è una funzione pura di un'intestazione, lo stesso che negativo di un numero è una funzione pura di quel numero.

Come regola generale, se l'impostazione non ha senso, probabilmente non dovrebbe essere una proprietà. L'impostazione può comunque essere vietata, ma l'aggiunta di un setter dovrebbe essere teoricamente possibile e significativa.

Ora, in alcune lingue, potrebbe avere senso averlo come proprietà come specificato nel contesto di quella lingua, se porta alcuni vantaggi dovuti alla meccanica di quella lingua. Ad esempio, se si desidera archiviarlo in un database, è probabilmente sensato renderlo come proprietà se consente la gestione automatica da parte del framework ORM. O forse è un linguaggio in cui non c'è distinzione tra una proprietà e una funzione membro senza parametri (non conosco un linguaggio del genere, ma sono sicuro che ce ne sono alcuni). Quindi spetta al progettista e al documentatore API distinguere tra funzioni e proprietà.


Modifica: in particolare per C #, darei un'occhiata alle classi esistenti, in particolare a quelle della Libreria standard.

  • Ci sono BigIntegere Complexstrutture. Ma sono strutture e hanno solo funzioni statiche e tutte le proprietà sono di tipo diverso. Quindi non c'è molto aiuto nel design per la tua Headingclasse.
  • Math.NET Numerics ha Vectorclasse , che ha pochissime proprietà e sembra essere abbastanza vicino alla tua Heading. Se lo segui, allora avresti una funzione per il reciproco, non una proprietà.

Potresti voler esaminare le librerie effettivamente utilizzate dal tuo codice o classi simili esistenti. Cerca di rimanere coerente, di solito è il migliore, se un modo non è chiaramente giusto e un altro modo sbagliato.


-1

Domanda davvero interessante. Non vedo alcuna violazione SOLID + DRY + KISS in quello che stai proponendo, ma, comunque, ha un cattivo odore.

Un metodo che restituisce un'istanza della classe si chiama costruttore , giusto? quindi stai creando una nuova istanza da un metodo non costruttore. Non è la fine del mondo, ma come cliente non me lo sarei aspettato . Questo di solito viene fatto in circostanze specifiche: una fabbrica o, a volte, un metodo statico della stessa classe (utile per singoli e per programmatori non orientati agli oggetti?)

Inoltre: cosa succede se invochi getReciprocal()l'oggetto restituito? ottieni un'altra istanza della classe, che potrebbe essere una copia esatta della prima! E se invochi getReciprocal()su QUELLO? Oggetti che tentacolano qualcuno?

Ancora: se l'invocatore avesse bisogno del valore reciproco (come scalare, intendo)? getReciprocal()->getValue()? stiamo violando la Legge di Demetra per piccoli guadagni.


Accetterei volentieri contro-argomentazioni dal downvoter, grazie!
Dott. Gianluigi Zane Zanettini,

1
Non ho votato a fondo, ma sospetto che il motivo potrebbe essere il fatto che non aggiunge molto al resto delle risposte, ma potrei sbagliarmi.
53777A

2
SOLID e KISS sono spesso opposti l'uno rispetto all'altro.
whatsisname
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.