La documentazione in OOP dovrebbe evitare di specificare se un "getter" esegue o meno un calcolo?


39

Il programma CS della mia scuola evita qualsiasi menzione di programmazione orientata agli oggetti, quindi ho fatto alcune letture per conto mio per integrarlo - in particolare, la costruzione di software orientata agli oggetti di Bertrand Meyer.

Meyer sottolinea ripetutamente che le classi dovrebbero nascondere quante più informazioni possibili sulla loro implementazione, il che ha senso. In particolare, sostiene ripetutamente che gli attributi (cioè le proprietà statiche, non calcolate delle classi) e le routine (proprietà delle classi che corrispondono alle chiamate di funzione / procedura) dovrebbero essere indistinguibili l'uno dall'altro.

Ad esempio, se una classe Personha l'attributo age, afferma che dovrebbe essere impossibile dire, dalla notazione, se Person.agecorrisponde internamente a qualcosa di simile return current_year - self.birth_dateo semplicemente return self.age, dove self.ageè stato definito come un attributo costante. Questo ha senso per me. Tuttavia, continua sostenendo quanto segue:

La documentazione client standard per una classe, conosciuta come la forma abbreviata della classe, sarà ideata in modo da non rivelare se una determinata caratteristica è un attributo o una funzione (nei casi in cui potrebbe essere).

cioè, afferma che anche la documentazione per la classe dovrebbe evitare di specificare se un "getter" esegue o meno un calcolo.

Questo non lo seguo. La documentazione non è l'unico posto in cui sarebbe importante informare gli utenti di questa distinzione? Se dovessi progettare un database pieno di Personoggetti, non sarebbe importante sapere se si Person.agetratta di una chiamata costosa o meno , così potrei decidere se implementare o meno una sorta di cache per esso? Ho frainteso quello che sta dicendo o è solo un esempio particolarmente estremo della filosofia del design OOP?


1
Domanda interessante. Recentemente ho chiesto qualcosa di molto simile: come progetterei un'interfaccia in modo tale che sia chiaro quali proprietà possono cambiare il loro valore e quali rimarranno costanti? . E ho ottenuto una buona risposta indicando la documentazione, ovvero esattamente ciò su cui Bertrand Meyer sembra discutere.
stakx,

Non ho letto il libro Meyer fornisce esempi dello stile di documentazione che consiglia? Trovo difficile immaginare cosa hai descritto lavorando per qualsiasi lingua.
user16764,

1
@PatrickCollins Ti suggerisco di leggere "esecuzione nel regno dei sostantivi" e di seguire qui il concetto di verbi e sostantivi. In secondo luogo, OOP NON riguarda getter e setter, suggerisco Alan Kay (inventore di OOP): programmazione e scala
AndreasScheinert,

@AndreasScheinert - ti riferisci a questo ? Ridacchiai del "tutto per la mancanza di un chiodo a ferro di cavallo", ma sembra essere una fregatura per i mali della programmazione orientata agli oggetti.
Patrick Collins,

1
@PatrickCollins sì questo: steve-yegge.blogspot.com/2006/03/… ! Dà alcuni punti su cui riflettere, gli altri sono: dovresti trasformare i tuoi oggetti in strutture dati usando (ab) usando setter.
AndreasScheinert,

Risposte:


58

Non credo che il punto di Meyer sia che non dovresti dire all'utente quando hai un'operazione costosa. Se la tua funzione sta per colpire il database, o fare una richiesta a un server web, e passare diverse ore a calcolare, sarà necessario che lo sappia altro codice.

Ma il programmatore che usa la tua classe non ha bisogno di sapere se hai implementato:

return currentAge;

o:

return getCurrentYear() - yearBorn;

Le caratteristiche prestazionali tra questi due approcci sono così minime che non dovrebbe importare. Il programmatore che usa la tua classe in realtà non dovrebbe preoccuparsi di ciò che hai. Questo è il punto di Meyer.

Ma non è sempre così, ad esempio, supponiamo di avere un metodo di dimensione su un contenitore. Quello potrebbe essere implementato:

return size;

o

return end_pointer - start_pointer;

o potrebbe essere:

count = 0
for(Node * node = firstNode; node; node = node->next)
{
    count++
}
return count

La differenza tra i primi due non dovrebbe davvero importare. Ma l'ultimo potrebbe avere gravi conseguenze sulle prestazioni. Ecco perché lo STL, per esempio, dice che lo .size()è O(1). Non documenta esattamente come viene calcolata la dimensione, ma mi dà le caratteristiche prestazionali.

Quindi : documentare problemi di prestazioni. Non documentare i dettagli di implementazione. Non mi interessa come std :: sort ordina le mie cose, purché lo faccia in modo corretto ed efficiente. Anche la tua classe non dovrebbe documentare come calcola le cose, ma se qualcosa ha un profilo prestazionale inaspettato, documentalo.


4
Inoltre: documentare innanzitutto la complessità tempo / spazio, quindi fornire una spiegazione del perché una funzione ha tali proprietà. Ad esempio:// O(n) Traverses the entire user list.
Jon Purdy,

2
= (Qualcosa di banale come Python lennon riesce a farlo ... (In almeno alcune situazioni, è O(n), come abbiamo imparato in un progetto al college quando ho suggerito di memorizzare la lunghezza invece di ricalcolarla ogni iterazione del ciclo)
Izkata

@Izkata, curioso. Ricordi che struttura era O(n)?
Winston Ewert,

@WinstonEwert Purtroppo no. È stato 4+ anni fa in un progetto di Data Mining e l'ho suggerito al mio amico solo perché avevo lavorato con C in un'altra classe ..
Izkata,

1
@JonPurdy Aggiungo che nel normale codice aziendale, probabilmente non ha senso specificare la complessità della grande O. Ad esempio, un accesso al database O (1) molto probabilmente sarà molto più lento rispetto all'attraversamento di elenchi in memoria di O (n), quindi documenta ciò che conta. Ma ci sono certamente casi in cui la complessità della documentazione è molto importante (raccolte o altro codice pesante per algoritmi).
svick,

16

Dal punto di vista accademico o dei puristi del CS, naturalmente non è possibile descrivere nella documentazione nulla sugli aspetti interni dell'implementazione di una funzione. Questo perché l'utente di una classe dovrebbe idealmente non fare ipotesi sull'implementazione interna della classe. Se l'implementazione cambia, idealmente nessun utente non se ne accorgerà: la funzione crea un'astrazione e gli interni dovrebbero essere completamente nascosti.

Tuttavia, la maggior parte dei programmi del mondo reale soffre della "Legge delle astrazioni che perdono " di Joel Spolsky , che dice

"Tutte le astrazioni non banali, in una certa misura, perdono."

Ciò significa che è praticamente impossibile creare un'astrazione completa della scatola nera di funzionalità complesse. E un sintomo tipico di questo sono problemi di prestazioni. Quindi, per i programmi del mondo reale, può diventare molto importante quali chiamate sono costose e quali no, e una buona documentazione dovrebbe includere tali informazioni (o dovrebbe indicare dove l'utente di una classe è autorizzato a fare ipotesi sulle prestazioni e dove no ).

Quindi il mio consiglio è: includi le informazioni su potenziali chiamate costose se scrivi documenti per un programma del mondo reale ed escludilo per un programma che stai scrivendo solo per scopi educativi del tuo corso CS, dato che qualsiasi considerazione sulle prestazioni dovrebbe essere mantenuta intenzionalmente fuori portata.


+1, più la maggior parte della documentazione che viene creata è per il prossimo programmatore per mantenere il tuo progetto, non per il programmatore successivo per usarlo .
jmoreno,

12

Puoi scrivere se una determinata chiamata è costosa o meno. Meglio, usare una convenzione di denominazione come getAgeper un accesso rapido loadAgeo fetchAgeper ricerche costose. Volete sicuramente informare l'utente se il metodo sta eseguendo IO.

Ogni dettaglio che dai nella documentazione è come un contratto che deve essere onorato dalla classe. Dovrebbe informare su comportamenti importanti. Spesso vedrai un'indicazione di complessità con una grande notazione O. Ma di solito vuoi essere basso e al punto.


1
+1 per menzionare che la documentazione fa parte del contratto di una classe tanto quanto la sua interfaccia.
Bart van Ingen Schenau,

Sostengo questo. Inoltre, in generale, si cerca di minimizzare la necessità di getter fornendo metodi con comportamento.
sevenforce,

9

Se dovessi progettare un database pieno di oggetti Person, non sarebbe importante sapere se Person.age è una chiamata costosa?

Sì.

Questo è il motivo per cui a volte utilizzo le Find()funzioni per indicare che la chiamata potrebbe richiedere del tempo. Questa è più una convenzione che altro. Il tempo impiegato per la restituzione di una funzione o di un attributo non fa alcuna differenza per il programma (anche se potrebbe essere per l'utente), sebbene tra i programmatori ci si aspetti che, se viene dichiarato come attributo, il costo per chiamarlo dovrebbe essere Basso.

In ogni caso, nel codice stesso dovrebbero esserci informazioni sufficienti per dedurre se qualcosa è una funzione o un attributo, quindi non vedo davvero la necessità di dirlo nella documentazione.


4
+1: quella convenzione è idiomatica in parecchi posti. Inoltre, la documentazione dovrebbe essere fatta a livello di interfaccia - a quel punto non sai come implementare Person.Age.
Telastyn,

@Telastyn: non ho mai pensato alla documentazione in questo modo; cioè, che dovrebbe essere fatto a livello di interfaccia. Sembra ovvio ora. +1 per quel prezioso commento.
stakx,

Mi piace molto questa risposta. Un esempio perfetto di ciò che descrivi che le prestazioni non sono una preoccupazione per il programma stesso sarebbe se Persona fosse un'entità recuperata da un servizio RESTful. OTTENERE è inerente ma non è chiaro se sarà economico o costoso. Questo ovviamente non è necessariamente OOP ma il punto è lo stesso.
maple_shaft

+1 per l'utilizzo di Getmetodi su attributi per indicare un'operazione più pesante. Ho visto abbastanza codice in cui gli sviluppatori ritengono che una proprietà sia solo un accessor e la usi più volte invece di salvare il valore in una variabile locale, e quindi eseguire un algoritmo molto complesso più di una volta. Se non esiste una convenzione per non implementare tali proprietà e la documentazione non accenna alla complessità, allora vorrei che chiunque dovesse conservare una simile applicazione in bocca al lupo.
enzi,

Da dove viene questa convenzione? Pensando a Java, mi aspetterei il contrario: il getmetodo è equivalente a un accesso agli attributi e quindi non costoso.
sevenforce,

3

È importante notare che la prima edizione di questo libro è stata scritta nel 1988, nei primi giorni di OOP. Queste persone stavano lavorando con linguaggi più puramente orientati agli oggetti che sono ampiamente utilizzati oggi. Le nostre lingue OO più popolari oggi - C ++, C # e Java - presentano alcune differenze piuttosto significative rispetto al modo in cui funzionavano le prime, più puramente OO.

In un linguaggio come C ++ e Java, è necessario distinguere tra l'accesso a un attributo e una chiamata al metodo. C'è un mondo di differenza tra instance.getter_methode instance.getter_method(). Uno in realtà ottiene il tuo valore e l'altro no.

Quando si lavora con un linguaggio più puramente OO, della persuasione Smalltalk o Ruby (che sembra essere la lingua Eiffel usata in questo libro), diventa un consiglio perfettamente valido. Queste lingue chiameranno implicitamente metodi per te. Non vi è alcuna differenza tra instance.attributee instance.getter_method.

Non vorrei sudare questo punto o prenderlo troppo dogmaticamente. L'intenzione è buona - non vuoi che gli utenti della tua classe si preoccupino dei dettagli di implementazione irrilevanti - ma non si traduce in modo chiaro nella sintassi di molti linguaggi moderni.


1
Punto molto importante riguardo al considerare l'anno in cui è stato presentato il suggerimento. Nit: Smalltalk e Simula risalgono agli anni '60 e '70, quindi 88 non sono certo "primi giorni".
Luser droog

2

Come utente, non è necessario sapere come viene implementato qualcosa.

Se le prestazioni sono un problema, qualcosa deve essere fatto all'interno dell'implementazione della classe, non attorno ad essa. Pertanto, l'azione corretta è quella di correggere l'implementazione della classe o di presentare un bug al manutentore.


3
È sempre vero che un metodo costoso dal punto di vista computazionale è un bug? Per un esempio banale, diciamo che mi occupo di riassumere le lunghezze di una serie di stringhe. Internamente, non so se le stringhe nella mia lingua sono in stile Pascal o in stile C. Nel primo caso, poiché le stringhe "conoscono" la loro lunghezza, posso aspettarmi che il mio ciclo di somma della lunghezza impieghi un tempo lineare dipendente dal numero di stringhe. Dovrei anche sapere che le operazioni che cambiano la lunghezza delle stringhe avranno un sovraccarico associato ad esse, poiché string.lengthverranno ricalcolate ogni volta che cambia.
Patrick Collins,

3
In quest'ultimo caso, poiché la stringa non "conosce" la sua lunghezza, posso aspettarmi che il mio ciclo di somma della lunghezza impieghi un tempo quadratico (che dipende sia dal numero di stringhe che dalle loro lunghezze), ma operazioni che cambiano la lunghezza delle stringhe sarà più economico. Nessuna di queste implementazioni è sbagliata e nessuna meriterebbe una segnalazione di bug, ma richiedono stili di codifica leggermente diversi per evitare singhiozzi inaspettati. Non sarebbe più facile se l'utente avesse almeno una vaga idea di cosa stesse succedendo?
Patrick Collins,

Quindi, se sai che la classe stringa implementa lo stile C, sceglierai un modo di codificare tenendo conto di quel fatto. E se la prossima versione della classe di stringa implementasse la nuova rappresentazione in stile Foo? Modificherai di conseguenza il tuo codice o accetterai la perdita di prestazioni causata da false assunzioni nel tuo codice?
mouviciel,

Vedo. Quindi la risposta OO a "Come posso ottenere alcune prestazioni extra dal mio codice, basandomi su un'implementazione specifica?" è "Non puoi". E la risposta a "Il mio codice è più lento di quanto mi aspetterei, perché?" è "Deve essere riscritto". È più o meno l'idea?
Patrick Collins,

2
@PatrickCollins La risposta OO si basa su interfacce e non implementazioni. Non utilizzare un'interfaccia che non includa le garanzie di prestazione come parte della definizione dell'interfaccia (come nell'esempio della dimensione List ++ di C ++ 11 garantita O (1)). Non richiede l'inclusione dei dettagli di implementazione nella definizione dell'interfaccia. Se il tuo codice è più lento di quello che vorresti, c'è qualche altra risposta che dovrai cambiarlo per essere più veloce (dopo averlo profilato per determinare i colli di bottiglia)?
stonemetal,

2

Qualsiasi documentazione orientata al programmatore che non è in grado di informare i programmatori sul costo della complessità delle routine / dei metodi è difettosa.

  • Stiamo cercando di produrre metodi privi di effetti collaterali.

  • Se l'esecuzione di un metodo presenta complessità del tempo di esecuzione e / o complessità della memoria diverse da O(1), in ambienti con limiti di memoria o di tempo, si può considerare che abbiano effetti collaterali .

  • Il principio della minima sorpresa viene violato se un metodo fa qualcosa di completamente inaspettato - in questo caso, trascinando la memoria o sprecando il tempo della CPU.


1

Penso che tu l'abbia capito correttamente, ma penso anche che tu abbia un buon punto. se Person.ageè implementato con un calcolo costoso, penso che mi piacerebbe vederlo anche nella documentazione. Potrebbe fare la differenza tra chiamarlo ripetutamente (se si tratta di un'operazione poco costosa) o chiamarlo una volta e memorizzarne il valore nella cache (se costoso). Non lo so per certo, ma penso che in questo caso Meyer potrebbe essere d'accordo sul fatto che un avviso nella documentazione dovrebbe essere incluso.

Un altro modo per gestirlo potrebbe essere quello di introdurre un nuovo attributo il cui nome implica che potrebbe aver luogo un lungo calcolo (come Person.ageCalculatedFromDB) e quindi Person.agerestituire un valore memorizzato nella cache all'interno della classe, ma questo potrebbe non essere sempre appropriato e sembra complicare eccessivamente cose, secondo me.


3
Si potrebbe anche argomentare che se è necessario conoscere il carattere agedi a Person, è necessario chiamare il metodo per ottenerlo indipendentemente. Se i chiamanti iniziano a fare cose troppo intelligenti per metà per evitare di dover effettuare il calcolo, corrono il rischio che le loro implementazioni non funzionino correttamente perché hanno superato un limite di compleanno. Le implementazioni costose nella classe si manifesteranno come problemi di prestazioni che possono essere sradicati dalla profilazione e miglioramenti come la memorizzazione nella cache possono essere fatti in classe, in cui tutti i chiamanti vedranno i benefici (e risultati corretti).
Blrfl,

1
@Blrfl: beh sì, la cache dovrebbe essere fatta in Personclasse, ma penso che la domanda fosse intesa come più generale e che Person.agefosse solo un esempio. Probabilmente ci sono alcuni casi in cui avrebbe più senso scegliere il chiamante - forse la chiamata ha due algoritmi diversi per calcolare lo stesso valore: uno veloce ma impreciso, uno molto più lento ma più preciso (il rendering 3D viene in mente come un posto dove ciò può accadere) e la documentazione dovrebbe menzionarlo.
FrustratedWithFormsDesigner,

Due metodi che forniscono risultati diversi sono un caso d'uso diverso rispetto a quando ci si aspetta la stessa risposta ogni volta.
Blrfl,

0

La documentazione per le classi orientate agli oggetti comporta spesso un compromesso tra la flessibilità dei gestori della classe e la possibilità di modificarne il design, anziché consentire ai consumatori della classe di sfruttare appieno il proprio potenziale. Se una classe immutabile avrà un numero di proprietà che avrà una certa esatta relazione tra loro (ad esempio la Left, RighteWidthproprietà di un rettangolo allineato alla griglia con coordinate intere), si potrebbe progettare la classe per memorizzare qualsiasi combinazione di due proprietà e calcolare la terza, oppure si potrebbe progettare per memorizzare tutte e tre. Se nulla sull'interfaccia chiarisce quali proprietà sono memorizzate, il programmatore della classe potrebbe essere in grado di cambiare il design nel caso in cui ciò risultasse utile per qualche motivo. Al contrario, se ad esempio due delle proprietà sono esposte come finalcampi e la terza no, le versioni future della classe dovranno sempre utilizzare le stesse due proprietà della "base".

Se le proprietà non hanno una relazione esatta (ad esempio perché sono floato doublepiuttosto che int), potrebbe essere necessario documentare quali proprietà "definiscono" il valore di una classe. Ad esempio, anche se si suppone che il Leftplus Widthsia uguale Right, la matematica in virgola mobile è spesso inesatta. Ad esempio, supponiamo Rectangleche un tipo che usa type Floataccetti Lefte Widthche i parametri del costruttore siano costruiti con Leftdato come 1234567fe Widthcome 1.1f. La migliore floatrappresentazione della somma è 1234568.125 [che può essere visualizzata come 1234568.13]; il prossimo più piccolo floatsarebbe 1234568.0. Se la classe effettivamente memorizza LefteWidth, potrebbe riportare il valore della larghezza così come è stato specificato. Se, tuttavia, il costruttore calcolasse in Rightbase al pass-in Lefte Width, e successivamente calcolato in Widthbase a Lefte Right, segnalerebbe la larghezza 1.25fanziché il pass-in 1.1f.

Con le classi mutabili, le cose possono essere ancora più interessanti, dal momento che una modifica a uno dei valori interconnessi implica una modifica ad almeno un'altra, ma potrebbe non essere sempre chiaro quale. In alcuni casi, può essere meglio per evitare di avere metodi che "set" una singola proprietà in quanto tale, ma invece hanno o metodi per es SetLeftAndWidtho SetLeftAndRight, altrimenti chiaramente quali sono stati specificati e che cambiano (ad esempio MoveRightEdgeToSetWidth, ChangeWidthToSetLeftEdgeo MoveShapeToSetRightEdge) .

A volte può essere utile avere una classe che tenga traccia di quali valori delle proprietà sono stati specificati e quali sono stati calcolati da altri. Ad esempio, una classe "moment in time" potrebbe includere un orario assoluto, un orario locale e un offset del fuso orario. Come con molti di questi tipi, date due informazioni qualsiasi, si può calcolare il terzo. Sapendo qualel'informazione è stata calcolata, tuttavia, a volte può essere importante. Ad esempio, supponiamo che un evento venga registrato come accaduto alle "17:00 UTC, fuso orario -5, ora locale 12:00 pm", e uno in seguito scopre che il fuso orario avrebbe dovuto essere -6. Se si sa che l'UTC è stato registrato da un server, il record deve essere corretto in "18:00 UTC, fuso orario -6, ora locale 12:00 pm"; se qualcuno digita l'ora locale senza orologio, dovrebbe essere "17:00 UTC, fuso orario -6, ora locale 11:00". Senza sapere se l'ora globale o locale dovrebbe essere considerata "più credibile", tuttavia, non è possibile sapere quale correzione applicare. Se, tuttavia, il record tenesse traccia dell'ora specificata, le modifiche al fuso orario potrebbero lasciarlo solo mentre si cambia l'altro.


0

Tutte queste regole su come nascondere le informazioni nelle classi hanno perfettamente senso sull'ipotesi di dover proteggere da qualcuno tra gli utenti della classe che commetterà l'errore di creare una dipendenza dall'implementazione interna.

Va bene integrare tale protezione, se la classe ha un tale pubblico. Ma quando l'utente scrive una chiamata a una funzione della tua classe, si fidano di te con il loro conto bancario al momento dell'esecuzione.

Ecco il genere di cose che vedo molto:

  1. Gli oggetti hanno un bit "modificato" che dice se sono, in un certo senso, obsoleti. Abbastanza semplice, ma poi hanno oggetti subordinati, quindi è semplice lasciare che "modificata" sia una funzione che sommi tutti gli oggetti subordinati. Quindi se ci sono più livelli di oggetti subordinati (a volte condividono lo stesso oggetto più di una volta) semplici "Get" s della proprietà "modificata" possono richiedere una buona frazione del tempo di esecuzione.

  2. Quando un oggetto viene in qualche modo modificato, si presume che gli altri oggetti sparsi nel software debbano essere "notificati". Ciò può avvenire su più livelli di struttura dati, finestre, ecc. Scritti da diversi programmatori e talvolta ripetersi in infinite ricorsioni che devono essere protette. Anche se tutti gli autori di quei gestori delle notifiche sono ragionevolmente attenti a non perdere tempo, l'intera interazione composita può finire per usare una frazione non prevista e dolorosamente ampia di tempo di esecuzione, e l'assunto che sia semplicemente "necessario" è fatto beato.

Quindi, mi piace vedere le classi che presentano un'interfaccia pulita e astratta al mondo esterno, ma mi piace avere un'idea di come funzionano, se non altro per capire quale lavoro mi stanno salvando. Ma oltre a ciò, tendo a pensare che "less is more". Le persone sono così innamorate della struttura dei dati che pensano che sia meglio, e quando eseguo l'ottimizzazione delle prestazioni, l'enorme ragione universale per i problemi di prestazioni è l'adesione faticosa alle strutture di dati gonfiate costruite nel modo in cui le persone vengono insegnate.

Quindi vai a capire.


0

L'aggiunta di dettagli di implementazione come "calcola o meno" o "informazioni sulle prestazioni" rende più difficile mantenere sincronizzati codice e documento .

Esempio:

Se si dispone di un metodo "costoso per le prestazioni", si desidera documentare "costoso" anche per tutte le classi che utilizzano il metodo? cosa succede se si modifica l'implementazione per non essere più costosa. Vuoi aggiornare queste informazioni anche a tutti i consumatori?

Naturalmente è bello che un manutentore del codice ottenga tutte le informazioni importanti dalla documentazione del codice, ma non mi piace la documentazione che afferma qualcosa che non è più valido (non sincronizzato con il codice)


0

Come la risposta accettata giunge alla conclusione:

Quindi: documentare problemi di prestazioni.

e il codice auto-documentato è considerato migliore della documentazione, ne consegue che il nome del metodo dovrebbe indicare risultati insoliti in termini di prestazioni.

Quindi ancora Person.ageper return current_year - self.birth_datema se il metodo utilizza un ciclo per calcolare l'età (sì):Person.calculateAge()

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.