Das CS-Programm meiner Schule vermeidet jede Erwähnung von objektorientierter Programmierung, deshalb habe ich einige Lektüre allein gemacht, um es zu ergänzen - insbesondere die objektorientierte Softwarekonstruktion von Bertrand Meyer.
Meyer weist wiederholt darauf hin, dass Klassen möglichst viele Informationen über ihre Implementierung verbergen sollten, was sinnvoll ist. Insbesondere argumentiert er wiederholt, dass Attribute (dh statische, nicht berechnete Eigenschaften von Klassen) und Routinen (Eigenschaften von Klassen, die Funktions- / Prozeduraufrufen entsprechen) nicht voneinander zu unterscheiden sind.
Wenn zum Beispiel eine Klasse Person
das Attribut hat age
, behauptet er, dass es unmöglich sein sollte, anhand der Notation zu erkennen, ob sie Person.age
intern so etwas wie return current_year - self.birth_date
oder einfach so entspricht return self.age
, wo self.age
sie als konstantes Attribut definiert wurde. Das macht für mich Sinn. Er behauptet jedoch weiterhin Folgendes:
Die Standard-Client-Dokumentation für eine Klasse, die als Kurzform der Klasse bezeichnet wird, soll nicht offenbaren, ob ein bestimmtes Merkmal ein Attribut oder eine Funktion ist (in Fällen, in denen es sich um eines handeln könnte).
Das heißt, er behauptet, dass selbst in der Dokumentation der Klasse nicht angegeben werden sollte, ob ein "Getter" eine Berechnung durchführt oder nicht.
Diesem folge ich nicht. Ist die Dokumentation nicht der einzige Ort, an dem es wichtig wäre, die Benutzer über diese Unterscheidung zu informieren? Wenn ich eine mit Person
Objekten gefüllte Datenbank entwerfen würde, wäre es dann nicht wichtig zu wissen, ob es sich Person.age
um einen teuren Aufruf handelt oder nicht , damit ich entscheiden könnte, ob eine Art Cache dafür implementiert werden soll oder nicht? Habe ich falsch verstanden, was er sagt, oder ist er nur ein besonders extremes Beispiel für die OOP-Designphilosophie?
quelle
Antworten:
Ich denke nicht, dass Meyer der Meinung ist, dass Sie dem Benutzer nicht sagen sollten, wann Sie eine teure Operation haben. Wenn Ihre Funktion die Datenbank erreicht oder eine Anfrage an einen Webserver stellt und mehrere Stunden mit dem Rechnen verbringt, muss dies für anderen Code bekannt sein.
Der Programmierer, der Ihre Klasse verwendet, muss jedoch nicht wissen, ob Sie Folgendes implementiert haben:
oder:
Die Leistungsmerkmale zwischen diesen beiden Ansätzen sind so gering, dass es keine Rolle spielen sollte. Dem Programmierer, der Ihre Klasse verwendet, sollte es wirklich egal sein, welche Sie haben. Das ist der Punkt von Meyer.
Angenommen, Sie haben eine Größenmethode für einen Container. Das könnte umgesetzt werden:
oder
oder es könnte sein:
Der Unterschied zwischen den ersten beiden sollte eigentlich keine Rolle spielen. Aber der letzte könnte schwerwiegende Auswirkungen auf die Leistung haben. Deshalb ist die STL zum Beispiel sagt , dass
.size()
istO(1)
. Es dokumentiert nicht genau, wie die Größe berechnet wird, aber es gibt mir die Leistungsmerkmale.Also : Leistungsprobleme dokumentieren. Dokumentieren Sie keine Implementierungsdetails. Es ist mir egal, wie std :: sort meine Sachen sortiert, solange es so richtig und effizient ist. Ihre Klasse sollte auch nicht dokumentieren, wie sie Dinge berechnet, aber wenn etwas ein unerwartetes Leistungsprofil aufweist, dokumentieren Sie dies.
quelle
// O(n) Traverses the entire user list.
len
tut dies nicht ... (Zumindest in einigen Situationen,O(n)
wie wir in einem College-Projekt erfahren haben, als ich vorschlug, die Länge zu speichern, anstatt sie bei jeder Schleifeniteration neu zu berechnen)O(n)
?Aus akademischer oder CS-puristischer Sicht ist es natürlich ein Versäumnis, in der Dokumentation irgendetwas über die Interna der Implementierung eines Features zu beschreiben. Dies liegt daran, dass der Benutzer einer Klasse im Idealfall keine Annahmen über die interne Implementierung der Klasse treffen sollte. Wenn sich die Implementierung ändert, wird dies im Idealfall kein Benutzer bemerken - die Funktion erstellt eine Abstraktion und die Interna sollten vollständig verborgen bleiben.
Allerdings leiden die meisten realen Programme von Joel Spolsky`s „Gesetz der undichten Abstraktionen“ , die besagt ,
Das heißt, es ist praktisch unmöglich, eine vollständige Black-Box-Abstraktion komplexer Features zu erstellen. Ein typisches Symptom hierfür sind Leistungsprobleme. Für reale Programme kann es daher sehr wichtig werden, welche Aufrufe teuer sind und welche nicht, und eine gute Dokumentation sollte diese Informationen enthalten (oder angeben, wo der Benutzer einer Klasse Annahmen über die Leistung treffen darf und wo nicht) ).
Mein Ratschlag lautet daher: Geben Sie Informationen zu potenziell teuren Anrufen an, wenn Sie Dokumente für ein reales Programm schreiben, und schließen Sie sie für ein Programm aus, das Sie nur zu Bildungszwecken für Ihren CS-Kurs schreiben, da Leistungsaspekte berücksichtigt werden sollten absichtlich außerhalb des Anwendungsbereichs.
quelle
Sie können schreiben, ob ein bestimmter Anruf teuer ist oder nicht. Verwenden Sie besser eine Benennungskonvention wie
getAge
für den schnellen Zugriff und /loadAge
oderfetchAge
für die teure Suche. Sie möchten den Benutzer auf jeden Fall informieren, wenn die Methode eine E / A durchführt.Jedes Detail, das Sie in der Dokumentation angeben, ist wie ein Vertrag, den die Klasse einhalten muss. Es sollte über wichtiges Verhalten informieren. Oft werden Sie Komplexitätsangaben mit großer O-Notation sehen. Aber normalerweise möchten Sie kurz und sachlich sein.
quelle
Ja.
Aus diesem Grund verwende ich manchmal
Find()
Funktionen, um anzuzeigen, dass das Aufrufen eine Weile dauern kann. Dies ist mehr eine Konvention als alles andere. Die Zeit, die für eine Funktion oder ein Attribut zu Rückkehr nimmt keinen Unterschied macht , um das Programm (obwohl es den Benutzer, könnte), obwohl unter Programmierern es ist die Erwartung , dass, wenn es als ein Attribut deklariert ist, die Kosten zu nennen sollten niedrig.In jedem Fall sollte es genügend Informationen im Code selbst geben, um abzuleiten, ob es sich um eine Funktion oder ein Attribut handelt, daher sehe ich die Notwendigkeit, dies in der Dokumentation zu sagen, nicht wirklich.
quelle
Get
Methoden über Attributen, um eine schwerere Operation anzuzeigen. Ich habe genug Code gesehen, in dem Entwickler davon ausgehen, dass eine Eigenschaft nur ein Accessor ist und ihn mehrmals verwendet, anstatt den Wert in einer lokalen Variablen zu speichern, und daher einen sehr komplexen Algorithmus mehrmals ausführen. Wenn es keine Konvention gibt, solche Eigenschaften nicht zu implementieren, und die Dokumentation keinen Hinweis auf die Komplexität gibt, dann wünsche ich allen, die eine solche Anwendung warten müssen, viel Glück.get
Methode, die einem Attributzugriff entspricht und daher nicht teuer ist.Es ist wichtig anzumerken, dass die erste Ausgabe dieses Buches 1988 in den Anfängen der OOP geschrieben wurde. Diese Leute arbeiteten mit rein objektorientierten Sprachen, die heute weit verbreitet sind. Unsere derzeit beliebtesten OO-Sprachen - C ++, C # und Java - haben einige ziemlich signifikante Unterschiede zu der Art und Weise, wie die frühen, rein OO-Sprachen funktionierten.
In einer Sprache wie C ++ und Java müssen Sie zwischen dem Zugriff auf ein Attribut und einem Methodenaufruf unterscheiden. Es gibt eine Welt voller Unterschiede zwischen
instance.getter_method
undinstance.getter_method()
. Einer bekommt tatsächlich deinen Wert und der andere nicht.Wenn Sie mit einer rein OO-Sprache arbeiten, der Smalltalk- oder Ruby-Sprache (die offenbar die in diesem Buch verwendete Eiffel-Sprache ist), wird dies zu einem absolut gültigen Ratschlag. Diese Sprachen rufen implizit Methoden für Sie auf. Es wird kein Unterschied zwischen
instance.attribute
undinstance.getter_method
.Ich würde diesen Punkt nicht schwitzen oder zu dogmatisch sehen. Die Absicht ist gut - Sie möchten nicht, dass sich die Benutzer Ihrer Klasse über irrelevante Implementierungsdetails Gedanken machen - aber es lässt sich nicht sauber in die Syntax vieler moderner Sprachen übersetzen.
quelle
Als Benutzer müssen Sie nicht wissen, wie etwas implementiert ist.
Wenn die Leistung ein Problem darstellt, muss innerhalb der Klassenimplementierung etwas getan werden, und nicht darum herum. Daher besteht die richtige Aktion darin, die Klassenimplementierung zu korrigieren oder einen Fehler beim Betreuer einzureichen.
quelle
string.length
sie bei jeder Änderung neu berechnet werden.Jede programmiererorientierte Dokumentation, die Programmierer nicht über die Komplexitätskosten von Routinen / Methoden informiert, ist fehlerhaft.
Wir streben nach nebenwirkungsfreien Methoden.
Wenn die Ausführung eines Verfahrens Zeitkomplexität ausgeführt wurde und / oder Speicher Komplexität außer
O(1)
, in Gedächtnis- oder zeitbeschränkten Umgebungen kann betrachtet werden , um Nebenwirkungen haben .Das Prinzip der geringsten Überraschung wird verletzt, wenn eine Methode etwas völlig Unerwartetes ausführt - in diesem Fall Speicher überfrachten oder CPU-Zeit verschwenden.
quelle
Ich denke, Sie haben ihn richtig verstanden, aber ich denke auch, dass Sie einen guten Punkt haben. wenn
Person.age
das mit einer teuren berechnung umgesetzt wird, dann würde ich das wohl auch gerne in der dokumentation sehen. Es kann den Unterschied zwischen dem wiederholten Aufrufen (wenn es sich um eine kostengünstige Operation handelt) oder dem einmaligen Aufrufen und dem Zwischenspeichern des Werts (wenn es teuer ist) ausmachen. Ich weiß es nicht genau, aber ich denke in diesem Fall könnte Meyer zustimmen, dass eine Warnung in die Dokumentation aufgenommen werden sollte.Eine andere Möglichkeit, dies zu handhaben, besteht darin, ein neues Attribut einzuführen, dessen Name impliziert, dass möglicherweise eine lange Berechnung stattfindet (z. B.
Person.ageCalculatedFromDB
) und dannPerson.age
einen Wert zurückgibt, der in der Klasse zwischengespeichert ist. Dies ist jedoch möglicherweise nicht immer angemessen und scheint zu kompliziert Dinge, meiner Meinung nach.quelle
age
von a wissen mussPerson
, die Methode aufrufen sollte, um es trotzdem zu bekommen. Wenn Anrufer anfangen, zu clevere Dinge zu tun, um der Berechnung aus dem Weg zu gehen, laufen sie Gefahr, dass ihre Implementierungen nicht richtig funktionieren, weil sie eine Geburtstagsgrenze überschritten haben. Teure Implementierungen in der Klasse manifestieren sich als Leistungsprobleme, die durch Profilerstellung behoben werden können, und Verbesserungen wie das Zwischenspeichern können in der Klasse durchgeführt werden, wobei alle Aufrufer die Vorteile (und korrekten Ergebnisse) sehen.Person
klasse gemacht werden, aber ich denke die frage war allgemeiner gedacht und dasPerson.age
war nur ein beispiel. Es gibt wahrscheinlich einige Fälle, in denen die Auswahl für den Anrufer sinnvoller wäre - vielleicht hat der Angerufene zwei verschiedene Algorithmen, um den gleichen Wert zu berechnen: einen schnellen, aber ungenauen, einen viel langsameren, aber genaueren (3D-Rendering wird als eine Stelle in den Sinn gebracht) wo das passieren kann), und die Dokumentation sollte dies erwähnen.Die Dokumentation für objektorientierte Klassen beinhaltet häufig einen Kompromiss zwischen der Flexibilität für die Betreuer der Klasse, ihren Entwurf zu ändern, und der Möglichkeit für die Konsumenten der Klasse, ihr Potenzial voll auszuschöpfen. Wenn eine unveränderliche Klasse wird eine Reihe von Eigenschaften aufweist , die eine gewisse hat genaue Beziehung zueinander (zB
Left
,Right
undWidth
Eigenschaften eines mit einem Ganzzahlkoordinatengitter ausgerichteten Rechtecks), kann man die Klasse so entwerfen, dass sie eine beliebige Kombination von zwei Eigenschaften speichert und die dritte berechnet, oder alle drei Eigenschaften speichern. Wenn nichts an der Schnittstelle verdeutlicht, welche Eigenschaften gespeichert sind, kann der Programmierer der Klasse möglicherweise das Design ändern, falls sich dies aus irgendeinem Grund als hilfreich erweisen sollte. Wenn im Gegensatz dazu beispielsweise zwei der Eigenschaften alsfinal
Felder verfügbar gemacht werden und die dritte nicht, müssen zukünftige Versionen der Klasse immer dieselben zwei Eigenschaften als "Basis" verwenden.Wenn Eigenschaften keine exakte Beziehung haben (z. B. weil sie
float
oderdouble
eher alsint
), muss möglicherweise dokumentiert werden, welche Eigenschaften den Wert einer Klasse "definieren". Beispielsweise ist Gleitkomma-Mathematik oft ungenau , obwohlLeft
PlusWidth
gleich sein sollRight
. Nehmen wir beispielsweise an, dass ein Parameter ,Rectangle
der typeFloat
acceptLeft
undWidth
as als Konstruktorparameter verwendet, mitLeft
as1234567f
undWidth
as erstellt wird1.1f
. Die bestefloat
Darstellung der Summe ist 1234568.125 [die als 1234568.13 angezeigt werden kann]; der nächst kleinerefloat
wäre 1234568.0. Wenn die Klasse tatsächlichLeft
und speichertWidth
kann der angegebene Breitenwert ausgegeben werden. Wenn jedoch berechnet , der KonstruktorRight
auf der Basis der übergebenen inLeft
undWidth
, und später berechnetWidth
auf der GrundlageLeft
undRight
würde es die Breite berichten und1.25f
nicht als übergebenen in1.1f
.Bei veränderlichen Klassen kann es noch interessanter sein, da eine Änderung an einem der miteinander verknüpften Werte eine Änderung an mindestens einem anderen impliziert, aber es ist möglicherweise nicht immer klar, welcher. Die „set“ eine einzige Eigenschaft als solche in einigen Fällen kann es am besten sein , Methoden zu müssen, sondern entweder Methoden zB
SetLeftAndWidth
oderSetLeftAndRight
oder auch deutlich machen , welche Eigenschaften festgelegt werden, und die ändern (zBMoveRightEdgeToSetWidth
,ChangeWidthToSetLeftEdge
oderMoveShapeToSetRightEdge
) .Manchmal kann es nützlich sein, eine Klasse zu haben, die verfolgt, welche Eigenschaftswerte angegeben und welche aus anderen berechnet wurden. Beispielsweise kann eine "Moment in Zeit" -Klasse eine absolute Zeit, eine Ortszeit und einen Zeitzonenversatz enthalten. Wie bei vielen derartigen Typen kann man mit zwei gegebenen Informationen die dritte berechnen. Wissen wasStück Information wurde berechnet, kann jedoch manchmal wichtig sein. Angenommen, ein Ereignis ist um "17:00 UTC, Zeitzone -5, Ortszeit 12:00 Uhr" aufgetreten, und später wird festgestellt, dass die Zeitzone -6 hätte sein müssen. Wenn bekannt ist, dass die UTC von einem Server aufgezeichnet wurde, sollte die Aufzeichnung auf "18:00 UTC, Zeitzone -6, Ortszeit 12:00 Uhr" korrigiert werden. Wenn jemand die Ortszeit außerhalb der Uhr eingibt, sollte dies "17:00 UTC, Zeitzone -6, Ortszeit 11:00 Uhr" sein. Ohne zu wissen, ob die globale oder die lokale Zeit als "glaubwürdiger" angesehen werden soll, ist es jedoch nicht möglich zu wissen, welche Korrektur angewendet werden soll. Wenn jedoch in der Aufzeichnung festgehalten wird, welche Zeit angegeben wurde, können Änderungen an der Zeitzone diese in Ruhe lassen, während die andere geändert wird.
quelle
All diese Regeln zum Ausblenden von Informationen in Klassen sind unter der Annahme, dass Sie jemanden unter den Benutzern der Klasse davor schützen müssen, der den Fehler macht, eine Abhängigkeit von der internen Implementierung zu erstellen, durchaus sinnvoll.
Es ist in Ordnung, einen solchen Schutz einzubauen, wenn die Klasse ein solches Publikum hat. Wenn der Benutzer jedoch eine Funktion in Ihrer Klasse aufruft, vertraut er Ihnen das Bankkonto zur Ausführungszeit.
Folgendes sehe ich oft:
Objekte haben ein "modifiziertes" Bit, das besagt, ob sie in gewisser Weise veraltet sind. Einfach genug, aber dann haben sie untergeordnete Objekte, so dass es einfach ist, "modified" eine Funktion zu lassen, die über alle untergeordneten Objekte summiert. Wenn es dann mehrere Ebenen untergeordneter Objekte gibt (die manchmal dasselbe Objekt mehrmals gemeinsam nutzen), können einfache Abrufe der Eigenschaft "modified" einen gesunden Bruchteil der Ausführungszeit in Anspruch nehmen.
Wenn ein Objekt auf irgendeine Weise modifiziert wird, wird angenommen, dass andere Objekte, die in der Software verstreut sind, "benachrichtigt" werden müssen. Dies kann über mehrere Ebenen von Datenstrukturen, Fenstern usw. erfolgen, die von verschiedenen Programmierern geschrieben wurden, und manchmal in unendlichen Rekursionen wiederholt werden, gegen die geschützt werden muss. Selbst wenn alle Autoren dieser Benachrichtigungs-Handler einigermaßen darauf bedacht sind, keine Zeit zu verschwenden, kann die gesamte zusammengesetzte Interaktion einen unvorhergesehenen und schmerzhaft großen Teil der Ausführungszeit in Anspruch nehmen, und die Annahme, dass dies einfach "notwendig" ist, wird munter getroffen.
Also, ich mag es Klassen zu sehen, die eine schöne abstrakte Oberfläche für die Außenwelt darstellen, aber ich mag es, eine Vorstellung davon zu haben, wie sie funktionieren, wenn ich nur verstehe, welche Arbeit sie mir ersparen. Aber darüber hinaus neige ich dazu, das Gefühl zu haben, dass "weniger mehr ist". Die Leute sind so verliebt in Datenstrukturen, dass sie denken, dass mehr besser ist, und wenn ich die Leistung optimiere, ist der universelle Grund für Leistungsprobleme das sklavische Festhalten an aufgeblähten Datenstrukturen, die so aufgebaut sind, wie Menschen unterrichtet werden.
Also mach eine Figur.
quelle
Durch Hinzufügen von Implementierungsdetails wie "Berechnen oder nicht" oder "Leistungsinfo" wird es schwieriger, Code und Dokument synchron zu halten .
Beispiel:
Wenn Sie eine "leistungsintensive" Methode haben, möchten Sie "teuer" auch für alle Klassen dokumentieren, die diese Methode verwenden? Was, wenn Sie die Implementierung so ändern, dass sie nicht mehr teuer ist? Möchten Sie diese Informationen auch für alle Verbraucher aktualisieren?
Natürlich ist es für einen Code-Betreuer schön, alle wichtigen Informationen aus der Code-Dokumentation zu erhalten, aber ich mag keine Dokumentation, die behauptet, dass etwas nicht mehr gültig ist (nicht mehr mit dem Code synchron).
quelle
Wie die akzeptierte Antwort zu dem Schluss kommt:
und selbst dokumentierte Code wird als besser als Dokumentation folgt daraus , dass der Methodenname soll ungewöhnliche Performance - Ergebnisse angeben.
Also noch
Person.age
für,return current_year - self.birth_date
aber wenn die Methode eine Schleife verwendet, um das Alter zu berechnen (ja):Person.calculateAge()
quelle