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

24

Ho una classe chiamata Heading che fa alcune cose, ma dovrebbe anche essere in grado di restituire l'opposto del valore corrente dell'intestazione, che deve essere infine utilizzato creando una nuova istanza di Heading classe stessa.

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

Tuttavia, uno dei miei colleghi mi ha consigliato di creare solo una classe property chiamata reciprocal che restituisce una nuova istanza della classe stessa tramite il suo metodo getter.

La mia domanda è: non è un anti-pattern per una proprietà di una classe comportarsi in quel modo?

In particolare trovo questo 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à, che è reciprocal , non aiuta lo sviluppatore a comprendere appieno il suo 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 vedo come questo si adatti alla definizione della proprietà della classe.

    
posta 53777A 08.11.2016 - 09:33
fonte

10 risposte

22

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

prendi ad esempio:

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

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

potresti anche assumere che:

h2.reciprocal == h1

Tuttavia. Se la tua classe di intestazione fosse un tipo di valore immutabile, sarebbe possibile implementare queste relazioni e il reciproco potrebbe essere un buon nome per l'operazione

    
risposta data 08.11.2016 - 09:39
fonte
17

Un'interfaccia di una classe guida l'utente della classe a fare supposizioni su come funziona.

Se molte di queste ipotesi sono corrette e poche sono errate, l'interfaccia è buona.

Se molti di questi presupposti sono sbagliati e pochi hanno ragione, l'interfaccia è spazzatura.

Un'ipotesi comune sulle proprietà è che chiamare la funzione get è economico. Un'altra ipotesi comune sulle proprietà è che chiamare la funzione get due volte di seguito restituirà la stessa cosa.

Puoi aggirare questo problema utilizzando la coerenza, al fine di cambiare le aspettative. Ad esempio, per una piccola libreria 3D in cui hai bisogno di Vector , Ray Matrix , ecc. Puoi renderla tale che i getter come Vector.normal e Matrix.inverse siano quasi sempre costosi. Sfortunatamente, anche se le tue interfacce utilizzano costantemente costose proprietà, le interfacce saranno nel migliore dei casi intuitive come quella che usa Vector.create_normal() e Matrix.create_inverse() - ma non conosco alcun argomento valido che possa essere fatto usando le proprietà per creare un'interfaccia più intuitiva, anche dopo aver cambiato le aspettative.

    
risposta data 08.11.2016 - 10:08
fonte
10

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

La tua intuizione è ampiamente corretta. Un metodo factory non restituisce lo stesso valore con chiamate ripetute. Restituisce una nuova istanza ogni volta. Questo praticamente lo squalifica come una 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 tipo fabbrica non soddisfano questi criteri.

    
risposta data 08.11.2016 - 17:03
fonte
5

Non penso che ci sia una risposta agonistica della lingua a questo dato che ciò che costituisce una "proprietà" è una domanda specifica della lingua, e ciò che un chiamante di una "proprietà" si aspetta è una domanda specifica della lingua. Penso che il modo più fruttuoso di pensare a questo sia pensare a come appare dal punto di vista del chiamante.

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

var reciprocalHeading = myHeading.Reciprocal;

Come novizio relativo di C #, ma uno che ha letto il Proprietà di Microsoft Linee guida per l'utilizzo , mi aspetto che Reciprocal , tra l'altro:

  1. essere un membro di dati logici della classe Heading
  2. essere economico da chiamare, in modo tale che non ci sia bisogno di memorizzare nella cache il valore
  3. mancano effetti collaterali osservabili
  4. produce lo stesso risultato se chiamato due volte in successione
  5. (forse) offre un evento ReciprocalChanged

Di queste ipotesi, (3) e (4) sono probabilmente corrette (assumendo che Heading sia un tipo di valore immutabile, come in La risposta di Ewan ), (1) è discutibile, (2) è sconosciuta ma anche discutibile, e (5) è improbabile che abbia senso semantico (sebbene qualunque abbia un'intestazione dovrebbe forse avere un evento HeadingChanged ). Questo mi suggerisce che in un'API C #, "ottenere o calcolare il reciproco" dovrebbe non essere implementato come una proprietà, ma soprattutto se il calcolo è economico e Heading è immutabile, è un caso limite.

(Si noti, tuttavia, che nessuno di questi dubbi ha nulla a che fare con la chiamata della proprietà crea una nuova istanza , nemmeno (2). Creazione di oggetti nel CLR, in e di se stesso , è piuttosto economico.)

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 meno esplicitamente definite): mi aspetto che la chiamata sia economica, idempotente e priva di effetti collaterali. Però, al di fuori del framework JavaBeans, il concetto di una "proprietà" non è tutto ciò che è significativo in Java, e in particolare quando si considera una proprietà immutabile senza corrispondente setReciprocal() , la convenzione getXXX() è ora un po 'antiquata. Da Efficace Java , seconda edizione ( già più di otto anni ora):

Methods that return a non-boolean function or attribute of the object on which they’re invoked are usually named with a noun, noun phrase, or a verb phrase beginning with the verb get …. There is a vocal contingent that claims that only the third form (beginning with get) is acceptable, but there is little basis for this claim. The first two forms usually lead to more readable code… (p. 239)

In un'API contemporanea e più fluente, quindi, mi aspetto di vedere

Heading reciprocalHeading = myHeading.reciprocal();

- che suggerirebbe ancora una volta che la chiamata è economica, idempotente e priva di effetti collaterali, ma non dirà nulla sul fatto che venga eseguito un nuovo calcolo o che venga creato un nuovo oggetto. Questo va bene; in una buona API, non me ne dovrebbe importare.

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

reciprocalHeading = my_heading.reciprocal

Non ho un modo immediato per sapere se sto accedendo a una variabile di istanza @reciprocal tramite un attr_reader o 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 dire calcReciprocal , 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 essi, ma

val reciprocal = heading.reciprocal

potrebbe essere uno qualsiasi di:

// 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 = …

(Si noti che Scala consente varie cose che sono tuttavia scoraggiate dal style guide . Questo è uno dei miei maggiori 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 ottiene il valore.

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

    
risposta data 08.11.2016 - 19:47
fonte
2

Come altri hanno già detto, è uno schema piuttosto comune per restituire istanze della stessa classe.

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

Ad esempio, in Java probabilmente mi aspetterei che si chiami getReciprocal();

Detto questo, prenderei in considerazione gli oggetti mutabili vs immutabili .

Con immutable , le cose sono piuttosto semplici e non ti farà male se restituisci lo stesso oggetto o meno.

Con mutabili , 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 del mutante? Questo potrebbe essere un esempio troppo breve, ma ottieni il punto.

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

Ma dipende molto anche dal contesto.

    
risposta data 08.11.2016 - 17:13
fonte
1

Nell'interesse di una singola responsabilità e chiarezza avrei una ReverseHeadingFactory che ha preso l'oggetto e ha 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 un altro codice.

    
risposta data 08.11.2016 - 11:20
fonte
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 'insolita.

Ma per esempio una classe di stringhe potrebbe avere proprietà "firstWord", "lastWord", o se gestisce Unicode "firstLetter", "lastLetter" che sarebbe un oggetto stringa intero - solo di solito più piccoli.

    
risposta data 08.11.2016 - 16:36
fonte
0

Poiché le altre risposte riguardano la parte Proprietà, mi occuperò solo del tuo # 2 su reciprocal :

Non utilizzare nulla oltre a reciprocal . È l'unico termine corretto per ciò che stai descrivendo (ed è il termine formale). Non scrivere software non corretto 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 apparentemente innocuo come reverse o opposite potrebbe portare a confusione in determinati contesti.

    
risposta data 08.11.2016 - 18:17
fonte
0

Logicamente no, non è una proprietà di un titolo. Se lo fosse, potresti anche dire che il negativo di un numero è proprietà di quel numero, e francamente che farebbe "perdere" ogni significato di "proprietà", quasi tutto sarebbe una proprietà.

Il reciproco è una funzione pura di un titolo, lo stesso del negativo di un numero è una funzione pura di quel numero.

Come regola generale, se impostarla non ha senso, probabilmente non dovrebbe essere una proprietà. L'impostazione può essere comunque disabilitata, ma aggiungere un setter dovrebbe essere teoricamente possibile e significativo.

Ora, in alcune lingue, potrebbe avere senso avere una proprietà come specificato nel contesto di quella lingua, comunque, se presenta alcuni vantaggi dovuti alla meccanica di quel linguaggio. Ad esempio, se si desidera archiviarlo in un database, è probabilmente consigliabile renderlo come proprietà se consente la gestione automatica mediante il 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 API e al documentatore distinguere tra funzioni e proprietà.

Modifica: in particolare per C #, guarderei le classi esistenti, specialmente quelle della Libreria standard.

  • Ci sono BigInteger e strutture Complex . Ma sono strutture e hanno solo funzioni statiche e tutte le proprietà sono di tipo diverso. Quindi non molto aiuto nella progettazione per te Heading class.
  • Math.NET Numerics ha Vector class , che ha molto poche proprietà e sembra essere molto vicino al tuo Heading . Se passi da questo, avresti una funzione per reciproco, non una proprietà.

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

    
risposta data 09.11.2016 - 08:30
fonte
-1

Davvero una domanda molto interessante. Non vedo alcuna violazione di SOLID + DRY + KISS in quello che stai proponendo, ma, ancora, ha un cattivo odore.

Un metodo che restituisce un'istanza della classe è chiamato un costruttore , giusto? quindi stai creando una nuova istanza da un metodo non di costruzione. Non è la fine del mondo, ma come cliente, non mi aspetto che. Questo di solito viene fatto in circostanze specifiche: un factory o, a volte, un metodo statico della stessa classe (utile per singleton e per ... programmatori non orientati agli oggetti?)

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

Ancora: cosa succede se il invocatore ha bisogno del valore reciproco (come scalare, intendo)? %codice%? stiamo violando Law of Demeter per piccoli guadagni.

    
risposta data 08.11.2016 - 17:50
fonte

Leggi altre domande sui tag