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 della programmazione orientata agli oggetti, quindi ho fatto alcune letture da solo per integrarlo, in particolare 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, egli sostiene ripetutamente che gli attributi (vale a dire, proprietà statiche, non calcolate delle classi) e le routine (proprietà delle classi che corrispondono alle chiamate funzione / procedura) dovrebbero essere indistinguibili l'una dall'altra.

Ad esempio, se una classe Person ha l'attributo age , afferma che dovrebbe essere impossibile dire, dalla notazione, se Person.age corrisponde internamente a qualcosa come return current_year - self.birth_date o semplicemente return self.age , dove self.age è stato definito come attributo costante. Questo ha senso per me. Tuttavia, prosegue affermando quanto segue:

The standard client documentation for a class, known as the short form of the class, will be devised so as not to reveal whether a given feature is an attribute or a function (in cases for which it could be either).

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

Questo, non seguo. La documentazione non è l'unico posto in cui sarebbe importante informare gli utenti di questa distinzione? Se dovessi progettare un database pieno di oggetti Person , non sarebbe importante sapere se Person.age è una chiamata costosa, quindi 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 di progettazione OOP?

    
posta Patrick Collins 26.08.2013 - 17:25
fonte

12 risposte

57

Non penso che la tesi di Meyer sia che non dovresti dire all'utente quando hai un'operazione costosa. Se la tua funzione colpisce il database, o effettua una richiesta a un server web e impiega diverse ore a calcolare, un altro codice dovrà saperlo.

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

return currentAge;

o

return getCurrentYear() - yearBorn;

Le caratteristiche delle prestazioni tra questi due approcci sono così minime che non dovrebbe avere importanza. Il programmatore che usa la tua classe in realtà non dovrebbe preoccuparsi di quello che hai. Questo è il punto di Meyer.

Ma non è sempre così, per esempio, supponiamo di avere un metodo di misura su un contenitore. Ciò 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 in realtà non dovrebbe avere importanza. Ma l'ultimo potrebbe avere serie conseguenze sulle prestazioni. Ecco perché l'STL, ad esempio, afferma che .size() è O(1) . Non documenta esattamente come viene calcolata la dimensione, ma mi dà le caratteristiche di prestazione.

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

    
risposta data 26.08.2013 - 20:39
fonte
16

Da un punto di vista accademico o purista di CS, è ovviamente un errore descrivere nella documentazione qualsiasi cosa circa gli aspetti interni dell'implementazione di una funzionalità. Questo perché l'utente di una classe dovrebbe idealmente non fare alcuna ipotesi sull'implementazione interna della classe. Se l'implementazione cambia, idealmente nessun utente lo noterà: la feature crea un'astrazione e gli interni dovrebbero essere completamente nascosti.

Tuttavia, la maggior parte dei programmi del mondo reale soffre di "Legge sulle stravaganti astrazioni" di Joel Spolsky, che dice

"All non-trivial abstractions, to some degree, are leaky."

Ciò significa che è praticamente impossibile creare un'astrazione completa black-box di funzioni complesse. E un sintomo tipico di questo sono i 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 è: includere le informazioni sulle potenziali chiamate costose se si scrivono documenti per un programma del mondo reale ed escluderlo per un programma che si sta scrivendo solo a scopo didattico del proprio corso CS, dato che qualsiasi considerazione sul rendimento dovrebbe essere mantenuto intenzionalmente fuori dal campo di applicazione.

    
risposta data 26.08.2013 - 21:46
fonte
12

Puoi scrivere se una determinata chiamata è costosa o meno. Meglio, usa una convenzione di denominazione come getAge per l'accesso rapido e loadAge o fetchAge per la ricerca costosa. Sicuramente vuoi informare l'utente se il metodo sta eseguendo qualsiasi IO.

Ogni dettaglio che dai alla documentazione è come un contratto che deve essere onorato dalla classe. Dovrebbe informare su un comportamento importante. Spesso, vedrai l'indicazione di complessità con una notazione O grande. Ma di solito vuoi essere breve e preciso.

    
risposta data 26.08.2013 - 17:35
fonte
9

If I were to design a database filled with Person objects, wouldn't it be important to know whether or not Person.age is an expensive call?

Sì.

Questo è il motivo per cui a volte utilizzo Find() funzioni per indicare che la chiamata potrebbe richiedere un po 'di tempo. Questa è più una convenzione che altro. Il tempo necessario per il ritorno di una funzione o di un attributo non fa alcuna differenza per il programma (sebbene possa essere per l'utente), sebbene tra i programmatori ci sia è un'aspettativa che, se è dichiarata come attributo, il costo per chiamarlo dovrebbe essere basso.

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

    
risposta data 26.08.2013 - 17:33
fonte
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 usati oggi. I nostri linguaggi OO più popolari oggi - C ++, C # e amp; Java - hanno alcune differenze piuttosto significative rispetto al modo in cui le lingue iniziali, più puramente OO, hanno funzionato.

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

Quando si lavora con un linguaggio OO più puramente, della persuasione Smalltalk o Ruby (che sembra che la lingua Eiffel usata in questo libro sia), diventa un consiglio perfettamente valido. Queste lingue chiameranno implicitamente metodi per te. Non ci sono differenze tra instance.attribute e instance.getter_method .

Non vorrei trasudare questo punto o prenderlo troppo dogmaticamente. L'intento è buono: non vuoi che gli utenti della tua classe si preoccupino dei dettagli di implementazione irrilevanti, ma non si traducono in modo pulito nella sintassi di molti linguaggi moderni.

    
risposta data 26.08.2013 - 20:01
fonte
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 esso. Pertanto, l'azione corretta consiste nel correggere l'implementazione della classe o nel presentare un bug al manutentore.

    
risposta data 26.08.2013 - 17:48
fonte
2

Ogni pezzo di documentazione orientato al programmatore che non riesce a informare i programmatori sul costo della complessità di routine / metodi è difettoso.

  • Stiamo cercando di produrre metodi senza effetti collaterali.

  • Se l'esecuzione di un metodo ha complessità temporale e / o complessità della memoria diverse da O(1) , in ambienti con vincoli di memoria o tempo può essere considerato avere effetti collaterali .

  • Il principio di sorpresa minima viene violato se un metodo fa qualcosa di completamente inaspettato - in questo caso, memoria da hogging o spreco di tempo della CPU.

risposta data 26.08.2013 - 22:49
fonte
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, allora penso che mi piacerebbe vederlo anche nella documentazione. Potrebbe fare la differenza tra chiamarlo ripetutamente (se è un'operazione poco costosa) o chiamarlo una volta e memorizzare il valore nella cache (se è costoso). Non so per certo, ma penso che in questo caso potrebbe essere d'accordo che un avvertimento nella documentazione dovrebbe essere incluso.

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

    
risposta data 26.08.2013 - 17:34
fonte
0

La documentazione per le classi orientate agli oggetti spesso implica un compromesso tra il fatto che i manutentori della flessibilità della classe cambino il proprio 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 relazione esatto (ad esempio le proprietà Left , Right e Width di una griglia di coordinate intero- rettangolo allineato), si potrebbe progettare la classe per memorizzare qualsiasi combinazione di due proprietà e calcolare la terza, oppure si potrebbe progettarla per memorizzarle tutte e tre. Se nulla dell'interfaccia rende chiaro quali proprietà sono memorizzate, il programmatore della classe potrebbe essere in grado di modificare il progetto nel caso in cui ciò risultasse utile per qualche motivo. Al contrario, se ad es. due delle proprietà sono esposte come campi final e la terza no, quindi le future versioni della classe dovranno sempre utilizzare le stesse due proprietà come "base".

Se le proprietà non hanno una relazione esatta (ad esempio perché sono float o double anziché int ), potrebbe essere necessario documentare quali proprietà "definiscono" il valore di una classe. Ad esempio, anche se Left più Width è uguale a Right , la matematica a virgola mobile è spesso inesatta. Ad esempio, supponiamo un Rectangle che utilizza il tipo Float accetta Left e Width mentre i parametri del costruttore sono costruiti con Left dato come 1234567f e Width come 1.1f . La migliore rappresentazione float della somma è 1234568.125 [che può essere visualizzata come 1234568.13]; il prossimo più piccolo float sarebbe 1234568.0. Se la classe memorizza effettivamente Left e Width , può riportare il valore della larghezza come è stato specificato. Se, tuttavia, il costruttore ha calcolato Right in base al valore di Left e Width , e in seguito a Width basato su Left e Right , segnalerebbe la larghezza come 1.25f piuttosto rispetto al passato% di1.1f.

Con le classi mutabili, le cose possono essere ancora più interessanti, dal momento che una modifica a uno dei valori correlati implicherà una modifica ad almeno un altro, ma potrebbe non essere sempre chiaro quale. In alcuni casi, potrebbe essere meglio evitare di avere metodi che "impostano" una singola proprietà in quanto tale, ma invece hanno metodi per es. SetLeftAndWidth o SetLeftAndRight , oppure chiarisci quali proprietà vengono specificate e quali cambiano (ad esempio MoveRightEdgeToSetWidth , ChangeWidthToSetLeftEdge o MoveShapeToSetRightEdge ).

A volte può essere utile avere una classe che tenga traccia dei valori delle proprietà che sono stati specificati e che sono stati calcolati da altri. Ad esempio, una classe "momento nel tempo" potrebbe includere un tempo assoluto, un'ora locale e un fuso orario. Come con molti di questi tipi, dati due pezzi di informazione, uno può calcolare il terzo. Sapendo che quale pezzo di informazione è stato calcolato, tuttavia, a volte può essere importante. Ad esempio, si supponga che un evento venga registrato come avvenuto a "17:00 UTC, fuso orario -5, ora locale 12:00 pm" e in seguito si scopre che il fuso orario deve essere stato -6. Se si sa che l'UTC è stato registrato su un server, il record deve essere corretto su "18:00 UTC, fuso orario -6, ora locale 12:00 pm"; se qualcuno ha inserito l'ora locale da un orologio dovrebbe essere "17:00 UTC, fuso orario -6, ora locale 11:00". Senza sapere se l'ora globale o locale debba essere considerata "più credibile", tuttavia, non è possibile sapere quale correzione deve essere applicata. Se, tuttavia, il record tiene traccia di quale tempo è stato specificato, le modifiche al fuso orario potrebbero lasciare quella sola mentre si cambia l'altra.

    
risposta data 27.08.2013 - 17:51
fonte
0

Tutte queste regole su come nascondere le informazioni nelle classi hanno perfettamente senso nell'ipotesi di dover proteggere contro quella persona tra gli utenti della classe che farà l'errore di creare una dipendenza dall'implementazione interna.

Va bene costruire questa protezione, se la classe ha un pubblico di questo tipo. Ma quando l'utente scrive una chiamata a una funzione della classe, si fida di te con il suo conto bancario in tempo di esecuzione.

Ecco il genere di cose che vedo molto:

  1. Gli oggetti hanno un bit "modificato" che dice se sono, in un certo senso, superati. È abbastanza semplice, ma poi hanno oggetti subordinati, quindi è facile lasciare che "modificato" sia una funzione che riassume tutti gli oggetti subordinati. Quindi, se ci sono più livelli di oggetti subordinati (a volte condividono lo stesso oggetto più di una volta), i semplici "Get" della proprietà "modificata" possono finire per prendere una buona parte 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 ripetuti in infinite ricorsioni che devono essere sorvegliati. Anche se tutti gli scrittori di questi gestori di notifiche sono ragionevolmente attenti a non sprecare tempo, l'intera interazione composita può finire usando una frazione imprevedibile e dolorosamente grande del tempo di esecuzione, e l'ipotesi che sia semplicemente "necessario" è fatta allegramente.

COSÌ, mi piace vedere le classi che presentano una bella interfaccia astratta pulita al mondo esterno, ma mi piace avere un'idea di come funzionano, se non altro per capire che lavoro mi stanno salvando. Ma oltre a questo, tendo a sentire che "less is more". Le persone sono così innamorate della struttura dei dati che pensano che sia meglio, e quando eseguo il tuning delle prestazioni il motivo universale per i problemi di prestazioni è l'aderenza servile a strutture di dati gonfie costruite nel modo in cui le persone vengono insegnate.

Quindi vai a capire.

    
risposta data 01.09.2013 - 02:35
fonte
0

L'aggiunta di dettagli di implementazione come "calcola o non" o "informazioni sul rendimento" rendono più difficile da mantenere il codice e il documento in sincronizzazione .

Esempio:

Se si dispone di un metodo "costoso per prestazioni", si desidera documentare "costoso" anche per tutte le classi che utilizzano il metodo? cosa succede se si cambia 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)

    
risposta data 01.09.2013 - 10:07
fonte
0

Come la risposta accettata giunge alla conclusione:

So: document performance issues.

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

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

    
risposta data 12.09.2013 - 21:43
fonte

Leggi altre domande sui tag