Dois-je toujours encapsuler entièrement une structure de données interne?

11

Veuillez considérer cette classe:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

Cette classe expose le tableau qu'il utilise pour stocker des données, à tout code client intéressé.

Je l'ai fait dans une application sur laquelle je travaille. J'ai eu une ChordProgressionclasse qui stocke une séquence de Chords (et fait d'autres choses). Il avait une Chord[] getChords()méthode qui renvoyait le tableau des accords. Lorsque la structure de données a dû changer (d'un tableau à une liste de tableaux), tout le code client s'est cassé.

Cela m'a fait penser - peut-être que l'approche suivante est meilleure:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThing(int index){
        return things[index];
    }

    public int getDataSize(){
        return things.length;
    }

    public void setThing(int index, Thing thing){
        things[index] = thing;
    }

}

Au lieu d'exposer la structure de données elle-même, toutes les opérations offertes par la structure de données sont désormais proposées directement par la classe qui l'entoure, en utilisant des méthodes publiques qui délèguent à la structure de données.

Lorsque la structure des données change, seules ces méthodes doivent changer - mais après cela, tout le code client fonctionne toujours.

Notez que les collections plus complexes que les tableaux peuvent nécessiter que la classe englobante implémente encore plus de trois méthodes juste pour accéder à la structure de données interne.


Cette approche est-elle courante? Que pensez-vous de cela? Quels sont ses inconvénients? Est-il raisonnable que la classe englobante implémente au moins trois méthodes publiques juste pour déléguer à la structure de données interne?

Aviv Cohn
la source

Réponses:

14

Code comme:

   public Thing[] getThings(){
        return things;
    }

Cela n'a pas beaucoup de sens, car votre méthode d'accès ne fait que renvoyer directement la structure de données interne. Vous pourriez tout aussi bien déclarer Thing[] thingsêtre public. L'idée derrière une méthode d'accès est de créer une interface qui isole les clients des changements internes et les empêche de manipuler la structure de données réelle, sauf de manière discrète comme le permet l'interface. Comme vous l'avez découvert lorsque tout votre code client s'est cassé, votre méthode d'accès ne l'a pas fait - c'est juste du code gaspillé. Je pense que beaucoup de programmeurs ont tendance à écrire du code comme ça parce qu'ils ont appris quelque part que tout doit être encapsulé avec des méthodes d'accès - mais c'est pour les raisons que j'ai expliquées. Le faire simplement pour «suivre le formulaire» lorsque la méthode d'accès ne sert à rien n'est que du bruit.

Je recommanderais certainement votre solution proposée, qui atteint certains des objectifs les plus importants de l'encapsulation: Donner aux clients une interface robuste et discrète qui les isole des détails d'implémentation interne de votre classe et ne leur permet pas de toucher la structure de données interne attendez de la manière que vous jugez appropriée - "la loi du moindre privilège". Si vous regardez les grands frameworks OOP populaires, tels que le CLR, le STL, le VCL, le modèle que vous avez proposé est répandu, pour exactement cette raison.

Faut-il toujours faire ça? Pas nécessairement. Par exemple, si vous avez des classes d'assistance ou d'amis qui sont essentiellement un composant de votre classe de travail principale et qui ne sont pas "orientées vers l'avant", ce n'est pas nécessaire - c'est une surpuissance qui va ajouter beaucoup de code inutile. Et dans ce cas, je n'utiliserais pas du tout de méthode d'accès - c'est insensé, comme expliqué. Déclarez simplement la structure de données d'une manière qui ne s'applique qu'à la classe principale qui l'utilise - la plupart des langues prennent en charge des façons de le faire - friend, ou la déclarez dans le même fichier que la classe de travail principale, etc.

Le seul inconvénient que je peux voir dans votre proposition, c'est que c'est plus de travail à coder (et maintenant vous allez devoir recoder vos classes de consommateurs - mais vous devez / devez le faire de toute façon.) Mais ce n'est pas vraiment un inconvénient - vous devez le faire correctement, et cela prend parfois plus de travail.

L'une des choses qui rendent un bon programmeur bon, c'est qu'il sait quand le travail supplémentaire en vaut la peine et quand il ne l'est pas. À long terme, le fait de verser un supplément maintenant rapportera de gros dividendes à l'avenir - sinon sur ce projet, puis sur d'autres. Apprenez à coder la bonne façon et à utiliser votre tête à ce sujet, pas seulement à suivre de manière robotique les formulaires prescrits.

Notez que les collections plus complexes que les tableaux peuvent nécessiter que la classe englobante implémente encore plus de trois méthodes juste pour accéder à la structure de données interne.

Si vous exposez une structure de données entière à travers une classe contenante, OMI, vous devez réfléchir à la raison pour laquelle cette classe est encapsulée, si ce n'est pas simplement pour fournir une interface plus sûre - une "classe wrapper". Vous dites que la classe conteneur n'existe pas à cette fin - alors peut-être qu'il y a quelque chose qui ne va pas dans votre conception. Pensez à diviser vos classes en modules plus discrets et à les superposer.

Une classe doit avoir un objectif clair et discret, et fournir une interface pour prendre en charge cette fonctionnalité - pas plus. Vous essayez peut-être de regrouper des choses qui ne vont pas ensemble. Lorsque vous faites cela, les choses se brisent à chaque fois que vous devez mettre en œuvre un changement. Plus vos classes sont petites et discrètes, plus il est facile de changer les choses: pensez à LEGO.

Vecteur
la source
1
Merci de répondre. Une question: qu'en est-il si la structure de données interne a, peut-être, 5 méthodes publiques - qui doivent toutes être présentées par l'interface publique de ma classe? Par exemple, un Java ArrayList a les méthodes suivantes: get(index), add(), size(), remove(index)et remove(Object). En utilisant la technique proposée, la classe qui contient cette ArrayList doit avoir cinq méthodes publiques juste pour déléguer à la collection interne. Et le but de cette classe dans le programme est très probablement de ne pas encapsuler cette ArrayList, mais plutôt de faire autre chose. L'ArrayList n'est qu'un détail. [...]
Aviv Cohn
La structure de données interne est juste un membre ordinaire, qui en utilisant la technique ci-dessus - nécessite qu'elle contienne la classe pour présenter cinq méthodes publiques supplémentaires. À votre avis - est-ce raisonnable? Et aussi - est-ce courant?
Aviv Cohn
@Prog - Et si la structure de données interne a, peut-être, 5 méthodes publiques ... IMO si vous trouvez que vous devez envelopper une classe d'assistance entière dans votre classe principale et l'exposer de cette façon, vous devez repenser que conception - votre classe publique en fait trop et / ou ne présente pas l'interface appropriée. Une classe doit avoir un rôle très discret et clairement défini, et son interface doit prendre en charge ce rôle et uniquement ce rôle. Pensez à diviser et à superposer vos cours. Une classe ne doit pas être un "évier de cuisine" qui contient toutes sortes d'objets au nom de l'encapsulation.
Vector
Si vous exposez une structure de données entière via une classe wrapper, IMO, vous devez réfléchir à la raison pour laquelle cette classe est encapsulée si ce n'est pas simplement pour fournir une interface plus sûre. Vous dites que la classe conteneur n'existe pas à cette fin - il n'y a donc rien de bien dans cette conception.
Vector
1
@Phoshi - En lecture seule est le mot - clé - je suis d'accord avec cela. Mais l'OP ne parle pas de lecture seule. par exemple removen'est pas en lecture seule. Je crois comprendre que le PO veut tout rendre public - comme dans le code d'origine avant le changement proposé. public Thing[] getThings(){return things;}C'est ce que je n'aime pas.
Vector
2

Vous avez demandé: Dois-je toujours encapsuler entièrement une structure de données interne?

Réponse brève: Oui, la plupart du temps mais pas toujours .

Réponse longue: Je pense que les classes suivent les catégories suivantes:

  1. Classes qui encapsulent des données simples. Exemple: point 2D. Il est facile de créer des fonctions publiques qui permettent d'obtenir / de définir les coordonnées X et Y, mais vous pouvez masquer facilement les données internes sans trop de problèmes. Pour de telles classes, il n'est pas nécessaire d'exposer les détails de la structure de données interne.

  2. Classes de conteneur qui encapsulent des collections. STL a les classes de conteneurs classiques. Je considère std::stringet std::wstringparmi eux aussi. Ils fournissent une interface riche pour faire face aux abstractions , mais std::vector, std::stringet std::wstringfournira également la possibilité d'obtenir l' accès aux données brutes. Je ne serais pas pressé de les appeler des classes mal conçues. Je ne connais pas la justification de ces classes exposant leurs données brutes. Cependant, dans mon travail, j'ai trouvé nécessaire d'exposer les données brutes lors du traitement de millions de nœuds de maillage et de données sur ces nœuds de maillage pour des raisons de performances.

    La chose importante à propos de l'exposition de la structure interne d'une classe est que vous devez réfléchir longuement et sérieusement avant de lui donner un signal vert. Si l'interface est interne à un projet, il sera coûteux de la changer à l'avenir mais pas impossible. Si l'interface est externe au projet (par exemple lorsque vous développez une bibliothèque qui sera utilisée par d'autres développeurs d'applications), il peut être impossible de modifier l'interface sans perdre vos clients.

  3. Les classes qui sont principalement de nature fonctionnelle. Exemples: std::istream, std::ostream, itérateurs des conteneurs STL. C'est carrément stupide d'exposer les détails internes de ces classes.

  4. Classes hybrides. Ce sont des classes qui encapsulent une certaine structure de données mais fournissent également des fonctionnalités algorithmiques. Personnellement, je pense que c'est le résultat d'une conception mal pensée. Cependant, si vous les trouvez, vous devez décider s'il est judicieux d'exposer leurs données internes au cas par cas.

En conclusion: La seule fois où j'ai trouvé nécessaire d'exposer la structure de données interne d'une classe, c'est lorsqu'elle est devenue un goulot d'étranglement des performances.

R Sahu
la source
Je pense que la raison la plus importante pour laquelle STL expose ses données internes est la compatibilité avec toutes les fonctions qui attendent des pointeurs, ce qui est beaucoup.
Siyuan Ren
0

Au lieu de renvoyer directement les données brutes, essayez quelque chose comme ceci

class ClassA {
  private Things[] things;
  ...
  public Things[] asArray() { return things; }
  public List<Thing> asList() { ... }
  ...
}

Donc, vous fournissez essentiellement une collection personnalisée qui présente tout ce que le visage est souhaité. Dans votre nouvelle implémentation alors,

class ClassA {
  private List<Thing> things;
  ...
  public Things[] asArray() { return things.asArray(); }
  public List<Thing> asList() { return things; }
  ...
}

Vous disposez maintenant de l'encapsulation appropriée, masquez les détails de l'implémentation et fournissez une compatibilité descendante (moyennant un coût).

BobDalgleish
la source
Idée intelligente pour une compatibilité descendante. Mais: Maintenant que vous avez l'encapsulation appropriée, masquez les détails de l'implémentation - pas vraiment. Les clients doivent encore faire face aux nuances de List. Les méthodes d'accès qui renvoient simplement des membres de données, même avec un cast pour rendre les choses plus robustes, ne sont pas vraiment une bonne IMO d'encapsulation. La classe des travailleurs devrait gérer tout cela, pas le client. Plus le client doit être «bête», plus il sera robuste. En passant, je ne suis pas sûr que vous ayez répondu à la question ...
Vector
1
@Vector - vous avez raison. La structure de données renvoyée est toujours modifiable et les effets secondaires tueront les informations qui se cachent.
BobDalgleish
La structure de données renvoyée est toujours modifiable et les effets secondaires tueront les informations qui se cachent - oui, cela aussi - c'est dangereux. Je pensais simplement en termes de ce qui est exigé du client, qui était au centre de la question.
Vector
@BobDalgleish: pourquoi ne pas retourner une copie de la collection originale?
Giorgio
1
@BobDalgleish: À moins qu'il n'y ait de bonnes raisons de performances, j'envisagerais de renvoyer une référence aux structures de données internes pour permettre à ses utilisateurs de les modifier comme une très mauvaise décision de conception. L'état interne d'un objet ne doit être modifié que par des méthodes publiques appropriées.
Giorgio
0

Vous devez utiliser des interfaces pour ces choses. N'aiderait pas dans votre cas, car le tableau de Java n'implémente pas ces interfaces, mais vous devriez le faire à partir de maintenant:

class ClassA{

    public ClassA(){
        things = new ArrayList<Thing>();
    }

    private List<Thing> things; // stores data

    // stuff omitted

    public List<Thing> getThings(){
        return things;
    }

}

De cette façon, vous pouvez passer ArrayListà LinkedListou autre chose, et vous ne casserez aucun code car toutes les collections Java (à l'exception des tableaux) qui ont un accès (pseudo?) Aléatoire seront probablement implémentées List.

Vous pouvez également utiliser Collection, qui offrent moins de méthodes que Listmais peuvent prendre en charge les collections sans accès aléatoire, ou Iterablequi peuvent même prendre en charge les flux mais n'offrent pas beaucoup en termes de méthodes d'accès ...

Idan Arye
la source
-1 - mauvais compromis et IMO pas particulièrement sûr: vous exposez la structure de données interne au client, vous la masquez et vous espérez le meilleur car "les collections Java ... implémenteront probablement List". Si votre solution était vraiment polymorphe / basée sur l'héritage - que toutes les collections implémentent invariablement en Listtant que classes dérivées, cela aurait plus de sens, mais simplement "espérer le meilleur" n'est pas une bonne idée. "Un bon programmeur regarde dans les deux sens dans une rue à sens unique".
Vector
@Vector Oui, je suppose que les futures collections Java seront mises en œuvre List(ou Collection, ou au moins Iterable). C'est tout l'intérêt de ces interfaces, et c'est dommage que les tableaux Java ne les implémentent pas, mais ce sont des interfaces officielles pour les collections en Java, il n'est donc pas si exagéré de supposer qu'une collection Java les implémentera - à moins que cette collection ne soit plus ancienne que List, et dans ce cas, il est très facile de l'envelopper avec AbstractList .
Idan Arye
Vous dites que votre hypothèse est pratiquement garantie, alors OK - je supprimerai le vote négatif (quand j'y serai autorisé) parce que vous étiez assez décent pour expliquer, et je ne suis pas un gars Java sauf par osmose. Pourtant, je ne soutiens pas cette idée d'exposer la structure de données interne, quelle que soit la façon dont cela est fait, et vous n'avez pas directement répondu à la question de l'OP, qui concerne vraiment l'encapsulation. c'est-à-dire limiter l'accès à la structure interne des données.
Vector
1
@Vector Oui, les utilisateurs peuvent diffuser le contenu Listdans ArrayList, mais ce n'est pas comme si l'implémentation pouvait être protégée à 100% - vous pouvez toujours utiliser la réflexion pour accéder aux champs privés. Cela est mal vu, mais le casting est également mal vu (pas autant que cela). Le point d'encapsulation n'est pas d'empêcher le piratage malveillant - c'est plutôt d'empêcher les utilisateurs de dépendre des détails d'implémentation que vous voudrez peut-être modifier. L'utilisation de l' Listinterface fait exactement cela - les utilisateurs de la classe peuvent dépendre de l' Listinterface au lieu de la ArrayListclasse concrète qui pourrait changer.
Idan Arye
vous pouvez toujours utiliser la réflexion pour accéder à des champs privés certainement - si quelqu'un veut écrire du mauvais code et renverser une conception, il peut le faire. c'est plutôt pour empêcher les utilisateurs ... - c'est une des raisons de l'encapsulation. Une autre consiste à assurer l'intégrité et la cohérence de l'état interne de votre classe. Le problème n'est pas un "piratage malveillant", mais une mauvaise organisation qui mène à des bugs désagréables. "Loi du privilège le moins nécessaire" - ne donnez au consommateur de votre classe que ce qui est obligatoire - pas plus. S'il est obligatoire de rendre publique une structure de données interne entière, vous avez un problème de conception.
Vector
-2

C'est assez courant pour cacher votre structure de données interne du monde extérieur. Parfois, il est exagéré spécialement dans DTO. Je le recommande pour le modèle de domaine. S'il est nécessaire de l'exposer, renvoyez la copie immuable. Parallèlement à cela, je suggère de créer une interface ayant ces méthodes comme obtenir, définir, supprimer, etc.

VGaur
la source
1
cela ne semble pas offrir quelque chose de substantiel sur 3 réponses précédentes
moucher