Kapselung ist kein Verstecken von Informationen

Worte sind rutschig. Wie Humpty Dumpty in Lewis Carrolls Through the Looking Glass verkündete : "Wenn ich ein Wort benutze, bedeutet es genau das, was ich wähle - weder mehr noch weniger." Sicherlich scheint die übliche Verwendung der Wörter Kapselung und Verstecken von Informationen dieser Logik zu folgen. Autoren unterscheiden selten zwischen den beiden und behaupten oft direkt, dass sie gleich sind.

Macht es das so? Nicht für mich. Wäre es nur eine Frage der Worte, würde ich kein weiteres Wort dazu schreiben. Hinter diesen Begriffen stehen jedoch zwei unterschiedliche Konzepte: Konzepte, die separat erstellt und am besten separat verstanden werden.

Kapselung bezieht sich auf die Bündelung von Daten mit den Methoden, die mit diesen Daten arbeiten. Oft wird diese Definition falsch interpretiert, um zu bedeuten, dass die Daten irgendwie verborgen sind. In Java können Sie gekapselte Daten haben, die überhaupt nicht verborgen sind.

Das Ausblenden von Daten ist jedoch nicht das vollständige Ausblenden von Informationen. David Parnas führte erstmals das Konzept des Versteckens von Informationen um 1972 ein. Er argumentierte, dass die Hauptkriterien für die Systemmodularisierung das Verbergen kritischer Entwurfsentscheidungen sein sollten. Er betonte, "schwierige Entwurfsentscheidungen oder Entwurfsentscheidungen, die sich wahrscheinlich ändern werden", zu verbergen. Durch das Ausblenden von Informationen auf diese Weise werden Kunden davon abgehalten, genaue Kenntnisse über das Design zur Verwendung eines Moduls zu benötigen, und von den Auswirkungen einer Änderung dieser Entscheidungen.

In diesem Artikel untersuche ich den Unterschied zwischen Kapselung und Verstecken von Informationen durch die Entwicklung von Beispielcode. Die Diskussion zeigt, wie Java die Kapselung erleichtert und die negativen Auswirkungen der Kapselung untersucht, ohne dass Daten ausgeblendet werden. Die Beispiele zeigen auch, wie das Klassendesign durch das Prinzip des Versteckens von Informationen verbessert werden kann.

Positionsklasse

Angesichts des wachsenden Bewusstseins für das enorme Potenzial des drahtlosen Internets erwarten viele Experten, dass standortbezogene Dienste die Möglichkeit für die erste drahtlose Killer-App bieten. Für den Beispielcode dieses Artikels habe ich eine Klasse ausgewählt, die die geografische Position eines Punktes auf der Erdoberfläche darstellt. Als Domänenentität Positionrepräsentiert die benannte Klasse GPS-Informationen (Global Position System). Ein erster Schnitt in der Klasse sieht so einfach aus wie:

öffentliche Klasse Position {öffentlicher doppelter Breitengrad; öffentliche doppelte Länge; }}

Die Klasse enthält zwei Datenelemente: GPS latitudeund longitude. Derzeit Positionist nichts weiter als eine kleine Tasche von Daten. Trotzdem Positionhandelt es sich um eine Klasse, und PositionObjekte können mithilfe der Klasse instanziiert werden. Um diese Objekte zu verwenden, PositionUtilityenthält die Klasse Methoden zum Berechnen der Entfernung und des Kurses, dh der Richtung, zwischen angegebenen PositionObjekten:

public class PositionUtility {öffentlicher statischer doppelter Abstand (Position position1, Position position2) {// Berechne den Abstand zwischen den angegebenen Positionen und gib ihn zurück. } öffentliche statische Doppelüberschrift (Position Position1, Position Position2) {// Berechne die Überschrift und gib sie von Position1 zu Position2 zurück. }}

Ich lasse den eigentlichen Implementierungscode für die Entfernungs- und Kursberechnungen weg.

Der folgende Code stellt eine typische Verwendung von Positionund dar PositionUtility:

// Erstelle eine Position, die mein Haus darstellt Position myHouse = new Position (); myHouse.latitude = 36.538611; myHouse.longitude = -121.797500; // Erstellen Sie eine Position, die ein lokales Café darstellt. Position CoffeeShop = new Position (); CoffeeShop.latitude = 36.539722; CoffeeShop.Longitude = -121.907222; // Verwenden Sie eine PositionUtility, um die Entfernung und den Weg von meinem Haus zum örtlichen Café zu berechnen. doppelte Entfernung = PositionUtility.distance (myHouse, CoffeeShop); doppelte Überschrift = PositionUtility.heading (myHouse, CoffeeShop); // Ergebnisse drucken System.out.println ("Von meinem Haus unter (" + myHouse.latitude + "," + myHouse.longitude + ") zum Café unter (" + CoffeeShop.latitude + "," + CoffeeShop ". Längengrad + ") ist eine Entfernung von" + Entfernung + "bei einer Überschrift von" + Überschrift + "Grad. ");

Der Code generiert die folgende Ausgabe, die angibt, dass sich das Café in einer Entfernung von 6,09 genau westlich (270,8 Grad) von meinem Haus befindet. Die spätere Diskussion befasst sich mit dem Mangel an Entfernungseinheiten.

================================================== ================= von meinem Hause an (36.538611, -121.7975) zu dem Coffee-Shop an (36.539722, -121,907222) ist Abstand von 6,0873776351893385 bei einem Kurs von 270,7547022304523 Grad. ================================================== =================

Position, PositionUtilityUnd deren Code Nutzung sind ein wenig beunruhigende und schon gar nicht sehr objektorientiert. Aber wie kann das sein? Java ist eine objektorientierte Sprache und der Code verwendet Objekte!

Obwohl der Code Java-Objekte verwenden kann, erinnert er an eine vergangene Ära: Dienstprogrammfunktionen, die mit Datenstrukturen arbeiten. Willkommen in 1972! Während sich Präsident Nixon über geheime Tonbandaufnahmen drängte, nutzten Computerfachleute, die in der Verfahrenssprache Fortran codierten, aufgeregt die neue Internationale Bibliothek für Mathematik und Statistik (IMSL) auf genau diese Weise. Code-Repositorys wie IMSL waren voll mit Funktionen für numerische Berechnungen. Benutzer übergaben Daten an diese Funktionen in langen Parameterlisten, die zeitweise nicht nur die Eingabe-, sondern auch die Ausgabedatenstrukturen enthielten. (IMSL hat sich im Laufe der Jahre weiterentwickelt und Java-Entwicklern steht jetzt eine Version zur Verfügung.)

Im aktuellen Design Positionhandelt es sich um eine einfache Datenstruktur und PositionUtilityum ein IMSL-ähnliches Repository für Bibliotheksfunktionen, die mit PositionDaten arbeiten. Wie das obige Beispiel zeigt, schließen moderne objektorientierte Sprachen die Verwendung antiquierter prozeduraler Techniken nicht unbedingt aus.

Daten und Methoden bündeln

Der Code kann leicht verbessert werden. Warum sollten Sie Daten und die Funktionen, die diese Daten verarbeiten, zunächst in separaten Modulen ablegen? Java-Klassen ermöglichen das Bündeln von Daten und Methoden:

public class Position {public double distance (Positionsposition) {// Berechne die Entfernung von diesem Objekt und gib sie an die angegebene // Position zurück. } public double heading (Position position) {// Berechne die Überschrift von diesem Objekt und bringe sie an die angegebene // Position zurück. } öffentlicher doppelter Breitengrad; öffentliche doppelte Länge; }}

Durch das Einfügen der Positionsdatenelemente und des Implementierungscodes zur Berechnung der Entfernung und des Kurses in dieselbe Klasse wird die Notwendigkeit einer separaten PositionUtilityKlasse vermieden . Beginnt nun Position, einer echten objektorientierten Klasse zu ähneln. Der folgende Code verwendet diese neue Version, die die Daten und Methoden zusammenfasst:

Position myHouse = neue Position (); myHouse.latitude = 36.538611; myHouse.longitude = -121.797500; Position CoffeeShop = neue Position (); CoffeeShop.latitude = 36.539722; CoffeeShop.Longitude = -121.907222; doppelte Entfernung = myHouse.distance (CoffeeShop); doppelte Überschrift = myHouse.heading (CoffeeShop); System.out.println ("Von meinem Haus unter (" + myHouse.latitude + "," + myHouse.longitude + ") zum Café unter (" + CoffeeShop.latitude + "," + CoffeeShop.longitude + ") ist eine Entfernung von "+ Entfernung +" bei einer Überschrift von "+ Überschrift +" Grad. ");

Die Ausgabe ist identisch wie zuvor, und was noch wichtiger ist, der obige Code scheint natürlicher zu sein. In der vorherigen Version wurden zwei PositionObjekte an eine Funktion in einer separaten Dienstprogrammklasse übergeben, um Entfernung und Kurs zu berechnen. In diesem Code gab die Berechnung der Überschrift mit dem Methodenaufruf util.heading( myHouse, coffeeShop )nicht eindeutig die Richtung der Berechnung an. Ein Entwickler muss sich daran erinnern, dass die Utility-Funktion die Überschrift vom ersten zum zweiten Parameter berechnet.

Im Vergleich dazu verwendet der obige Code die Anweisung myHouse.heading(coffeeShop), um dieselbe Überschrift zu berechnen. Die Semantik des Anrufs zeigt deutlich, dass die Richtung von meinem Haus zum Café verläuft. Das Konvertieren der Funktion heading(Position, Position)mit zwei Argumenten in eine Funktion mit einem Argument position.heading(Position)wird als Currying der Funktion bezeichnet. Currying spezialisiert die Funktion effektiv auf das erste Argument, was zu einer klareren Semantik führt.

Das Platzieren der Methoden unter Verwendung von PositionKlassendaten in der PositionKlasse selbst macht das Currying der Funktionen distanceund headingmöglich. Das Ändern der Aufrufstruktur der Funktionen auf diese Weise ist ein wesentlicher Vorteil gegenüber prozeduralen Sprachen. Die Klasse Positionstellt jetzt einen abstrakten Datentyp dar, der Daten und die Algorithmen, die mit diesen Daten arbeiten, kapselt. Als benutzerdefinierter Typ sind PositionObjekte auch erstklassige Bürger, die alle Vorteile des Java-Sprachtypsystems nutzen.

Die Sprachfunktion, die Daten mit den Operationen bündelt, die für diese Daten ausgeführt werden, ist die Kapselung. Beachten Sie, dass die Kapselung weder Datenschutz noch das Verstecken von Informationen garantiert. Die Kapselung gewährleistet auch kein zusammenhängendes Klassendesign. Um diese Qualitätsdesignattribute zu erreichen, sind Techniken erforderlich, die über die von der Sprache bereitgestellte Kapselung hinausgehen. Wie derzeit implementiert, enthält die Klasse Positionkeine überflüssigen oder nicht verwandten Daten und Methoden, sondern Positionmacht beide latitudeund longitudein Rohform verfügbar. Auf diese Weise kann jeder Client der Klasse Positionjedes interne Datenelement direkt ändern, ohne dass ein Eingriff von erforderlich ist Position. Die Einkapselung reicht natürlich nicht aus.

Defensive Programmierung

Angenommen, ich beschließe, ein wenig defensive Programmierung hinzuzufügen, um die Auswirkungen der Offenlegung interner Datenelemente weiter zu untersuchen, indem ich Positionden Breiten- und Längengrad auf die vom GPS festgelegten Bereiche beschränke. Der Breitengrad fällt in den Bereich [-90, 90] und der Längengrad in den Bereich (-180, 180]. Die Belichtung der Datenelemente latitudeund longitudein Positionder aktuellen Implementierung macht diese defensive Programmierung unmöglich.

Making attributes latitude and longitude private data members of class Position and adding simple accessor and mutator methods, also commonly called getters and setters, provides a simple remedy to exposing raw data items. In the example code below, the setter methods appropriately screen the internal values of latitude and longitude. Rather than throw an exception, I specify performing modulo arithmetic on input values to keep the internal values within specified ranges. For example, attempting to set the latitude to 181.0 results in an internal setting of -179.0 for latitude.

The following code adds getter and setter methods for accessing the private data members latitude and longitude:

public class Position { public Position( double latitude, double longitude ) { setLatitude( latitude ); setLongitude( longitude ); } public void setLatitude( double latitude ) { // Ensure -90 <= latitude <= 90 using modulo arithmetic. // Code not shown. // Then set instance variable. this.latitude = latitude; } public void setLongitude( double longitude ) { // Ensure -180 < longitude <= 180 using modulo arithmetic. // Code not shown. // Then set instance variable. this.longitude = longitude; } public double getLatitude() { return latitude; } public double getLongitude() { return longitude; } public double distance( Position position ) { // Calculate and return the distance from this object to the specified // position. // Code not shown. } public double heading( Position position ) { // Calculate and return the heading from this object to the specified // position. } private double latitude; private double longitude; } 

Using the above version of Position requires only minor changes. As a first change, since the above code specifies a constructor that takes two double arguments, the default constructor is no longer available. The following example uses the new constructor, as well as the new getter methods. The output remains the same as in the first example.

Position myHouse = new Position( 36.538611, -121.797500 ); Position coffeeShop = new Position( 36.539722, -121.907222 ); double distance = myHouse.distance( coffeeShop ); double heading = myHouse.heading( coffeeShop ); System.out.println ( "From my house at (" + myHouse.getLatitude() + ", " + myHouse.getLongitude() + ") to the coffee shop at (" + coffeeShop.getLatitude() + ", " + coffeeShop.getLongitude() + ") is a distance of " + distance + " at a heading of " + heading + " degrees." ); 

Choosing to restrict the acceptable values of latitude and longitude through setter methods is strictly a design decision. Encapsulation does not play a role. That is, encapsulation, as manifested in the Java language, does not guarantee protection of internal data. As a developer, you are free to expose the internals of your class. Nevertheless, you should restrict access and modification of internal data items through the use of getter and setter methods.

Isolating potential change

Protecting internal data is only one of many concerns driving design decisions on top of language encapsulation. Isolation to change is another. Modifying the internal structure of a class should not, if at all possible, affect client classes.

Zum Beispiel habe ich zuvor bemerkt, dass die Entfernungsberechnung in der Klasse Positionkeine Einheiten anzeigt. Um nützlich zu sein, benötigt die gemeldete Entfernung von 6,09 von meinem Haus zum Café eindeutig eine Maßeinheit. Ich weiß vielleicht, in welche Richtung ich gehen soll, aber ich weiß nicht, ob ich 6,09 Meter laufen, 6,09 Meilen fahren oder 6,09 Tausend Kilometer fliegen soll.