L'encapsulation ne cache pas l'information

Les mots sont glissants. Comme Humpty Dumpty a proclamé dans Through the Looking Glass de Lewis Carroll , "Quand j'utilise un mot, cela signifie exactement ce que je choisis de le dire - ni plus ni moins." Il est certain que l'usage courant des mots encapsulation et dissimulation d'informations semble suivre cette logique. Les auteurs font rarement la distinction entre les deux et prétendent souvent directement qu'ils sont identiques.

Cela en fait-il le cas? Pas pour moi. Si c'était simplement une question de mots, je n'écrirais pas un autre mot à ce sujet. Mais il y a deux concepts distincts derrière ces termes, des concepts engendrés séparément et mieux compris séparément.

L'encapsulation fait référence au regroupement de données avec les méthodes qui opèrent sur ces données. Souvent, cette définition est mal interprétée comme signifiant que les données sont en quelque sorte cachées. En Java, vous pouvez avoir des données encapsulées qui ne sont pas du tout masquées.

Cependant, cacher des données n'est pas la pleine mesure de la dissimulation d'informations. David Parnas a introduit pour la première fois le concept de dissimulation d'informations vers 1972. Il a fait valoir que les principaux critères de modularisation du système devraient concerner le masquage des décisions de conception critiques. Il a insisté sur le fait de cacher «des décisions de conception difficiles ou des décisions de conception susceptibles de changer». Cacher les informations de cette manière empêche les clients d'exiger une connaissance approfondie de la conception pour utiliser un module, et des effets de la modification de ces décisions.

Dans cet article, j'explore la distinction entre l'encapsulation et le masquage d'informations grâce au développement d'exemples de code. La discussion montre comment Java facilite l'encapsulation et étudie les ramifications négatives de l'encapsulation sans masquage des données. Les exemples montrent également comment améliorer la conception des classes grâce au principe du masquage des informations.

Classe de position

Avec une prise de conscience croissante du vaste potentiel de l'Internet sans fil, de nombreux experts s'attendent à ce que les services basés sur la localisation offrent l'opportunité de la première application tueur sans fil. Pour l'exemple de code de cet article, j'ai choisi une classe représentant l'emplacement géographique d'un point sur la surface de la terre. En tant qu'entité de domaine, la classe, nommée Position, représente les informations du système de positionnement global (GPS). Une première coupe à la classe semble aussi simple que:

Classe publique Position {double latitude publique; double longitude publique; }

La classe contient deux éléments de données: GPS latitudeet longitude. À l'heure actuelle, ce Positionn'est rien de plus qu'un petit sac de données. Néanmoins, Positionest une classe et les Positionobjets peuvent être instanciés à l'aide de la classe. Pour utiliser ces objets, la classe PositionUtilitycontient des méthodes pour calculer la distance et le cap - c'est-à-dire la direction - entre les Positionobjets spécifiés :

public class PositionUtility {double distance statique publique (Position position1, Position position2) {// Calcule et renvoie la distance entre les positions spécifiées. } double titre statique public (Position position1, Position position2) {// Calcule et renvoie le cap de la position1 à la position2. }}

J'omets le code d'implémentation réel pour les calculs de distance et de cap.

Le code suivant représente une utilisation typique de Positionet PositionUtility:

// Créer une position représentant ma maison Position myHouse = new Position (); myHouse.latitude = 36,538611; myHouse.longitude = -121,797500; // Créer une position représentant un café local Position coffeeShop = new Position (); coffeeShop.latitude = 36,539722; coffeeShop.longitude = -121,907222; // Utilisez un PositionUtility pour calculer la distance et le cap de ma maison // au café local. double distance = PositionUtility.distance (myHouse, coffeeShop); double titre = PositionUtility.heading (myHouse, coffeeShop); // Résultats d'impression System.out.println ("De ma maison à (" + maMaison.latitude + "," + maMaison.longitude + ") au café à (" + coffeeShop.latitude + "," + coffeeShop. longitude + ") est une distance de" + distance + "à un cap de" + cap + "degrés." );

Le code génère la sortie ci-dessous, qui indique que le café est plein ouest (270,8 degrés) de ma maison à une distance de 6,09. Une discussion ultérieure aborde le manque d'unités de distance.

=================================================== ================= De ma maison à (36.538611, -121.7975) au café à (36.539722, -121.907222) est une distance de 6.0873776351893385 au cap de 270.7547022304523 degrés. =================================================== ==================

Position,, PositionUtilityet leur utilisation du code est un peu inquiétante et certainement pas très orientée objet. Mais comment cela peut-il être? Java est un langage orienté objet, et le code utilise des objets!

Bien que le code puisse utiliser des objets Java, il le fait d'une manière qui rappelle une époque révolue: des fonctions utilitaires fonctionnant sur des structures de données. Bienvenue en 1972! Alors que le président Nixon se blottissait autour d'enregistrements secrets, les professionnels de l'informatique codant dans le langage procédural Fortran utilisaient avec enthousiasme la nouvelle bibliothèque internationale de mathématiques et de statistiques (IMSL) de cette manière. Les référentiels de code tels que IMSL étaient remplis de fonctions pour les calculs numériques. Les utilisateurs transmettaient des données à ces fonctions dans de longues listes de paramètres, qui incluaient parfois non seulement les structures de données d'entrée mais également de sortie. (IMSL a continué d'évoluer au fil des ans, et une version est désormais disponible pour les développeurs Java.)

Dans la conception actuelle, Positionest une structure de données simple et PositionUtilityest un référentiel de style IMSL de fonctions de bibliothèque qui opère sur les Positiondonnées. Comme le montre l'exemple ci-dessus, les langages modernes orientés objet n'empêchent pas nécessairement l'utilisation de techniques procédurales désuètes.

Regroupement des données et des méthodes

Le code peut être facilement amélioré. Pour commencer, pourquoi placer les données et les fonctions qui opèrent sur ces données dans des modules séparés? Les classes Java permettent de regrouper les données et les méthodes:

public class Position {public double distance (Position position) {// Calcule et renvoie la distance de cet objet à la position // spécifiée. } public double head (Position position) {// Calcule et renvoie l'en-tête de cet objet à la position // spécifiée. } double latitude publique; double longitude publique; }

Le fait de placer les éléments de données de position et le code d'implémentation pour calculer la distance et le cap dans la même classe évite le besoin d'une PositionUtilityclasse séparée . PositionCommence maintenant à ressembler à une véritable classe orientée objet. Le code suivant utilise cette nouvelle version qui regroupe les données et les méthodes:

Position myHouse = nouvelle position (); myHouse.latitude = 36,538611; myHouse.longitude = -121,797500; Position coffeeShop = nouvelle position (); coffeeShop.latitude = 36,539722; coffeeShop.longitude = -121,907222; double distance = myHouse.distance (coffeeShop); double titre = myHouse.heading (coffeeShop); System.out.println ("De ma maison à (" + maMaison.latitude + "," + maMaison.longitude + ") au café à (" + coffeeShop.latitude + "," + coffeeShop.longitude + ") est une distance de "+ distance +" à un cap "+ cap +" degrés. ");

La sortie est identique à la précédente, et plus important encore, le code ci-dessus semble plus naturel. La version précédente transmettait deux Positionobjets à une fonction dans une classe d'utilité distincte pour calculer la distance et le cap. Dans ce code, le calcul du cap avec l'appel de méthode util.heading( myHouse, coffeeShop )n'indiquait pas clairement la direction du calcul. Un développeur doit se rappeler que la fonction utilitaire calcule l'en-tête du premier paramètre au second.

En comparaison, le code ci-dessus utilise l'instruction myHouse.heading(coffeeShop)pour calculer le même titre. La sémantique de l'appel indique clairement que la direction va de ma maison au café. La conversion de la fonction heading(Position, Position)à deux arguments en fonction à un argument position.heading(Position)est appelée currying de la fonction. Currying spécialise efficacement la fonction sur son premier argument, ce qui donne une sémantique plus claire.

Mise en place des méthodes utilisant des Positiondonnées de classe dans la Positionclasse elle - même fait taitement les fonctions distanceet headingpossible. Changer la structure d'appel des fonctions de cette manière est un avantage significatif par rapport aux langages procéduraux. La classe Positionreprésente désormais un type de données abstrait qui encapsule les données et les algorithmes qui fonctionnent sur ces données. En tant que type défini par l'utilisateur, les Positionobjets sont également des citoyens de première classe qui bénéficient de tous les avantages du système de type de langage Java.

La fonction de langage qui regroupe les données avec les opérations effectuées sur ces données est l'encapsulation. Notez que l'encapsulation ne garantit ni la protection des données ni le masquage des informations. L'encapsulation n'assure pas non plus une conception de classe cohérente. Pour atteindre ces attributs de conception de qualité, il faut des techniques allant au-delà de l'encapsulation fournie par le langage. Telle qu'actuellement implémentée, la classe Positionne contient pas de données et de méthodes superflues ou non liées, mais Positionexpose les deux latitudeet longitudesous forme brute. Cela permet à n'importe quel client de la classe Positionde modifier directement l'une ou l'autre des données internes sans aucune intervention de Position. De toute évidence, l'encapsulation ne suffit pas.

Programmation défensive

Pour étudier plus en détail les ramifications de l'exposition d'éléments de données internes, supposons que je décide d'ajouter un peu de programmation défensive Positionen limitant la latitude et la longitude aux plages spécifiées par le GPS. La latitude se situe dans la plage [-90, 90] et la longitude dans la plage (-180, 180]. L'exposition des éléments de données latitudeet longitudedans Positionl'implémentation actuelle de s rend cette programmation défensive 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.

Par exemple, j'ai noté précédemment que le calcul de la distance en classe Positionn'indiquait pas les unités. Pour être utile, la distance signalée de 6,09 entre ma maison et le café a clairement besoin d'une unité de mesure. Je connais peut-être la direction à prendre, mais je ne sais pas si je dois marcher 6,09 mètres, parcourir 6,09 milles ou parcourir 6,09 mille kilomètres.