Ich schreibe Klassen, die "auf eine bestimmte Art und Weise verwendet werden müssen" (ich denke, alle Klassen müssen ...).
Zum Beispiel erstelle ich die fooManager
Klasse, für die ein Aufruf erforderlich ist, zum Beispiel Initialize(string,string)
. Und um das Beispiel ein wenig voranzutreiben, wäre die Klasse nutzlos, wenn wir nicht auf ihre ThisHappened
Aktion hören.
Mein Punkt ist, die Klasse, die ich schreibe, erfordert Methodenaufrufe. Aber es wird gut kompiliert, wenn Sie diese Methoden nicht aufrufen und am Ende einen leeren neuen FooManager erhalten. Irgendwann funktioniert es entweder nicht oder stürzt ab, je nach Klasse und Einsatzzweck. Der Programmierer, der meine Klasse implementiert, würde offensichtlich hineinschauen und feststellen, dass "Oh, ich habe Initialize nicht aufgerufen!", Und es wäre in Ordnung.
Aber das gefällt mir nicht. Was ich im Idealfall möchte, ist der Code NICHT zu kompilieren, wenn die Methode nicht aufgerufen wurde; Ich vermute, das ist einfach nicht möglich. Oder etwas, das sofort sichtbar und klar sein würde.
Ich bin beunruhigt über die derzeitige Vorgehensweise, die ich hier verfolge:
Fügen Sie einen privaten Booleschen Wert in die Klasse ein und überprüfen Sie überall, ob die Klasse initialisiert ist. Wenn nicht, werde ich eine Ausnahme auslösen, die besagt: "Die Klasse wurde nicht initialisiert, sind Sie sicher, dass Sie anrufen .Initialize(string,string)
?".
Ich bin mit diesem Ansatz einverstanden, aber es führt zu viel Code, der kompiliert wird und letztendlich für den Endbenutzer nicht notwendig ist.
Manchmal ist es sogar noch mehr Code, wenn es mehr Methoden als nur einen Initiliaze
Aufruf gibt. Ich versuche, meine Klassen mit nicht zu vielen öffentlichen Methoden / Aktionen zu belassen, aber das löst das Problem nicht, es bleibt nur vernünftig.
Was ich hier suche, ist:
- Ist mein Ansatz korrekt?
- Gibt es eine bessere?
- Was tut / berät ihr?
- Versuche ich, ein Problem zu lösen, das nicht auftaucht? Ich habe von Kollegen erfahren, dass der Programmierer die Klasse überprüfen soll, bevor er versucht, sie zu verwenden. Ich bin mit Respekt anderer Meinung, aber das ist eine andere Sache, die ich glaube.
Einfach ausgedrückt, ich versuche einen Weg zu finden, um nie zu vergessen, Aufrufe zu implementieren, wenn diese Klasse später oder von jemand anderem wiederverwendet wird.
Erklärungen:
Um hier viele Fragen zu klären:
Ich spreche definitiv NICHT nur über den Initialisierungsteil einer Klasse, sondern es ist das ganze Leben. Verhindern Sie, dass Kollegen eine Methode zweimal aufrufen, und stellen Sie sicher, dass sie X vor Y usw. aufrufen. Alles, was am Ende obligatorisch und in der Dokumentation wäre, aber ich möchte, dass Code so einfach und klein wie möglich ist. Die Idee von Asserts hat mir sehr gut gefallen, obwohl ich mir ziemlich sicher bin, dass ich einige andere Ideen mischen muss, da Asserts nicht immer möglich sein werden.
Ich benutze die C # -Sprache! Wie habe ich das nicht erwähnt ?! Ich arbeite in einer Xamarin-Umgebung und erstelle mobile Apps in der Regel mit 6 bis 9 Projekten in einer Lösung, einschließlich PCL-, iOS-, Android- und Windows-Projekten. Ich bin seit ungefähr anderthalb Jahren Entwickler (Schule und Arbeit kombiniert), daher meine manchmal lächerlichen Aussagen \ Fragen. All das ist hier wahrscheinlich irrelevant, aber zu viele Informationen sind nicht immer eine schlechte Sache.
Ich kann aufgrund von Plattformbeschränkungen und der Verwendung von Dependency Injection nicht immer alle obligatorischen Elemente in den Konstruktor einfügen, da andere Parameter als Schnittstellen nicht in der Tabelle enthalten sind. Oder vielleicht reicht mein Wissen nicht aus, was durchaus möglich ist. Meistens handelt es sich nicht um ein Initialisierungsproblem, sondern um mehr
Wie kann ich sicherstellen, dass er sich für diese Veranstaltung angemeldet hat?
wie kann ich sicherstellen, dass er nicht vergaß, "den Prozess irgendwann anzuhalten"
Hier erinnere ich mich an eine Klasse zum Abrufen von Werbung. Solange die Ansicht, in der die Anzeige sichtbar ist, sichtbar ist, ruft die Klasse jede Minute eine neue Anzeige ab. Diese Klasse benötigt bei der Erstellung eine Ansicht, in der die Anzeige angezeigt werden kann, die offensichtlich in einem Parameter enthalten sein kann. Sobald die Ansicht verschwunden ist, muss StopFetching () aufgerufen werden. Andernfalls würde die Klasse weiterhin Anzeigen für eine Ansicht abrufen, die noch nicht vorhanden ist, und das ist schlecht.
Diese Klasse hat auch Ereignisse, die abgehört werden müssen, wie zum Beispiel "AdClicked". Alles funktioniert einwandfrei, wenn es nicht abgehört wird, aber wir verlieren die Verfolgung von Analysen, wenn keine Abgriffe registriert sind. Die Anzeige funktioniert jedoch weiterhin, sodass der Nutzer und der Entwickler keinen Unterschied bemerken und die Analyse nur falsche Daten enthält. Das muss vermieden werden, aber ich bin nicht sicher, wie Entwickler wissen können, dass sie sich für das Tao-Ereignis registrieren müssen. Das ist zwar ein vereinfachtes Beispiel, aber die Idee ist, "stellen Sie sicher, dass er die öffentliche Aktion verwendet, die verfügbar ist" und natürlich zum richtigen Zeitpunkt!
initialize
erst spät nach der Objekterstellung erfolgen? Wäre der ctor zu "riskant" in dem Sinne, dass er Ausnahmen werfen und die Schöpfungskette durchbrechen könnte?new
wenn das Objekt nicht zur Verwendung bereit ist. Wenn Sie sich auf eine Initialisierungsmethode verlassen, werden Sie immer wieder verfolgt und sollten diese vermeiden, es sei denn, dies ist absolut notwendig. Das ist zumindest meine Erfahrung.Antworten:
In solchen Fällen ist es am besten, das Typensystem Ihrer Sprache zu verwenden, um Sie bei der richtigen Initialisierung zu unterstützen. Wie können wir verhindern,
FooManager
dass a verwendet wird, ohne initialisiert zu werden? Durch eine VerhinderungFooManager
von wird erstellt , ohne die notwendigen Informationen , um es richtig zu initialisieren. Insbesondere liegt die gesamte Initialisierung in der Verantwortung des Konstrukteurs. Sie sollten niemals zulassen, dass Ihr Konstruktor ein Objekt in einem unzulässigen Zustand erstellt.Aufrufer müssen jedoch eine
FooManager
erstellen, bevor sie sie initialisieren können, z. B. weil dieFooManager
als Abhängigkeit weitergegeben wird.Erstellen Sie keine,
FooManager
wenn Sie keine haben. Sie können stattdessen ein Objekt übergeben, mit dem Sie eine vollständig erstellte Datei abrufen könnenFooManager
, jedoch nur mit den Initialisierungsinformationen. (In der funktionalen Programmierung schlage ich vor, dass Sie eine Teilanwendung für den Konstruktor verwenden.) Bsp .:Das Problem dabei ist, dass Sie die Init-Informationen jedes Mal angeben müssen, wenn Sie auf die zugreifen
FooManager
.Wenn dies in Ihrer Sprache erforderlich ist, können Sie die
getFooManager()
Operation in eine Factory- oder Builder-ähnliche Klasse einbinden.Ich möchte wirklich zur Laufzeit prüfen, ob die
initialize()
Methode aufgerufen wurde, anstatt eine Lösung auf Typsystemebene zu verwenden.Es ist möglich, einen Kompromiss zu finden. Wir erstellen eine Wrapper-Klasse
MaybeInitializedFooManager
mit einerget()
Methode, die die zurückgibtFooManager
, aber auslöst, wenn dieFooManager
nicht vollständig initialisiert wurde. Dies funktioniert nur, wenn die Initialisierung über den Wrapper erfolgt oder wenn es eineFooManager#isInitialized()
Methode gibt.Ich möchte die API meiner Klasse nicht ändern.
In diesem Fall sollten Sie die
if (!initialized) throw;
Bedingungen in jeder Methode vermeiden . Glücklicherweise gibt es ein einfaches Muster, um dies zu lösen.Das Objekt, das Sie Benutzern zur Verfügung stellen, ist lediglich eine leere Shell, die alle Aufrufe an ein Implementierungsobjekt delegiert. Standardmäßig gibt das Implementierungsobjekt für jede Methode, die nicht initialisiert wurde, einen Fehler aus. Die
initialize()
Methode ersetzt jedoch das Implementierungsobjekt durch ein vollständig erstelltes Objekt.Dies extrahiert das Hauptverhalten der Klasse in die
MainImpl
.quelle
FooManager
und inUninitialisedImpl
besser ist als das Wiederholenif (!initialized) throw;
.FooManager
es viele Methoden gibt, ist es möglicherweise einfacher, als einige derif (!initialized)
Überprüfungen zu vergessen . Aber in diesem Fall sollten Sie es wahrscheinlich vorziehen, die Klasse zu trennen.Die effektivste und hilfreichste Methode, um zu verhindern, dass Clients ein Objekt "missbrauchen", besteht darin, es unmöglich zu machen.
Die einfachste Lösung ist das Zusammenführen
Initialize
mit dem Konstruktor. Auf diese Weise ist das Objekt im nicht initialisierten Zustand niemals für den Client verfügbar, sodass der Fehler nicht möglich ist. Falls Sie die Initialisierung nicht im Konstruktor selbst durchführen können, können Sie eine Factory-Methode erstellen. Wenn für Ihre Klasse beispielsweise die Registrierung bestimmter Ereignisse erforderlich ist, kann der Ereignis-Listener als Parameter in der Konstruktor- oder Factory-Methodensignatur erforderlich sein.Wenn Sie vor der Initialisierung auf das nicht initialisierte Objekt zugreifen müssen, können Sie die beiden Status als separate Klassen implementieren. Beginnen Sie also mit einer
UnitializedFooManager
Instanz, die eineInitialize(...)
Methode hat, die zurückgibtInitializedFooManager
. Die Methoden, die nur im initialisierten Zustand aufgerufen werden können, existieren nur amInitializedFooManager
. Dieser Ansatz kann bei Bedarf auf mehrere Status erweitert werden.Im Vergleich zu Laufzeitausnahmen ist es aufwendiger, Zustände als unterschiedliche Klassen darzustellen, aber Sie erhalten auch die Garantie, dass Sie keine Methoden aufrufen, die für den Objektzustand ungültig sind, und die Zustandsübergänge werden deutlicher im Code dokumentiert.
Generell ist es jedoch die ideale Lösung, Ihre Klassen und Methoden ohne Einschränkungen zu entwerfen, z. B. indem Sie Methoden zu bestimmten Zeiten und in einer bestimmten Reihenfolge aufrufen müssen. Möglicherweise ist dies nicht immer möglich, kann jedoch in vielen Fällen durch Verwendung eines geeigneten Musters vermieden werden.
Wenn Sie eine komplexe zeitliche Kopplung haben (eine Reihe von Methoden müssen in einer bestimmten Reihenfolge aufgerufen werden), kann eine Lösung darin bestehen, die Steuerung umzukehren. Sie erstellen also eine Klasse, die die Methoden in der entsprechenden Reihenfolge aufruft, jedoch Vorlagenmethoden oder -ereignisse verwendet Damit der Client benutzerdefinierte Vorgänge in geeigneten Phasen des Ablaufs ausführen kann. Auf diese Weise wird die Verantwortung für die Ausführung von Vorgängen in der richtigen Reihenfolge vom Kunden auf die Klasse selbst übertragen.
Ein einfaches Beispiel: Sie haben ein
File
-Objekt, mit dem Sie aus einer Datei lesen können. Der Client muss jedoch aufgerufen werden,Open
bevor dieReadLine
Methode aufgerufen werden kann, und muss sich daran erinnern, immerClose
(auch wenn eine Ausnahme auftritt) aufzurufen, wonach dieReadLine
Methode nicht mehr aufgerufen werden darf. Dies ist eine zeitliche Kopplung. Es kann vermieden werden, indem eine einzelne Methode verwendet wird, die einen Rückruf oder einen Delegaten als Argument verwendet. Die Methode verwaltet das Öffnen der Datei, das Aufrufen des Rückrufs und das Schließen der Datei. Es kann eine eindeutige Schnittstelle mit einerRead
Methode an den Rückruf übergeben. Auf diese Weise kann der Client nicht vergessen, Methoden in der richtigen Reihenfolge aufzurufen.Schnittstelle mit zeitlicher Kopplung:
Ohne zeitliche Kopplung:
Eine schwergewichtigere Lösung wäre die Definition
File
einer abstrakten Klasse, bei der Sie eine (geschützte) Methode implementieren müssen, die für die geöffnete Datei ausgeführt wird. Dies wird als Vorlagenmethodenmuster bezeichnet.Der Vorteil ist derselbe: Der Client muss nicht daran denken, Methoden in einer bestimmten Reihenfolge aufzurufen. Es ist einfach nicht möglich, Methoden in der falschen Reihenfolge aufzurufen.
quelle
UnitializedFooManager
) keinen Zustand mit den Instanzen (InitializedFooManager
) teilen dürfen , wieInitialize(...)
es tatsächlichInstantiate(...)
semantisch ist. Andernfalls führt dieses Beispiel zu neuen Problemen, wenn dieselbe Vorlage von einem Entwickler zweimal verwendet wird. Und das lässt sich auch nicht statisch verhindern / validieren, wenn die Sprache diemove
Semantik nicht explizit unterstützt , um die Vorlage zuverlässig zu nutzen.Normalerweise prüfe ich nur auf Initialisierung und werfe (sprich) ein,
IllegalStateException
wenn Sie versuchen, es zu verwenden, während es nicht initialisiert ist.Wenn Sie jedoch während der Kompilierung sicher sein möchten (und das ist lobenswert und vorzuziehen), warum behandeln Sie die Initialisierung nicht als eine Factory-Methode, die ein erstelltes und initialisiertes Objekt zurückgibt, z
Auf diese Weise steuern Sie die Erstellung und den Lebenszyklus von Objekten, und Ihre Kunden erhalten das
Component
Ergebnis Ihrer Initialisierung. Es ist effektiv eine faule Instanziierung.quelle
IllegalStateException
.IllegalStateException
oderInvalidOperationException
aus heiterem Himmel wird ein Benutzer nie verstehen, was er falsch gemacht hat. Diese Ausnahmen sollten nicht zur Umgehung des Konstruktionsfehlers werden.Ich werde ein wenig von den anderen Antworten abweichen und nicht zustimmen: Es ist unmöglich, dies zu beantworten, ohne zu wissen, in welcher Sprache Sie arbeiten Ihre Benutzer hängen vollständig von den Mechanismen ab, die Ihre Sprache bereitstellt, und den Konventionen, denen andere Entwickler dieser Sprache tendenziell folgen.
Wenn Sie eine
FooManager
in Haskell haben, wäre es strafbar , Ihren Benutzern zu erlauben, eine zu erstellen, die nicht in der Lage ist,Foo
s zu verwalten, da das Typsystem dies so einfach macht und dies die Konventionen sind, die Haskell-Entwickler erwarten.Auf der anderen Seite, wenn Sie C schreiben, haben Ihre Mitarbeiter das Recht, Sie zurückzunehmen und Sie für die Definition von separaten
struct FooManager
undstruct UninitializedFooManager
Typen zu erschießen , die verschiedene Operationen unterstützen, da dies zu unnötig komplexem Code mit sehr geringem Nutzen führen würde .Wenn Sie Python schreiben, gibt es in der Sprache keinen Mechanismus, mit dem Sie dies tun können.
Sie schreiben wahrscheinlich nicht Haskell, Python oder C, aber sie sind anschauliche Beispiele dafür, wie viel / wie wenig Arbeit das Typsystem erwartet und leisten kann.
Befolgen Sie die vernünftigen Erwartungen eines Entwicklers in Ihrer Sprache und widersetzen Sie sich dem Drang, eine Lösung zu überarbeiten, die keine natürliche, idiomatische Implementierung aufweist (es sei denn, der Fehler ist so leicht zu begehen und so schwer zu fassen, dass es sich lohnt, extrem zu werden) Längen, um es unmöglich zu machen). Wenn Sie nicht genug Erfahrung in Ihrer Sprache haben, um zu beurteilen, was angemessen ist, befolgen Sie den Rat von jemandem, der es besser kennt als Sie.
quelle
Da Sie den Code-Check anscheinend nicht an den Kunden senden möchten (aber für den Programmierer in Ordnung zu sein scheinen), können Sie Assert- Funktionen verwenden, wenn diese in Ihrer Programmiersprache verfügbar sind.
Auf diese Weise haben Sie die Überprüfungen in der Entwicklungsumgebung (und alle Tests, die ein Entwicklerkollege vorhersagen würde, scheitern), aber Sie werden den Code nicht an den Kunden senden, da Asserts (zumindest in Java) nur selektiv kompiliert werden.
Eine Java-Klasse, die dies verwendet, würde folgendermaßen aussehen:
Assertions sind ein großartiges Werkzeug, um den Laufzeitstatus Ihres Programms zu überprüfen, das Sie benötigen, aber erwarten Sie nicht, dass jemand in einer realen Umgebung jemals etwas falsch macht.
Außerdem sind sie ziemlich schlank und nehmen keine großen Teile der if () throw ... -Syntax ein, sie müssen nicht abgefangen werden usw.
quelle
Betrachten Sie dieses Problem allgemeiner als die aktuellen Antworten, die sich größtenteils auf die Initialisierung konzentriert haben. Betrachten Sie ein Objekt mit zwei Methoden
a()
undb()
. Voraussetzung ist, dassa()
immer vorher gerufen wirdb()
. Sie können beim Kompilieren überprüfen, ob dies der Fall ist, indem Sie ein neues Objekt zurückgebena()
undb()
auf das neue Objekt anstatt auf das ursprüngliche verschieben. Beispielimplementierung:Jetzt ist es unmöglich, b () aufzurufen, ohne zuerst a () aufzurufen, da es in einer Schnittstelle implementiert ist, die ausgeblendet ist, bis a () aufgerufen wird. Zugegeben, das ist ein ziemlicher Aufwand, daher würde ich diesen Ansatz normalerweise nicht verwenden, aber es gibt Situationen, in denen er von Vorteil sein kann (insbesondere, wenn Ihre Klasse in vielen Situationen von Programmierern wiederverwendet wird, die dies möglicherweise nicht tun) vertraut mit seiner Implementierung oder in Code, in dem Zuverlässigkeit von entscheidender Bedeutung ist). Beachten Sie auch, dass dies eine Verallgemeinerung des Builder-Musters ist, wie viele der vorhandenen Antworten nahe legen. Es funktioniert so ziemlich genauso, der einzige wirkliche Unterschied besteht darin, wo die Daten gespeichert sind (im ursprünglichen Objekt und nicht im zurückgegebenen) und wann sie verwendet werden sollen (zu jeder Zeit im Vergleich zu nur während der Initialisierung).
quelle
Wenn ich eine Basisklasse implementiere, die zusätzliche Initialisierungsinformationen oder Verknüpfungen zu anderen Objekten benötigt, bevor sie nützlich werden, tendiere ich dazu, diese Basisklasse abstrakt zu machen und dann mehrere abstrakte Methoden in dieser Klasse zu definieren, die im Fluss der Basisklasse verwendet werden (wie
abstract Collection<Information> get_extra_information(args);
undabstract Collection<OtherObjects> get_other_objects(args);
die laut Vererbungsvertrag von der konkreten Klasse implementiert werden muss, wodurch der Benutzer dieser Basisklasse gezwungen wird, alle von der Basisklasse benötigten Dinge bereitzustellen.Wenn ich also die Basisklasse implementiere, weiß ich sofort und explizit, was ich schreiben muss, damit sich die Basisklasse richtig verhält, da ich nur die abstrakten Methoden implementieren muss und das war's.
EDIT: Zur Verdeutlichung entspricht dies fast der Bereitstellung von Argumenten für den Konstruktor der Basisklasse, aber die Implementierungen der abstrakten Methode ermöglichen der Implementierung, die an die Aufrufe der abstrakten Methode übergebenen Argumente zu verarbeiten. Selbst wenn keine Argumente verwendet werden, kann es dennoch nützlich sein, wenn der Rückgabewert der abstrakten Methode von einem Zustand abhängt, den Sie im Methodentext definieren können. Dies ist nicht möglich, wenn die Variable als Argument an übergeben wird der Konstruktor. Zugegeben, Sie können immer noch ein Argument übergeben, dessen dynamisches Verhalten auf demselben Prinzip basiert, wenn Sie die Komposition anstelle der Vererbung verwenden möchten.
quelle
Die Antwort lautet: Ja, nein und manchmal. :-)
Einige der von Ihnen beschriebenen Probleme lassen sich zumindest in vielen Fällen leicht lösen.
Die Prüfung zur Kompilierungszeit ist der Laufzeitprüfung, die überhaupt keiner Prüfung vorzuziehen ist, mit Sicherheit vorzuziehen.
RE-Laufzeit:
Es ist zumindest konzeptionell einfach, das Aufrufen von Funktionen in einer bestimmten Reihenfolge usw. mit Laufzeitprüfungen zu erzwingen. Fügen Sie einfach Flags ein, die angeben, welche Funktion ausgeführt wurde, und lassen Sie jede Funktion mit so etwas wie "Wenn nicht Vorbedingung-1 oder nicht Vorbedingung-2, dann werfen Sie eine Ausnahme" beginnen. Wenn die Prüfungen komplex werden, können Sie sie in eine private Funktion verschieben.
Sie können einige Dinge automatisch geschehen lassen. Um ein einfaches Beispiel zu nennen: Programmierer sprechen oft von "faulen Populationen" eines Objekts. Wenn die Instanz erstellt wird, setzen Sie ein gefülltes Flag auf false oder einen Schlüsselobjektverweis auf null. Wenn dann eine Funktion aufgerufen wird, die die relevanten Daten benötigt, prüft sie das Flag. Wenn es falsch ist, werden die Daten gefüllt und das Flag auf wahr gesetzt. Wenn dies zutrifft, wird einfach davon ausgegangen, dass die Daten vorhanden sind. Mit anderen Voraussetzungen können Sie dasselbe tun. Setzen Sie ein Flag im Konstruktor oder als Standardwert. Wenn Sie dann einen Punkt erreichen, an dem eine Vorbedingungsfunktion hätte aufgerufen werden sollen, rufen Sie sie auf, falls sie noch nicht aufgerufen wurde. Dies funktioniert natürlich nur, wenn Sie die Daten haben, um sie an diesem Punkt aufzurufen, aber in vielen Fällen können Sie alle erforderlichen Daten in die Funktionsparameter der Funktion "
RE-Kompilierungszeit:
Wie bereits erwähnt, wird die Initialisierung im Konstruktor abgelegt, wenn Sie eine Initialisierung durchführen müssen, bevor ein Objekt verwendet werden kann. Dies ist ein ziemlich typischer Rat für OOP. Wenn möglich, sollten Sie den Konstruktor veranlassen, eine gültige, verwendbare Instanz des Objekts zu erstellen.
Ich sehe eine Diskussion darüber, was zu tun ist, wenn Sie Verweise auf das Objekt weitergeben müssen, bevor es initialisiert wird. Ich kann mir kein wirkliches Beispiel dafür vorstellen, aber ich nehme an, es könnte passieren. Es wurden Lösungen vorgeschlagen, aber die Lösungen, die ich hier gesehen habe, und alle, die mir einfallen, sind chaotisch und komplizieren den Code. Irgendwann müssen Sie sich die Frage stellen, ob es sich lohnt, hässlichen, schwer verständlichen Code zu erstellen, damit wir die Kompilierungszeit und nicht die Laufzeit überprüfen können. Oder mache ich mir und anderen viel Arbeit für ein Ziel, das nett, aber nicht erforderlich ist?
Wenn zwei Funktionen immer zusammen ausgeführt werden sollen, besteht die einfache Lösung darin, sie zu einer Funktion zu machen. Wenn Sie beispielsweise jedes Mal, wenn Sie einer Seite eine Anzeige hinzufügen, einen Kennzahlen-Handler hinzufügen, fügen Sie den Code zum Hinzufügen des Kennzahlen-Handlers in die Funktion "Anzeige hinzufügen" ein.
Einige Dinge könnten zur Kompilierungszeit kaum überprüft werden. Wie eine Anforderung, dass eine bestimmte Funktion nicht mehr als einmal aufgerufen werden kann. (Ich habe viele Funktionen wie diese geschrieben. Ich habe kürzlich eine Funktion geschrieben, die nach Dateien in einem magischen Verzeichnis sucht, alle gefundenen verarbeitet und sie dann löscht. Natürlich ist es nicht möglich, die Datei erneut auszuführen, sobald sie gelöscht wurde. Ich kenne keine Sprache mit einer Funktion, mit der Sie verhindern können, dass eine Funktion beim Kompilieren zweimal auf derselben Instanz aufgerufen wird. Ich weiß nicht, wie der Compiler definitiv herausfinden konnte, dass dies geschah. Alles, was Sie tun können, ist die Laufzeitüberprüfung. Diese besondere Laufzeitprüfung ist offensichtlich, nicht wahr? msgstr "wenn schon hier = wahr, dann wirf eine Ausnahme, sonst setze schon hier = wahr".
RE unmöglich durchzusetzen
Es ist nicht ungewöhnlich, dass eine Klasse eine Bereinigung benötigt, wenn Sie fertig sind: Schließen von Dateien oder Verbindungen, Freigeben von Speicher, Schreiben der endgültigen Ergebnisse in die Datenbank usw. Es gibt keine einfache Möglichkeit, dies zu erzwingen oder Laufzeit. Ich denke, dass die meisten OOP-Sprachen eine Art "Finalizer" -Funktion enthalten, die aufgerufen wird, wenn eine Instanz überflüssig wird, aber ich denke, die meisten sagen auch, dass sie nicht garantieren können, dass dies jemals ausgeführt wird. OOP-Sprachen enthalten häufig eine "dispose" -Funktion mit einer Art von Bestimmung, um einen Bereich für die Verwendung einer Instanz festzulegen, eine "using" - oder "with" -Klausel, und wenn das Programm diesen Bereich verlässt, wird die dispose ausgeführt. Dafür muss der Programmierer jedoch den entsprechenden Bereichsspezifizierer verwenden. Es zwingt den Programmierer nicht, es richtig zu machen,
In Fällen, in denen es unmöglich ist, den Programmierer zur korrekten Verwendung der Klasse zu zwingen, versuche ich, sie so zu gestalten, dass das Programm in die Luft sprengt, wenn er es falsch macht, anstatt falsche Ergebnisse zu liefern. Einfaches Beispiel: Ich habe viele Programme gesehen, die eine Reihe von Daten mit Dummy-Werten initialisieren, sodass der Benutzer niemals eine Nullzeiger-Ausnahme erhält, selbst wenn er die Funktionen zum korrekten Auffüllen der Daten nicht aufruft. Und ich frage mich immer warum. Sie tun alles, um das Auffinden von Fehlern zu erschweren. Wenn ein Programmierer die Funktion "Daten laden" nicht aufrief und dann mühsam versuchte, die Daten zu verwenden, erhielt er eine Nullzeiger-Ausnahme, die ihm schnell zeigte, wo das Problem lag. Wenn Sie es in solchen Fällen jedoch ausblenden, indem Sie die Daten leer lassen oder auf Null setzen, wird das Programm möglicherweise vollständig ausgeführt, führt jedoch zu ungenauen Ergebnissen. In einigen Fällen merkt der Programmierer möglicherweise nicht einmal, dass ein Problem vorliegt. Wenn er es bemerkt, muss er möglicherweise einem langen Pfad folgen, um herauszufinden, wo es schief gegangen ist. Lieber früh scheitern als mutig weitermachen.
Im Allgemeinen ist es immer gut, wenn Sie einen falschen Gebrauch eines Objekts einfach unmöglich machen können. Es ist gut, wenn Funktionen so strukturiert sind, dass der Programmierer einfach keine Möglichkeit hat, ungültige Aufrufe zu tätigen, oder wenn ungültige Aufrufe zu Fehlern bei der Kompilierung führen.
Es gibt aber auch einen Punkt, an dem Sie sagen müssen: Der Programmierer sollte die Dokumentation zur Funktion lesen.
quelle
Ja, Ihre Instinkte sind genau richtig. Vor vielen Jahren schrieb ich Artikel in Magazinen (so ähnlich wie hier, aber auf Dead-Tree und einmal im Monat veröffentlicht) über Debugging , Abugging und Antibugging .
Wenn Sie den Compiler dazu bringen können, den Missbrauch zu erkennen, ist dies der beste Weg. Welche Sprachfunktionen verfügbar sind, hängt von der Sprache ab, die Sie nicht angegeben haben. Spezifische Techniken werden auf dieser Site ohnehin für einzelne Fragen detailliert beschrieben.
Ein Test zum Erkennen der Verwendung zur Kompilierungszeit statt zur Laufzeit ist jedoch die gleiche Art von Test: Sie müssen immer noch wissen, wie die richtigen Funktionen zuerst aufgerufen und richtig verwendet werden.
Das Entwerfen der Komponente, um zu vermeiden, dass es überhaupt zu Problemen kommt, ist viel subtiler, und ich finde, dass der Puffer dem Element- Beispiel gleicht . Teil 4 (veröffentlicht vor zwanzig Jahren! Wow!) Enthält ein Beispiel mit einer Diskussion, die Ihrem Fall ziemlich ähnlich ist: Diese Klasse muss sorgfältig verwendet werden, da buffer () möglicherweise nicht aufgerufen wird, nachdem Sie read () verwendet haben. Vielmehr darf es unmittelbar nach dem Bau nur einmal verwendet werden. Wenn die Klasse den Puffer automatisch und für den Aufrufer unsichtbar verwaltet, wird das Problem konzeptionell vermieden.
Sie fragen, ob Tests zur Laufzeit durchgeführt werden können, um die ordnungsgemäße Verwendung sicherzustellen. Sollten Sie dies tun?
Ich würde ja sagen . Dies erspart Ihrem Team eines Tages viel Debugging-Arbeit, wenn der Code beibehalten wird und die spezifische Verwendung gestört wird. Das Testen kann als formale Gruppe von Einschränkungen und Invarianten eingerichtet werden , die getestet werden können. Dies bedeutet nicht, dass bei jedem Anruf zusätzlicher Speicherplatz für die Verfolgung des Status und die Durchführung der Überprüfungen verbleibt. Es ist in der Klasse , so dass es kann genannt werden, und der Code dokumentiert die tatsächlichen Einschränkungen und Annahmen.
Möglicherweise wird die Überprüfung nur in einem Debugbuild durchgeführt.
Vielleicht ist die Prüfung komplex (denken Sie beispielsweise an Heapcheck), aber sie ist vorhanden und kann gelegentlich durchgeführt werden, oder es werden hier und da Aufrufe hinzugefügt, wenn der Code bearbeitet wird und ein Problem auftritt oder spezifische Tests durchgeführt werden müssen Stellen Sie sicher, dass in einem Unit-Test- oder Integrationstestprogramm , das mit derselben Klasse verknüpft ist, keine derartigen Probleme auftreten .
Sie können entscheiden, dass der Overhead schließlich trivial ist. Wenn die Klasse Datei-E / A ausführt, ist ein zusätzliches Statusbyte und ein Test dafür nichts. Gängige Iostream-Klassen prüfen, ob der Stream fehlerhaft ist.
Dein Instinkt ist gut. Mach weiter!
quelle
Das Prinzip des Versteckens von Informationen (Encapsulation) besteht darin, dass Entitäten außerhalb der Klasse nicht mehr wissen sollten, als für die ordnungsgemäße Verwendung dieser Klasse erforderlich ist.
Ihr Fall scheint so, als würden Sie versuchen, ein Objekt einer Klasse zur ordnungsgemäßen Ausführung zu bringen, ohne den externen Benutzern genügend Informationen über die Klasse zu geben. Sie haben mehr Informationen versteckt, als Sie sollten.
Worte der Weisheit:
* In beiden Fällen müssen Sie Klasse und / oder Konstruktor und / oder Methoden neu entwerfen.
* Indem Sie nicht richtig designen und die Klasse von den richtigen Informationen abhalten, riskieren Sie, dass Ihre Klasse nicht an einer, sondern möglicherweise an mehreren Stellen unterbrochen wird.
* Wenn Sie die Ausnahmen und Fehlermeldungen schlecht geschrieben haben, wird Ihre Klasse noch mehr über sich selbst verraten.
* Wenn der Außenseiter schließlich wissen soll, dass er a, b und c initialisieren und dann d (), e (), f (int) aufrufen muss, liegt eine Undichtigkeit in Ihrer Abstraktion vor.
quelle
Da Sie angegeben haben, dass Sie C # verwenden, besteht eine praktikable Lösung für Ihre Situation darin, die Roslyn Code Analyzer zu nutzen . Auf diese Weise können Sie Verstöße sofort abfangen und sogar Codekorrekturen vorschlagen .
Eine Möglichkeit zur Implementierung besteht darin, die zeitlich gekoppelten Methoden mit einem Attribut zu versehen, das die Reihenfolge angibt, in der die Methoden aufgerufen werden müssen 1 . Wenn der Analysator diese Attribute in einer Klasse findet, überprüft er, ob die Methoden in der richtigen Reihenfolge aufgerufen werden. Dadurch würde Ihre Klasse ungefähr so aussehen:
1: Ich habe noch nie einen Roslyn Code Analyzer geschrieben, daher ist dies möglicherweise nicht die beste Implementierung. Die Idee, Roslyn Code Analyzer zu verwenden, um zu überprüfen, ob Ihre API korrekt verwendet wird, ist jedoch 100% einwandfrei.
quelle
Ich habe dieses Problem zuvor mit einem privaten Konstruktor und einer statischen
MakeFooManager()
Methode innerhalb der Klasse gelöst . Beispiel:Da ein Konstruktor implementiert ist, kann niemand eine Instanz von erstellen,
FooManager
ohne dies zu durchlaufenMakeFooManager()
.quelle
Es gibt verschiedene Ansätze:
Bilden Sie die Klasse einfach, richtig zu verwenden und hart, falsch zu verwenden. Einige der anderen Antworten konzentrieren sich ausschließlich auf diesen Punkt, daher erwähne ich einfach RAII und das Builder-Muster .
Achten Sie darauf, wie Sie Kommentare verwenden. Wenn die Klasse selbsterklärend ist, verfassen Sie keine Kommentare. * Auf diese Weise werden Ihre Kommentare mit größerer Wahrscheinlichkeit gelesen, wenn die Klasse sie tatsächlich benötigt. Und wenn Sie einen dieser seltenen Fälle haben, in denen eine Klasse schwer korrekt zu verwenden ist und Kommentare benötigt, geben Sie Beispiele an.
Verwenden Sie Asserts in Ihren Debugbuilds.
Ausnahmen auslösen, wenn die Verwendung der Klasse ungültig ist.
* Dies sind die Arten von Kommentaren, die Sie nicht schreiben sollten:
quelle