L'incapsulamento non è nascondere le informazioni

Le parole sono scivolose. Come Humpty Dumpty ha proclamato in Through the Looking Glass di Lewis Carroll , "Quando uso una parola, significa esattamente quello che scelgo di significare - né più né meno." Certamente l'uso comune delle parole incapsulamento e occultamento delle informazioni sembra seguire questa logica. Gli autori distinguono raramente tra i due e spesso affermano direttamente che sono la stessa cosa.

Questo lo rende così? Non per me. Se fosse solo questione di parole, non scriverei un'altra parola sull'argomento. Ma ci sono due concetti distinti dietro questi termini, concetti generati separatamente e meglio compresi separatamente.

L'incapsulamento si riferisce al raggruppamento dei dati con i metodi che operano su tali dati. Spesso tale definizione viene interpretata erroneamente nel senso che i dati sono in qualche modo nascosti. In Java, puoi avere dati incapsulati che non sono affatto nascosti.

Tuttavia, l'occultamento dei dati non è l'intera portata dell'occultamento delle informazioni. David Parnas ha introdotto per la prima volta il concetto di occultamento delle informazioni intorno al 1972. Ha sostenuto che i criteri primari per la modularizzazione del sistema dovrebbero riguardare l'occultamento delle decisioni critiche di progettazione. Ha sottolineato di nascondere "decisioni di progettazione difficili o decisioni di progettazione che probabilmente cambieranno". Nascondere le informazioni in questo modo isola i clienti dal richiedere una conoscenza approfondita del progetto per utilizzare un modulo e dagli effetti del cambiamento di tali decisioni.

In questo articolo, esploro la distinzione tra incapsulamento e informazioni nascoste attraverso lo sviluppo di codice di esempio. La discussione mostra come Java faciliti l'incapsulamento e indaga le ramificazioni negative dell'incapsulamento senza nascondere i dati. Gli esempi mostrano anche come migliorare il design delle classi attraverso il principio dell'occultamento delle informazioni.

Classe di posizione

Con una crescente consapevolezza del vasto potenziale di Internet wireless, molti esperti si aspettano che i servizi basati sulla posizione offrano l'opportunità per la prima app killer wireless. Per il codice di esempio di questo articolo, ho scelto una classe che rappresenta la posizione geografica di un punto sulla superficie terrestre. In quanto entità di dominio, la classe, denominata Position, rappresenta le informazioni GPS (Global Position System). Un primo taglio in classe sembra semplice come:

public class Posizione {public double latitude; doppia longitudine pubblica; }

La classe contiene due elementi di dati: GPS latitudee longitude. Al momento, Positionnon è altro che un piccolo sacchetto di dati. Tuttavia, Positionè una classe e gli Positionoggetti possono essere istanziati utilizzando la classe. Per utilizzare questi oggetti, la classe PositionUtilitycontiene metodi per calcolare la distanza e la direzione, ovvero la direzione, tra Positionoggetti specificati :

public class PositionUtility {public static double distance (Position position1, Position position2) {// Calcola e restituisce la distanza tra le posizioni specificate. } doppia intestazione statica pubblica (Posizione posizione1, Posizione posizione2) {// Calcola e riporta l'intestazione dalla posizione1 alla posizione2. }}

Ometto il codice di implementazione effettivo per i calcoli della distanza e della direzione.

Il codice seguente rappresenta un uso tipico di Positione PositionUtility:

// Crea una posizione che rappresenti la mia casa Position myHouse = new Position (); myHouse.latitude = 36.538611; myHouse.longitude = -121.797500; // Crea una posizione che rappresenta un bar locale Position coffeeShop = new Position (); coffeeShop.latitude = 36.539722; coffeeShop.longitude = -121.907222; // Usa PositionUtility per calcolare la distanza e la direzione da casa mia // al bar locale. doppia distanza = PositionUtility.distance (myHouse, coffeeShop); doppia intestazione = PositionUtility.heading (myHouse, coffeeShop); // Stampa i risultati System.out.println ("Da casa mia a (" + myHouse.latitude + "," + myHouse.longitude + ") alla caffetteria a (" + coffeeShop.latitude + "," + coffeeShop. longitudine + ") è una distanza di" + distanza + "in corrispondenza di un'intestazione di" + rotta + "gradi." );

Il codice genera l'output di seguito, che indica che il bar è a ovest (270,8 gradi) di casa mia a una distanza di 6,09. La discussione successiva affronta la mancanza di unità di distanza.

================================================== ================= Da casa mia a (36.538611, -121.7975) alla caffetteria a (36.539722, -121.907222) c'è una distanza di 6.0873776351893385 a un'intestazione di 270.7547022304523 gradi. ================================================== =================

Position, PositionUtilitye il loro utilizzo del codice è un po 'inquietante e certamente non molto orientato agli oggetti. Ma come può essere? Java è un linguaggio orientato agli oggetti e il codice utilizza oggetti!

Sebbene il codice possa utilizzare oggetti Java, lo fa in un modo che ricorda un'epoca passata: funzioni di utilità che operano su strutture di dati. Benvenuti nel 1972! Mentre il presidente Nixon si rannicchiava su registrazioni su nastro segreto, i professionisti del computer che codificano nel linguaggio procedurale Fortran hanno utilizzato con entusiasmo la nuova International Mathematics and Statistics Library (IMSL) proprio in questo modo. I repository di codice come IMSL erano pieni di funzioni per i calcoli numerici. Gli utenti passavano i dati a queste funzioni in lunghi elenchi di parametri, che a volte includevano non solo l'input ma anche le strutture dei dati di output. (IMSL ha continuato ad evolversi nel corso degli anni e una versione è ora disponibile per gli sviluppatori Java.)

Nella progettazione attuale, Positionè una struttura dati semplice ed PositionUtilityè un repository in stile IMSL di funzioni di libreria che opera sui Positiondati. Come mostra l'esempio sopra, i moderni linguaggi orientati agli oggetti non precludono necessariamente l'uso di tecniche procedurali antiquate.

Raggruppamento di dati e metodi

Il codice può essere facilmente migliorato. Per cominciare, perché collocare i dati e le funzioni che operano su quei dati in moduli separati? Le classi Java consentono di raggruppare dati e metodi insieme:

public class Position {public double distance (Position position) {// Calcola e restituisce la distanza da questo oggetto alla posizione // specificata. } doppia intestazione pubblica (posizione posizione) {// Calcola e riporta l'intestazione da questo oggetto alla posizione // specificata. } doppia latitudine pubblica; doppia longitudine pubblica; }

Mettere gli elementi dei dati di posizione e il codice di implementazione per il calcolo della distanza e della rotta nella stessa classe evita la necessità di una PositionUtilityclasse separata . Ora Positioninizia ad assomigliare a una vera classe orientata agli oggetti. Il codice seguente utilizza questa nuova versione che raggruppa i dati e i metodi insieme:

Posizione myHouse = nuova posizione (); myHouse.latitude = 36.538611; myHouse.longitude = -121.797500; Posizione coffeeShop = nuova posizione (); coffeeShop.latitude = 36.539722; coffeeShop.longitude = -121.907222; doppia distanza = myHouse.distance (coffeeShop); doppia intestazione = myHouse.heading (coffeeShop); System.out.println ("Da casa mia a (" + myHouse.latitude + "," + myHouse.longitude + ") alla caffetteria a (" + coffeeShop.latitude + "," + coffeeShop.longitude + ") è una distanza di "+ distanza +" a un'intestazione di "+ prua +" gradi. ");

L'output è identico a prima e, cosa più importante, il codice sopra sembra più naturale. La versione precedente passava due Positionoggetti a una funzione in una classe di utilità separata per calcolare la distanza e la direzione. In quel codice, il calcolo dell'intestazione con la chiamata al metodo util.heading( myHouse, coffeeShop )non indicava chiaramente la direzione del calcolo. Uno sviluppatore deve ricordare che la funzione di utilità calcola l'intestazione dal primo parametro al secondo.

In comparison, the above code uses the statement myHouse.heading(coffeeShop) to calculate the same heading. The call's semantics clearly indicate that the direction proceeds from my house to the coffee shop. Converting the two-argument function heading(Position, Position) to a one-argument function position.heading(Position) is known as currying the function. Currying effectively specializes the function on its first argument, resulting in clearer semantics.

Placing the methods utilizing Position class data in the Position class itself makes currying the functions distance and heading possible. Changing the call structure of the functions in this way is a significant advantage over procedural languages. Class Position now represents an abstract data type that encapsulates data and the algorithms that operate on that data. As a user-defined type, Position objects are also first class citizens that enjoy all the benefits of the Java language type system.

The language facility that bundles data with the operations that perform on that data is encapsulation. Note that encapsulation guarantees neither data protection nor information hiding. Nor does encapsulation ensure a cohesive class design. To achieve those quality design attributes requires techniques beyond the encapsulation provided by the language. As currently implemented, class Position doesn't contain superfluous or nonrelated data and methods, but Position does expose both latitude and longitude in raw form. That allows any client of class Position to directly change either internal data item without any intervention by Position. Clearly, encapsulation is not enough.

Defensive programming

To further investigate the ramifications of exposing internal data items, suppose I decide to add a bit of defensive programming to Position by restricting the latitude and longitude to ranges specified by GPS. Latitude falls in the range [-90, 90] and longitude in the range (-180, 180]. The exposure of the data items latitude and longitude in Position's current implementation renders this defensive programming impossible.

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.

Ad esempio, in precedenza ho notato che il calcolo della distanza in classe Positionnon indicava le unità. Per essere utile, la distanza riportata di 6.09 da casa mia al bar necessita chiaramente di un'unità di misura. Potrei conoscere la direzione da prendere, ma non so se camminare per 6,09 metri, guidare per 6,09 miglia o volare per 6,09 mila chilometri.