Wie wird mit der Sichtbarkeit in Bibliotheken umgegangen?

12

Diese Frage, wann privat und wann geschützt in Klassen verwendet werden soll, brachte mich zum Nachdenken. (Ich werde diese Frage auch auf die letzten Klassen und Methoden ausdehnen, da sie verwandt sind. Ich programmiere in Java, denke aber, dass dies für jede OOP-Sprache relevant ist.)

Die akzeptierte Antwort lautet:

Eine gute Faustregel lautet: Machen Sie alles so privat wie möglich.

Und noch einer:

  1. Machen Sie alle Klassen endgültig, es sei denn, Sie müssen sie sofort unterordnen.
  2. Machen Sie alle Methoden endgültig, es sei denn, Sie müssen sie sofort in Unterklassen unterteilen und überschreiben.
  3. Machen Sie alle Methodenparameter endgültig, es sei denn, Sie müssen sie im Hauptteil der Methode ändern, was in den meisten Fällen ohnehin umständlich ist.

Das ist ziemlich einfach und klar, aber was ist, wenn ich hauptsächlich Bibliotheken (Open Source auf GitHub) anstelle von Anwendungen schreibe?

Ich könnte viele Bibliotheken und Situationen nennen, in denen

  • Eine Bibliothek wurde auf eine Weise erweitert, an die die Entwickler niemals gedacht hätten
  • Dies musste aufgrund von Sichtbarkeitsbeschränkungen mit "Klassenladeprogramm-Magie" und anderen Hacks durchgeführt werden
  • Bibliotheken wurden auf eine Weise verwendet, für die sie nicht gebaut wurden, und die benötigte Funktionalität wurde "gehackt"
  • Bibliotheken konnten aufgrund eines kleinen Problems (Fehler, fehlende Funktionalität, "falsches" Verhalten) nicht verwendet werden, das aufgrund einer eingeschränkten Sichtbarkeit nicht geändert werden konnte
  • Ein Problem, das nicht behoben werden konnte, führte zu riesigen, hässlichen und fehlerhaften Problemumgehungen, bei denen das Überschreiben einer einfachen (privaten oder endgültigen) Funktion hätte helfen können

Und ich fing tatsächlich an, diese zu benennen, bis die Frage zu lang wurde und ich beschloss, sie zu entfernen.

Ich mag die Idee, nicht mehr Code als nötig zu haben, mehr Sichtbarkeit als nötig, mehr Abstraktion als nötig. Dies funktioniert möglicherweise auch beim Schreiben einer Anwendung für den Endbenutzer, bei der der Code nur von denjenigen verwendet wird, die ihn schreiben. Aber wie hält es sich, wenn der Code von anderen Entwicklern verwendet werden soll, bei denen es unwahrscheinlich ist, dass der ursprüngliche Entwickler im Voraus über jeden möglichen Anwendungsfall nachgedacht hat und Änderungen / Umgestaltungen schwierig / unmöglich sind?

Wie wird in solchen Projekten mit objektorientierten Sprachen am häufigsten mit der Sichtbarkeit umgegangen, da große Open-Source-Bibliotheken nichts Neues sind?

Piegames
quelle
Angesichts der Tatsache, dass Sie nach Open Source fragen, ist es noch weniger sinnvoll, die richtigen Codierungsprinzipien zu biegen, um die von Ihnen aufgeführten Probleme zu beheben, als dies bei Closed Source der Fall ist Was auch immer Korrekturen sie wollen
gnat
2
Es geht mir nicht darum, sondern darum, dass Sie auf Open Source verweisen, was in diesem Zusammenhang keinen Sinn macht. Ich kann mir vorstellen, wie pragmatische Bedürfnisse in einigen Fällen eine Abweichung von strengen Grundsätzen rechtfertigen können (auch bekannt als "Accruing Technical Debt" ), aber aus dieser Perspektive ist es egal, ob Code geschlossen oder Open Source ist. Oder genauer gesagt, es geht in die entgegengesetzte Richtung als die, die Sie sich hier vorgestellt haben, weil Code, der Open Source ist, dazu führen kann, dass diese Anforderungen weniger dringend sind als abgeschlossen, da er zusätzliche Optionen bietet, um diese
Probleme
1
@piegames: Ich stimme voll und ganz zu, dass die Probleme, die Sie skizziert haben, sehr viel wahrscheinlicher bei Closed-Source- Bibliotheken auftreten - wenn es sich um eine OS-Bibliothek mit einer zulässigen Lizenz handelt, können die Betreuer eine Änderungsanforderung ignorieren , und die Bibliothek kann gegabelt werden Ändern Sie die Sichtbarkeit bei Bedarf selbst.
Doc Brown
1
@piegames: Ich verstehe deine Frage nicht. "Java" ist eine Sprache, keine Bibliothek. Und wenn "Ihre kleine Open Source-Bibliothek" eine zu strenge Sichtbarkeit aufweist, wird die Abwärtskompatibilität normalerweise nicht durch eine spätere Sichtbarkeitserweiterung beeinträchtigt. Nur umgekehrt.
Doc Brown

Antworten:

15

Die unglückliche Wahrheit ist, dass viele Bibliotheken nicht entworfen , sondern geschrieben werden . Das ist traurig, denn ein bisschen vorheriges Nachdenken kann viele Probleme auf der Straße verhindern.

Wenn wir uns daran machen, eine Bibliothek zu entwerfen, wird es einige erwartete Anwendungsfälle geben. Die Bibliothek erfüllt möglicherweise nicht alle Anwendungsfälle direkt, kann jedoch als Teil einer Lösung dienen. Die Bibliothek muss also flexibel genug sein, um sich anzupassen.

Die Einschränkung ist, dass es normalerweise keine gute Idee ist, den Quellcode der Bibliothek zu übernehmen und ihn zu ändern, um den neuen Anwendungsfall zu behandeln. Für proprietäre Bibliotheken ist die Quelle möglicherweise nicht verfügbar, und für Open-Source-Bibliotheken ist es möglicherweise unerwünscht, eine gespaltene Version beizubehalten. Es ist möglicherweise nicht möglich, hochspezifische Anpassungen in das vorgelagerte Projekt einzubeziehen.

Hier kommt das Open-Closed-Prinzip ins Spiel: Die Bibliothek sollte für Erweiterungen offen sein, ohne den Quellcode zu ändern. Das kommt natürlich nicht. Dies muss ein beabsichtigtes Entwurfsziel sein. Es gibt eine Fülle von Techniken, die hier helfen können. Einige davon sind die klassischen OOP-Designmuster. Im Allgemeinen geben wir Hooks an, mit denen Benutzercode sicher in die Bibliothek eingefügt und Funktionen hinzugefügt werden können.

Es reicht nicht aus, jede Methode zu veröffentlichen oder zuzulassen, dass jede Klasse einer Unterklasse zugeordnet wird, um Erweiterbarkeit zu erzielen. Erstens ist es wirklich schwierig, die Bibliothek zu erweitern, wenn nicht klar ist, wo Benutzer sich in die Bibliothek einklinken können. Das Überschreiben der meisten Methoden ist beispielsweise nicht sicher, da die Basisklassenmethode mit impliziten Annahmen geschrieben wurde. Sie müssen wirklich auf Erweiterbarkeit ausgelegt sein.

Noch wichtiger ist, dass Sie etwas, das Teil der öffentlichen API ist, nicht mehr zurücknehmen können. Sie können es nicht umgestalten, ohne den nachgelagerten Code zu brechen. Vorzeitige Offenheit beschränkt die Bibliothek auf ein suboptimales Design. Im Gegensatz dazu ist es sicherer, interne Inhalte privat zu machen, aber Hooks hinzuzufügen, wenn sie später benötigt werden. Dies ist zwar eine vernünftige Methode, um die langfristige Entwicklung einer Bibliothek in Angriff zu nehmen, für Benutzer, die die Bibliothek verwenden müssen, jedoch unbefriedigend jetzt .

Was passiert also stattdessen? Wenn der aktuelle Zustand der Bibliothek erheblich beeinträchtigt ist, können die Entwickler das gesamte Wissen über die tatsächlichen Anwendungsfälle, die sich im Laufe der Zeit angesammelt haben, nutzen und eine Version 2 der Bibliothek schreiben. Es wird toll sein! Es wird all diese Designfehler beheben! Es wird auch länger dauern als erwartet, in vielen Fällen sprudelt es aus. Und wenn die neue Version der alten Version sehr unähnlich ist, kann es schwierig sein, Benutzer zur Migration zu ermutigen. Sie behalten dann zwei inkompatible Versionen bei.

amon
quelle
Ich muss also Hooks für die Erweiterung hinzufügen, da es nicht ausreicht, sie öffentlich / überschreibbar zu machen. Außerdem muss ich mir aus Gründen der Abwärtskompatibilität überlegen, wann Änderungen / neue APIs veröffentlicht werden sollen. Aber was ist mit der Sichtbarkeit von Methoden im Besonderen?
Piegames
@piegames Mit Sichtbarkeit entscheiden Sie, welche Teile öffentlich sind (Teil Ihrer stabilen API) und welche Teile privat sind (Änderungen vorbehalten). Wenn jemand dies mit Nachdenken umgeht, ist es sein Problem, wenn diese Funktion in der Zukunft unterbrochen wird. Übrigens liegen Erweiterungspunkte häufig in Form einer Methode vor, die überschrieben werden kann. Aber es gibt einen Unterschied zwischen einem Verfahren , das kann außer Kraft gesetzt werden, und ein Verfahren , das wird bestimmt werden außer Kraft gesetzt (siehe auch das Template Method - Muster).
Amon
8

Jede öffentliche und erweiterbare Klasse / Methode ist Teil Ihrer API, die unterstützt werden muss. Begrenzen dieses Satzes auf a vernünftiges Maß Teilmenge der Bibliothek ermöglicht die größte Stabilität und begrenzt die Anzahl der Fehler, die auftreten können. Es ist eine Managemententscheidung (und selbst OSS-Projekte werden bis zu einem gewissen Grad verwaltet), basierend auf dem, was Sie vernünftigerweise unterstützen können.

Der Unterschied zwischen OSS und Closed Source besteht darin, dass die meisten Menschen versuchen, eine Community rund um den Code zu erstellen und zu erweitern, sodass die Bibliothek von mehr als einer Person verwaltet wird. Es stehen jedoch eine Reihe von Verwaltungstools zur Verfügung:

  • Mailinglisten erläutern die Benutzeranforderungen und die Implementierung
  • Problemverfolgungssysteme (JIRA- oder Git-Probleme usw.) verfolgen Fehler und Funktionsanforderungen
  • Die Versionskontrolle verwaltet den Quellcode.

In ausgereiften Projekten sehen Sie Folgendes:

  1. Jemand möchte etwas mit der Bibliothek machen, für die sie ursprünglich nicht entwickelt wurde
  2. Sie fügen der Problemverfolgung ein Ticket hinzu
  3. Das Team kann das Problem in der Mailingliste oder in den Kommentaren diskutieren, und der Anforderer ist immer eingeladen, sich an der Diskussion zu beteiligen
  4. Die API-Änderung wird aus irgendeinem Grund akzeptiert und priorisiert oder abgelehnt

Wenn die Änderung akzeptiert wurde, der Benutzer sie jedoch beschleunigen möchte, um sie zu reparieren, kann er die Arbeit erledigen und entweder eine Pull-Anforderung oder einen Patch senden (abhängig vom Versionskontroll-Tool).

Keine API ist statisch. Das Wachstum muss jedoch in gewisser Weise gestaltet werden. Indem Sie alles geschlossen halten, bis nachweislich Bedarf besteht, Dinge zu öffnen, vermeiden Sie den Ruf einer fehlerhaften oder instabilen Bibliothek.

Berin Loritsch
quelle
1
Stimmen Sie voll und ganz zu, und ich habe den Änderungsanforderungsprozess, den Sie für Closed-Source-Bibliotheken von Drittanbietern sowie für Open-Source-Bibliotheken skizziert haben, erfolgreich praktiziert.
Doc Brown
Nach meiner Erfahrung sind selbst kleine Änderungen in kleinen Bibliotheken eine Menge Arbeit (auch wenn es nur darum geht, die anderen zu überzeugen) und können einige Zeit in Anspruch nehmen (bis zur nächsten Veröffentlichung warten, wenn Sie es sich nicht leisten können, bis dahin einen Snapshot zu verwenden). Das ist also offensichtlich keine Option für mich. Ich würde mich allerdings interessieren: Gibt es größere Bibliotheken (in GitHub), die dieses Konzept wirklich verwenden?
Piegames
Es ist immer viel Arbeit. Fast jedes Projekt, an dem ich mitgewirkt habe, hat einen ähnlichen Prozess. In meinen Apache-Tagen haben wir vielleicht tagelang über etwas diskutiert, weil wir eine Leidenschaft für das hatten, was wir geschaffen haben. Wir wussten, dass eine Menge Leute die Bibliotheken nutzen würden, also mussten wir diskutieren, ob die vorgeschlagene Änderung die API beschädigen würde. Lohnt sich das vorgeschlagene Feature, wann sollten wir es tun? etc.
Berin Loritsch
0

Ich werde meine Antwort umformulieren, da es den Anschein hat, als hätte es bei ein paar Leuten einen Nerv getroffen.

Sichtbarkeit von Klasseneigenschaften / Methoden hat nichts mit Sicherheit oder Offenheit der Quelle zu tun.

Der Grund, warum Sichtbarkeit besteht, ist, weil Objekte für 4 spezifische Probleme anfällig sind:

  1. Parallelität

Wenn Sie Ihr Modul ungekapselt erstellen, werden sich Ihre Benutzer daran gewöhnen, den Modulstatus direkt zu ändern. Dies funktioniert problemlos in einer Umgebung mit nur einem Thread, aber wenn Sie erst einmal darüber nachgedacht haben, Threads hinzuzufügen, können Sie dies auch tun. Sie werden gezwungen sein, den Staat privat zu machen und Sperren / Monitore zusammen mit Gettern und Setzern zu verwenden, die andere Threads auf die Ressourcen warten lassen, anstatt auf ihnen zu rasen. Dies bedeutet, dass Ihre Benutzerprogramme nicht mehr funktionieren, da auf private Variablen nicht auf herkömmliche Weise zugegriffen werden kann. Dies kann bedeuten, dass Sie viele Änderungen vornehmen müssen.

Die Wahrheit ist, dass es viel einfacher ist, mit Blick auf eine einzelne Thread-Laufzeit zu codieren, und mit privatem Schlüsselwort können Sie einfach das synchronisierte Schlüsselwort oder einige Sperren hinzufügen, und der Code Ihrer Benutzer wird nicht beschädigt, wenn Sie ihn von Anfang an gekapselt haben .

  1. Verhindern Sie, dass Benutzer in der Fuß- / Stromlinienbenutzung der Benutzeroberfläche selbst schießen. Im Wesentlichen hilft es Ihnen, die Invarianten des Objekts zu steuern.

Jedes Objekt hat eine Reihe von Dingen, die wahr sein müssen, um in einem konsistenten Zustand zu sein. Leider leben diese Dinge im sichtbaren Bereich des Clients, da es teuer ist, jedes Objekt in seinen eigenen Prozess zu verschieben und über Nachrichten mit ihm zu sprechen. Dies bedeutet, dass es für ein Objekt sehr einfach ist, das gesamte Programm zum Absturz zu bringen, wenn der Benutzer die volle Sichtbarkeit hat.

Dies ist unvermeidlich. Sie können jedoch verhindern, dass ein Objekt versehentlich in einen inkonsistenten Zustand versetzt wird, indem Sie die Dienste einer Schnittstelle schließen, um versehentliche Abstürze zu vermeiden, indem Sie dem Benutzer nur die Interaktion mit dem Objektzustand über eine sorgfältig gestaltete Schnittstelle ermöglichen, die das Programm wesentlich robuster macht . Dies bedeutet nicht, dass der Benutzer die Invarianten nicht absichtlich beschädigen kann. Wenn dies jedoch der Fall ist, stürzt der Client ab. Sie müssen lediglich das Programm neu starten (die zu schützenden Daten sollten nicht clientseitig gespeichert werden ).

Ein weiteres gutes Beispiel, mit dem Sie die Benutzerfreundlichkeit Ihrer Module verbessern können, besteht darin, den Konstruktor privat zu machen. Denn wenn der Konstruktor eine Ausnahme auslöst, bricht er das Programm ab. Ein fauler Ansatz, dies zu lösen, besteht darin, den Konstruktor zu veranlassen, einen Fehler bei der Kompilierung auszulösen, den Sie nur dann erstellen können, wenn er sich in einem try / catch-Block befindet. Indem Sie den Konstruktor privat machen und eine öffentliche statische create-Methode hinzufügen, können Sie die create-Methode null zurückgeben lassen, wenn sie ihn nicht erstellt, oder eine Rückruffunktion verwenden, um den Fehler zu behandeln, wodurch das Programm benutzerfreundlicher wird.

  1. Umweltverschmutzung

Viele Klassen haben viele Zustände und Methoden und es ist leicht, überwältigt zu werden, wenn man versucht, durch sie zu scrollen. Viele dieser Methoden sind nur visuelles Rauschen wie Hilfsfunktionen, Zustand. Indem Variablen und Methoden privatisiert werden, wird die Belastung des Anwendungsbereichs verringert und es wird dem Benutzer erleichtert, die gewünschten Dienste zu finden.

Im Wesentlichen können Sie damit davonkommen, Hilfsfunktionen nicht außerhalb der Klasse, sondern innerhalb der Klasse zu haben. ohne Sichtbarkeitskontrolle, ohne den Benutzer durch eine Reihe von Diensten abzulenken, die der Benutzer niemals nutzen sollte, sodass Sie davonkommen können, Methoden in eine Reihe von Hilfsmethoden aufzuteilen (obwohl dies immer noch Ihren Bereich verschmutzt, aber nicht den des Benutzers).

  1. an Abhängigkeiten gebunden sein

Eine gut gestaltete Benutzeroberfläche kann interne Datenbanken / Fenster / Imaging verbergen, von denen sie abhängig ist, und wenn Sie zu einer anderen Datenbank / einem anderen Fenstersystem / einer anderen Imaging-Bibliothek wechseln möchten, können Sie die Benutzeroberfläche und die Benutzer gleich lassen werde es nicht bemerken.

Wenn Sie dies nicht tun, kann es jedoch leicht vorkommen, dass Ihre Abhängigkeiten nicht mehr geändert werden können, da sie offengelegt werden und der Code darauf basiert. Bei einem ausreichend großen System können die Migrationskosten unerschwinglich werden, während eine Kapselung gut verhaltene Clientbenutzer vor zukünftigen Entscheidungen zum Auslagern von Abhängigkeiten schützen kann.

Dmitry
quelle
1
"Es macht keinen Sinn, etwas zu verstecken" - warum sollte man dann überhaupt an die Verkapselung denken? Reflexion erfordert in vielen Zusammenhängen besondere Privilegien.
Frank Hileman
Sie denken an die Verkapselung, weil Sie dadurch bei der Entwicklung Ihres Moduls mehr Platz zum Atmen haben und die Wahrscheinlichkeit eines Missbrauchs verringern. Wenn Sie beispielsweise 4 Threads haben, die den internen Status einer Klasse direkt ändern, kann dies leicht zu Problemen führen. Wenn Sie die Variable jedoch privat machen, wird der Benutzer dazu angeregt, die öffentlichen Methoden zu verwenden, um den Weltstatus zu manipulieren. Diese können mithilfe von Monitoren / Sperren Probleme verhindern . Dies ist der einzige wirkliche Vorteil der Verkapselung.
Dmitry
Das Ausblenden von Dingen aus Sicherheitsgründen ist ein einfacher Weg, um ein Design zu erstellen, bei dem Sie am Ende Löcher in Ihrer API haben müssen. Ein gutes Beispiel hierfür sind Anwendungen mit mehreren Dokumenten, in denen Sie viele Toolboxes und viele Fenster mit Unterfenstern haben. Wenn Sie die Verkapselung verrückt machen, werden Sie eine Situation haben, in der Sie etwas auf ein Dokument zeichnen müssen. Sie müssen das Fenster auffordern, das innere Dokument aufzufordern, das innere Dokument aufzufordern, eine Aufforderung zum Zeichnen von etwas zu stellen und seinen Kontext ungültig machen. Wenn die Client-Seite mit der Client-Seite spielen möchte, können Sie sie nicht verhindern.
Dmitry
OK, das ist sinnvoller, obwohl Sicherheit über die Zugriffskontrolle erreicht werden kann, wenn die Umgebung dies unterstützt, und dies war eines der ursprünglichen Ziele beim OO-Sprachdesign. Außerdem fördern Sie die Verkapselung und sagen, dass Sie sie nicht gleichzeitig verwenden sollten. ein bisschen verwirrend.
Frank Hileman
Ich wollte es nie nicht benutzen; Ich wollte es nicht aus Sicherheitsgründen benutzen; Verwenden Sie es strategisch, um die Benutzererfahrung zu verbessern und sich eine reibungslosere Entwicklungsumgebung zu verschaffen. Mein Punkt ist, dass es nichts mit Sicherheit oder Offenheit der Quelle zu tun hat. Clientseitige Objekte sind per definitionem anfällig für Introspektion, und wenn sie aus dem Benutzerprozessbereich verschoben werden, ist der Zugriff auf nicht gekapselte Objekte ebenso unmöglich wie auf gekapselte.
Dmitry