Eines der wichtigsten Prinzipien der objektorientierten Programmierung – die Abgrenzung der internen Schnittstelle von der externen.
Das ist „ein Muss“ bei der Entwicklung von etwas Komplexerem als einer „Hallo Welt“-App.
Um dies zu verstehen, lösen wir uns von der Entwicklung und richten unseren Blick auf die reale Welt.
Normalerweise sind die von uns verwendeten Geräte recht komplex. Aber die Abgrenzung der internen Schnittstelle von der externen ermöglicht eine problemlose Nutzung.
Zum Beispiel eine Kaffeemaschine. Von außen betrachtet einfach: ein Knopf, ein Display, ein paar Löcher … Und das Ergebnis ist ganz sicher: toller Kaffee! :) :)
Aber innen... (ein Bild aus dem Reparaturhandbuch)
Viele Details. Aber wir können es nutzen, ohne etwas zu wissen.
Kaffeemaschinen sind ziemlich zuverlässig, nicht wahr? Wir können eines jahrelang verwenden und nur, wenn etwas schief geht, bringen Sie es zur Reparatur.
Das Geheimnis der Zuverlässigkeit und Einfachheit einer Kaffeemaschine – alle Details sind gut aufeinander abgestimmt und im Inneren verborgen .
Wenn wir die Schutzhülle von der Kaffeemaschine entfernen, wird die Bedienung viel komplizierter (wo drücken?) und gefährlicher (es kann zu einem Stromschlag kommen).
Wie wir sehen werden, sind Objekte in der Programmierung wie Kaffeemaschinen.
Um jedoch innere Details zu verbergen, verwenden wir keine Schutzhülle, sondern eine spezielle Syntax der Sprache und Konventionen.
Bei der objektorientierten Programmierung werden Eigenschaften und Methoden in zwei Gruppen unterteilt:
Interne Schnittstelle – Methoden und Eigenschaften, auf die von anderen Methoden der Klasse aus zugegriffen werden kann, jedoch nicht von außen.
Externe Schnittstelle – Methoden und Eigenschaften, auf die auch von außerhalb der Klasse zugegriffen werden kann.
Wenn wir die Analogie zur Kaffeemaschine fortsetzen, ist das, was sich im Inneren verbirgt: ein Boilerrohr, ein Heizelement usw., ihre interne Schnittstelle.
Damit das Objekt funktioniert, wird eine interne Schnittstelle verwendet, seine Details nutzen sich gegenseitig. Am Heizelement wird beispielsweise ein Kesselrohr befestigt.
Von außen ist eine Kaffeemaschine jedoch durch die Schutzhülle verschlossen, so dass niemand an diese herankommen kann. Details sind verborgen und unzugänglich. Wir können seine Funktionen über die externe Schnittstelle nutzen.
Um ein Objekt nutzen zu können, müssen wir also lediglich seine externe Schnittstelle kennen. Wir sind uns möglicherweise überhaupt nicht bewusst, wie es im Inneren funktioniert, und das ist großartig.
Das war eine allgemeine Einführung.
In JavaScript gibt es zwei Arten von Objektfeldern (Eigenschaften und Methoden):
Öffentlich: von überall aus zugänglich. Sie bilden die externe Schnittstelle. Bisher haben wir nur öffentliche Eigenschaften und Methoden verwendet.
Privat: Nur aus der Klasse heraus zugänglich. Diese sind für die interne Schnittstelle.
In vielen anderen Sprachen gibt es auch „geschützte“ Felder: Zugriff nur innerhalb der Klasse und solchen, die sie erweitern (wie private, aber plus Zugriff von erbenden Klassen). Sie sind auch für die interne Schnittstelle nützlich. Sie sind gewissermaßen weiter verbreitet als private, da wir in der Regel wollen, dass erbende Klassen Zugriff auf sie erhalten.
Geschützte Felder sind in JavaScript auf Sprachebene nicht implementiert, in der Praxis sind sie jedoch sehr praktisch und werden daher emuliert.
Jetzt erstellen wir in JavaScript eine Kaffeemaschine mit all diesen Eigenschaften. Eine Kaffeemaschine hat viele Details, wir werden sie nicht modellieren, um einfach zu bleiben (obwohl wir es könnten).
Machen wir zunächst einen einfachen Kaffeemaschinenkurs:
Klasse CoffeeMachine { Wassermenge = 0; // die Menge an Wasser im Inneren Konstruktor(Leistung) { this.power = Leistung; Alert( `Eine Kaffeemaschine erstellt, Leistung: ${power}` ); } } // Erstelle die Kaffeemaschine let CoffeeMachine = new CoffeeMachine(100); // Wasser hinzufügen CoffeeMachine.waterAmount = 200;
Derzeit sind die Eigenschaften waterAmount
und power
öffentlich. Wir können sie leicht von außen auf jeden beliebigen Wert bringen/einstellen.
Ändern wir die Eigenschaft waterAmount
in „protected“, um mehr Kontrolle darüber zu haben. Wir wollen zum Beispiel nicht, dass jemand den Wert unter Null setzt.
Geschützten Eigenschaften wird normalerweise ein Unterstrich _
vorangestellt.
Dies wird auf Sprachebene nicht durchgesetzt, aber es gibt eine bekannte Konvention unter Programmierern, dass auf solche Eigenschaften und Methoden nicht von außen zugegriffen werden sollte.
Unsere Eigenschaft heißt also _waterAmount
:
Klasse CoffeeMachine { _waterAmount = 0; setze waterAmount(value) { if (Wert < 0) { Wert = 0; } this._waterAmount = value; } get waterAmount() { return this._waterAmount; } Konstruktor(Leistung) { this._power = Leistung; } } // Erstelle die Kaffeemaschine let CoffeeMachine = new CoffeeMachine(100); // Wasser hinzufügen CoffeeMachine.waterAmount = -10; // _waterAmount wird 0, nicht -10
Jetzt ist der Zugang unter Kontrolle, sodass eine Einstellung der Wassermenge unter Null nicht mehr möglich ist.
Machen wir die power
Eigenschaft schreibgeschützt. Es kommt manchmal vor, dass eine Eigenschaft nur zum Zeitpunkt der Erstellung festgelegt und dann nie geändert werden darf.
Genau das ist bei einer Kaffeemaschine der Fall: Die Leistung ändert sich nie.
Dazu müssen wir nur den Getter erstellen, nicht jedoch den Setter:
Klasse CoffeeMachine { // ... Konstruktor(Leistung) { this._power = Leistung; } Holen Sie sich power() { return this._power; } } // Erstelle die Kaffeemaschine let CoffeeMachine = new CoffeeMachine(100); Alert(`Leistung ist: ${coffeeMachine.power}W`); // Leistung beträgt: 100W CoffeeMachine.power = 25; // Fehler (kein Setter)
Getter/Setter-Funktionen
Hier haben wir die Getter/Setter-Syntax verwendet.
Aber meistens get.../set...
Funktionen bevorzugt, wie diese:
Klasse CoffeeMachine { _waterAmount = 0; setWaterAmount(value) { wenn (Wert < 0) Wert = 0; this._waterAmount = value; } getWaterAmount() { return this._waterAmount; } } new CoffeeMachine().setWaterAmount(100);
Das sieht etwas länger aus, aber die Funktionen sind flexibler. Sie können mehrere Argumente akzeptieren (auch wenn wir sie gerade nicht brauchen).
Andererseits ist die get/set-Syntax kürzer, sodass es letztendlich keine strenge Regel gibt, sondern die Entscheidung liegt bei Ihnen.
Geschützte Felder werden vererbt
Wenn wir class MegaMachine extends CoffeeMachine
, hindert uns nichts daran, über die Methoden der neuen Klasse auf this._waterAmount
oder this._power
zuzugreifen.
Geschützte Felder sind also von Natur aus vererbbar. Im Gegensatz zu privaten, die wir weiter unten sehen werden.
Eine neue Ergänzung
Dies ist eine neue Ergänzung der Sprache. In JavaScript-Engines nicht oder teilweise noch nicht unterstützt, erfordert Polyfilling.
Es gibt einen fertigen JavaScript-Vorschlag, der fast im Standard enthalten ist und Unterstützung für private Eigenschaften und Methoden auf Sprachebene bietet.
Privates sollten mit #
beginnen. Sie sind nur aus der Klasse heraus zugänglich.
Hier ist zum Beispiel eine private #waterLimit
Eigenschaft und die private Methode zur Wasserprüfung #fixWaterAmount
:
Klasse CoffeeMachine { #waterLimit = 200; #fixWaterAmount(value) { wenn (Wert < 0) 0 zurückgeben; if (value > this.#waterLimit) return this.#waterLimit; } setWaterAmount(value) { this.#waterLimit = this.#fixWaterAmount(value); } } let CoffeeMachine = new CoffeeMachine(); // kann von außerhalb der Klasse nicht auf Privates zugreifen CoffeeMachine.#fixWaterAmount(123); // Fehler CoffeeMachine.#waterLimit = 1000; // Fehler
Auf Sprachebene ist #
ein besonderes Zeichen dafür, dass das Feld privat ist. Wir können nicht von außen oder von erbenden Klassen darauf zugreifen.
Private Felder stehen nicht im Konflikt mit öffentlichen Feldern. Wir können sowohl private #waterAmount
als auch öffentliche waterAmount
Felder gleichzeitig haben.
Machen wir zum Beispiel waterAmount
zu einem Accessor für #waterAmount
:
Klasse CoffeeMachine { #waterAmount = 0; get waterAmount() { gib dies zurück.#waterAmount; } setze waterAmount(value) { wenn (Wert < 0) Wert = 0; this.#waterAmount = value; } } let machine = new CoffeeMachine(); machine.waterAmount = 100; alarm(machine.#waterAmount); // Fehler
Im Gegensatz zu geschützten Feldern werden private Felder von der Sprache selbst erzwungen. Das ist eine gute Sache.
Aber wenn wir von CoffeeMachine
erben, haben wir keinen direkten Zugriff auf #waterAmount
. Wir müssen uns auf waterAmount
Getter/Setter verlassen:
Klasse MegaCoffeeMachine erweitert CoffeeMachine { method() { alarm( this.#waterAmount ); // Fehler: Zugriff nur über CoffeeMachine möglich } }
In vielen Fällen ist eine solche Einschränkung zu stark. Wenn wir eine CoffeeMachine
erweitern, haben wir möglicherweise legitime Gründe, auf ihre Interna zuzugreifen. Aus diesem Grund werden geschützte Felder häufiger verwendet, obwohl sie von der Sprachsyntax nicht unterstützt werden.
Private Felder sind nicht verfügbar, da dies[Name]
Private Felder sind etwas Besonderes.
Wie wir wissen, können wir normalerweise mit this[name]
auf Felder zugreifen:
Klasse Benutzer { ... sayHi() { let fieldName = "name"; Alert(`Hallo, ${this[fieldName]}`); } }
Bei privaten Feldern ist das unmöglich: this['#name']
funktioniert nicht. Dies ist eine Syntaxbeschränkung, um den Datenschutz zu gewährleisten.
In Bezug auf OOP wird die Abgrenzung der internen Schnittstelle von der externen als Kapselung bezeichnet.
Es bietet folgende Vorteile:
Schutz für den Nutzer, damit er sich nicht selbst ins Bein schießt
Stellen Sie sich vor, ein Entwicklerteam bedient eine Kaffeemaschine. Es wurde von der Firma „Best CoffeeMachine“ hergestellt und funktioniert einwandfrei, allerdings wurde eine Schutzhülle entfernt. Die interne Schnittstelle wird also freigelegt.
Alle Entwickler sind zivilisiert – sie nutzen die Kaffeemaschine bestimmungsgemäß. Aber einer von ihnen, John, entschied, dass er der Klügste ist, und nahm einige Änderungen am Inneren der Kaffeemaschine vor. Also fiel die Kaffeemaschine zwei Tage später aus.
Das ist sicherlich nicht Johns Schuld, sondern die Person, die die Schutzhülle entfernt hat und John seine Manipulationen vornehmen ließ.
Das Gleiche gilt für die Programmierung. Wenn ein Benutzer einer Klasse Dinge ändert, die von außen nicht geändert werden sollen, sind die Folgen unvorhersehbar.
Unterstützbar
Die Situation beim Programmieren ist komplexer als bei einer echten Kaffeemaschine, da wir sie nicht nur einmal kaufen. Der Code wird ständig weiterentwickelt und verbessert.
Wenn wir die interne Schnittstelle strikt abgrenzen, kann der Entwickler der Klasse ihre internen Eigenschaften und Methoden frei ändern, auch ohne die Benutzer darüber zu informieren.
Wenn Sie Entwickler einer solchen Klasse sind, ist es gut zu wissen, dass private Methoden sicher umbenannt, ihre Parameter geändert und sogar entfernt werden können, da kein externer Code von ihnen abhängt.
Wenn eine neue Version herauskommt, kann es für Benutzer zu einer internen Komplettüberarbeitung kommen, die jedoch immer noch einfach zu aktualisieren ist, wenn die externe Schnittstelle dieselbe ist.
Komplexität verbergen
Menschen lieben es, einfache Dinge zu verwenden. Zumindest von außen. Was drin ist, ist etwas anderes.
Programmierer sind keine Ausnahme.
Es ist immer praktisch, wenn Implementierungsdetails ausgeblendet sind und eine einfache, gut dokumentierte externe Schnittstelle verfügbar ist.
Um eine interne Schnittstelle zu verbergen, verwenden wir entweder geschützte oder private Eigenschaften:
Geschützte Felder beginnen mit _
. Das ist eine bekannte Konvention, die auf Sprachebene nicht durchgesetzt wird. Programmierer sollten nur auf ein Feld zugreifen, das mit _
beginnt, von seiner Klasse und von Klassen, die davon erben.
Private Felder beginnen mit #
. JavaScript stellt sicher, dass wir nur innerhalb der Klasse auf diese zugreifen können.
Derzeit werden private Felder von Browsern nicht gut unterstützt, können aber mehrfach ausgefüllt werden.